{ "info": { "author": "Dieter Maurer", "author_email": "dieter@handshake.de", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Framework :: ZODB", "Framework :: Zope2", "Framework :: Zope :: 4", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Topic :: Utilities" ], "description": "The ZODB is a mostly easy to use object oriented database -- especially,\nwhen used within a framework which provides transaction management (such\nas Zope). Nice features are the almost transparent persistency (modified\nobjects are automatically stored when the transaction is committed)\nand the absence of locking requirements (due to an optimistic\nconcurrency control). However, the ZODB becomes a bit difficult when\noperations need to be performed asynchronously, i.e. in a separate thread.\n\nThis package contains some utilities to make it easier to implement\nasynchronous access to the ZODB. Some of those can be helpful, too,\nin a synchronous environment.\n\n\nDependencies\n============\nThe package depends on ``decorator``, ``transaction``,\n``dm.transaction.aborthook`` and (``ZODB3`` (>= 3.8) or ``ZODB`` (>= 5.0)).\n\nThe module ``zope2`` depends on Zope 2 (>= 2.10) or Zope (>= 4.0b7).\n\nEasy dependencies are declared, complex ones not.\n\n\nModules\n=======\n\nThe package consists of modules ``transactional``, ``scheduler``,\n``context`` and ``zope2``. \n\nDetailed information can be found in the source via docstrings.\n\n\n``transactional``\n-----------------\n\n``transactional`` contains decorators which provide transaction management\nin environments where this is not provided by the framework.\nThey can be useful even in a synchronous environment (e.g. a script\nenvironment). The transaction management comprises automatic retry\nafter concurrency problems (which the ZODB indicates by a so\ncalled ``ConflictError``).\n\nIts main content is the decorator ``transactional``, a particular\ninstance of the class ``TransactionManager``. ``transactional``\n(and other instances of ``TransactionManager``) declare a function or method to\nbe transactional: before the function is called, a new transaction\nis begun (a potentially pending transaction aborted), metadata is registered\nfor the transaction\nand when the function returns the transaction is either committed (no exception)\nor aborted (exception). If the exception was a ``ConflictError``, the\ncall is retried up to a configurable number of times after configurable delays.\n\n``transactional`` (and other instances of ``TransactionManager``)\nhave effect only at the top level, as the ZODB does not support fully\nnested transactions (it can, however, partially emulated\nnested transactions by so called \"savepoint\"s).\nNested calls (inside the same transaction) simply\ncall the decorated function/method. The decorators recognize only\ntheir own transaction management: if the transaction is managed on higher\nlevel, this is not recognized and control is taken over.\n\nExample\n,,,,,,,\n\nIn this section, we set up a simple example that demonstrates\nhow ``transactional`` (and other instances of\n``TransactionManager``) is used and what it does.\n\nFor the sake of Python 2/Python 3 compatibility, we activate\nthe future ``print_function``.\n\n>>> from __future__ import print_function\n\n``transactional`` manages transactions. Therefore, it is\nuseful to be able to monitor transaction management.\nWe use after commit hooks (directly provided by ``transaction``)\nand abort hooks (provided by ``dm.transaction.aborthook``).\nWith them, we define the auxiliary function ``register_hooks`` which\nwill monitor transaction aborts and commits. We also set up logging\nto see logging messages.\n\n>>> import transaction\n>>> \n>>> def register_hooks(text):\n... \"\"\"register transaction hooks such that we can monitor transaction operation\"\"\"\n... def show(status, type):\n... print (\"transaction %s:\" % type, text)\n... T = transaction.get() # current transaction\n... T.addAfterCommitHook(show, (\"commit\",))\n... T.addAfterAbortHook(show, (False, \"abort\"))\n... \n>>> from logging import basicConfig\n>>> basicConfig()\n\nWe now define two simple transactional functions ``f`` and ``g``\nwith ``f`` calling ``g`` and then call ``f``.\n\n>>> @transactional\n... def f(a=1, b=2):\n... register_hooks(\"f\")\n... print (\"f:\", a, b)\n... g(2*a)\n... print (\"after g call\")\n... return a + b\n... \n>>> @transactional\n... def g(x):\n... register_hooks(\"g\")\n... print (\"g:\", x)\n... \n>>> f()\nf: 1 2\ng: 2\nafter g call\ntransaction commit: f\ntransaction commit: g\n3\n\nThe output tells us, that transaction commit hooks have been\ncalled. This means that some transaction has been commited.\nIn addition, the ``g`` transaction commit hook was not called at\nthe end of the ``g`` call but at the end of the ``f`` call.\nThis means that the ``g`` call has not introduced its own transaction\nlevel but participates on that of ``f`` -- even though, ``g`` has\nbe declared transactional. When we call ``g`` directly, we see that\nin this case, it gets its own transaction control.\n\n>>> g(1)\ng: 1\ntransaction commit: g\n\n\nShould a transactional method raise an exception, the transaction\nis aborted and the exception is propagated:\n\n>>> @transactional\n... def raise_exception():\n... register_hooks(\"raise exception\")\n... print (\"raise exception\")\n... raise ValueError()\n... \n>>>> raise_exception()\nraise exception\ntransaction abort: raise exception\nTraceback (most recent call last):\n ...\nValueError\n\n\nIn case of a ``ConflictError``, the call is automatically retried\n(in a new transaction). Retrial may be repeated (how often is\ncontrolled by a ``TransactionManager`` attribute) with increasing\nrandomly chosen delays between retries (also controlled\nby ``TransactionManager`` attributes).\n\nFor demonstrational purposes, we define\na class for with the first call raises ``ConflictError`` and the second\ncall succeeds. Therefore, the first retrial succeeds and the example\nwill not show further retrials.\n\n>>> class ConflictRaiser(object):\n... raised = False\n... \n... @transactional\n... def __call__(self):\n... register_hooks(\"conflict raiser\")\n... if self.raised: print (\"conflict raiser returns without exception\")\n... else:\n... print (\"conflict raiser raises `ConflictError`\")\n... self.raised = True\n... from ZODB.POSException import ConflictError\n... raise ConflictError()\n... \n>>> cr = ConflictRaiser()\n>>> cr()\nconflict raiser raises `ConflictError`\ntransaction abort: conflict raiser\nERROR:dm.zodb.asynchronous.transactional:retrying __call__\nTraceback (most recent call last):\n ...\nConflictError: database conflict error\nconflict raiser returns without exception\ntransaction commit: conflict raiser\n\n\n\n\n``scheduler``\n-------------\n\nThis module defines the class ``TransactionalScheduler`` which supports\nthe following use case: some context starts an operation in a separate\nthread and then terminates; a different context later checks whether\nthe operation has completed and if so processes the results.\nThe use case arises for example in a web application (such as Zope) for\nlong running operations which should be processed asynchronously\n(in a separate thread) rather than inline (in the originating request)\nto provide useful partial results or feedback immediately. Later results\nare fetched and presented e.g. via dynamic (AJAX, Web 2) techniques.\n\nThe initial schedule returns an identifier which can later be used\nto check for and access results.\n\nThe function becomes nontrivial when the operation must access the ZODB.\nThe ZODB forbids a thread to access persistent objects loaded in\na separate thread. Therefore, persistent objects accessed asynchronously\nmust be reloaded from the ZODB via a new thread specific connection.\nWithout special measures, the asynchronous operation may not see modifications\nto persistent objects performed by the context which has scheduled\nthe asynchonous operation (as they become available only after the\ntransaction has committed). ``TransactionScheduler`` uses the\n``after-commit`` hook of ZODB transactions to start the asynchronous\noperation ensuring that modifications are seen.\n\nWhen the result of an asynchronous operation is fetched, its deletion\nis automatically scheduled at transaction commit. A deletion timeout\ncontrols deletion of results which got \"forgotten\".\n\n``TransactionalScheduler`` maintains its schedules in RAM. It is therefore\nimportant that the ``schedule`` and ``get_result`` methods are\ncalled in the same process (such that they see the same RAM content).\nAs a conseqeunce, \nin a replicated web application context the requests with ``get_result`` \ncalls must arrive at the same web application process as the former\nrequest which called the ``schedule``.\n\nAs an alternative, this module defines the class\n``PersistentTransactionalScheduler`` whose instances store the schedules\nin itself and thereby in the ZODB. For details, read its docstring.\n\n\nExample\n,,,,,,,\n\nThis example demonstrates the working of the\n``TransactionalScheduler``. We set up logging, a scheduler (``s``)\nand a simple function (``show``)\nwith prints something and returns something so that we can monitor\nwhen it is called.\n\n>>> from transaction import abort, commit\n>>> from logging import basicConfig\n>>> basicConfig()\n>>>\n>>> import dm.zodb.asynchronous.scheduler\n>>> s = dm.zodb.asynchronous.scheduler.TransactionalScheduler()\n>>> \n>>> def show(*args, **kw):\n... print (\"show:\", args, kw)\n... return \"ok\"\n... \n\nThe scheduling returns an id which can be used to learn about\nthe operation's fate via a ``get_result`` call.\nIf ``get_result`` returns ``None``, the schedule is unknown\n(probably lost); ``False`` means known but not yet complete.\nFinally, ``get_result`` may return a tuple *return-value*, *exception*.\n\nAfter a new schedule, the schedule is known but not yet complete.\n\n>>> sid = s.schedule(show, 1, 2, a=\"a\")\n>>> s.get_result(sid)\nFalse\n\nA transaction abort deletes the schedule.\n\n>>> abort()\n>>> s.get_result(sid)\n\nIf the transaction is commited, the scheduled operation is\ncalled.\n\n>>> sid = s.schedule(show, 1, 2, a=\"a\")\n>>> commit()\nshow: (1, 2) {'a': 'a'}\n\nAfter the completion, ``get_result`` returns its result.\nA transaction abort does not delete the result. However, a commit\nwill.\n\n>>> s.get_result(sid)\n('ok', None)\n>>> abort()\n>>> s.get_result(sid)\n('ok', None)\n>>> commit()\n>>> s.get_result(sid)\n\nWe now schedule ``exc``, a function which raises an exception.\n\ndef exc(): raise Exception()\n... \n>>> sid = s.schedule(exc)\n>>> commit()\n>>> ERROR:dm.zodb.asynchronous.scheduler:exception in call of \nTraceback (most recent call last):\n...\nException\n\nNote: The output above comes from the logging; the ``commit`` does\nnot output anything by itself.\n\nAgain, ``get_result`` provides information about the result.\n\n>>> s.get_result(sid)\n(None, )\n\n\n\n``context``\n-----------\n\nThis module defines the class ``PersistentContext``. It can be used\nto pass persistent objects from one (thread) context to another one.\nAs described in the ``scheduler`` section, persistent objects cannot\nsimply be passed on: instead the target context must reload them\nfrom a (new) connection associated with the target. ``PersistentContext``\nrecords the databases and oids associated with the persistent objects\nand facilitates the reloading inside the target.\n\nSee the module docstrings for details, especially\nabout the restrictions and risks.\n\nAn example is shown in the section \"Typical Usage Example\".\n\nNote: ``PersistentContext`` does not retain any acquisition context.\nThis means (among others) that the Zope2 security mechanism will fail\nand that the target thread will not have access to the request object\n(a good thing as it gets closed asynchronously). Thus, there are\nstill severe limitations of what you can do in an asynchronous operation.\n\n\n``zope2``\n---------\n\nThis module contains adaptations of facilities defined in the other\nmodules to a Zope2 environment. For example, there is\nan adapted ``TransactionManager`` (and derived ``transactional``\ndecorator) which provides transaction metadata in the way typical\nfor the Zope 2 framework. There are also ``PersistantContext``\nand ``PersistentTransactionalScheduler`` implementations\nwhich automatically determines the root database using Zope 2 implementation\ndetails.\n\n\nTypical Usage Example\n=====================\n\nAs mentioned in section ``scheduler``, the package can be used in\n(e.g.) a Zope 2 environment when some operation takes too much time\nto be performed inline (in the same request). In this case,\none can execute it in a separate thread and look for its results\nin a following (new) request. We present now a simple example.\n\nWe define a simple ``asynchronous_operation``, for demonstrational \npurposes. In real life, the scheduler would probably be global,\ne.g. provided by a so called \"utility\".\n\n>>> import transaction\n>>> from dm.zodb.asynchronous.zope2 import transactional, PersistentContext\n>>> from dm.zodb.asynchronous.scheduler import TransactionalScheduler\n>>> \n>>> \n>>> @transactional\n... def asynchronous_operation(context):\n... print (\"asynchronous_operation\")\n... return (context[\"param\"].x, context[0].x)\n... \n>>> scheduler = TransactionalScheduler()\n\nWe simulate now a request which schedules ``asynchronous_operation``\nand allows it to access the ``app`` object (the Zope2 root object)\nvia ``PersistentContext``. ``PersistentContext`` supports both positional\nas well as keyword parameters. For demonstational purposes, we\npass ``app`` both positional as well as via the keyword ``param``.\nInside ``asynchronous_operation``, subscription is used to access the\npersistent objects; an integer index accesses positional arguments, an ``str``\nindex the keyword arguments.\n\nThe scheduling returns an id which (in real life) would somehow be\nstored (e.g. in the user session or (better) be incorporated inside\nthe generated response and be used as parameter of a followup request).\nThe assignment to ``app.x`` is used to demonstrate that\n``asynchronous_operation`` sees modifications performed in the\noriginal request (even when they happen after the scheduling).\n\nAt the end of the initial request, there will be either\na ``transaction.abort()`` or a ``transaction.commit()``. In the former\ncase, the schedule will be removed and ``asynchronous_operation`` not\nstarted. In the latter case, ``asynchronous_operation`` will start.\n\n>>> sid = scheduler.schedule(asynchronous_operation,\n... PersistentContext(app, param=app)\n... )\n>>> print (sid)\n1a1e2d987b154945b7da12d6b09ed658\n>>> app.x = 1\n>>> \n>>> transaction.commit()\nasynchronous_operation\n\nWe look now at the followup request. Things must somehow have\nbeen set up that it can access the same ``scheduler`` (usually\ndone via an utility). Somehow, the followup request has learned\nof the schedule id (from the user session or via a request parameter).\nWith this information, it can check the fate of the asynchronous\noperation, process the result and commit.\n\n>>> r = scheduler.get_result(sid)\n>>> if r is None: print (\"lost schedule\")\n... elif not r: print (\"operation not yet complete\")\n... else:\n... (rv, exc) = r\n... if exc is not None:\n... # the asynchronous operation has raised *exc*.\n... # Do not reraise it! It belongs to a different context.\n... # If you raise a different exception, you might want\n... # to call ``scheduler.remove(sid)``; otherwise, the schedule\n... # gets removed only after timeout.\n... # Usually, you would not raise an exception but only provide information\n... # about the failure of the asynchronous operation\n... print (\"exceptioon: \", exc)\n... #return process_exception(exc)\n... else:\n... print (rv)\n... #return process_return_value(rv)\n... \n(1, 1)\n>>> transaction.commit()\n \nThe code snippet above has an extended comment about exception\nhandling from ``asynchronous_operation`` (in our trivial exemple, there\nwill be no exception). Note that a failing asynchronous operation\ndoes not mean that the current request has failed. The purpose of the\ncurrent request is to inform us about the fate of the asynchronous\noperation, not to perform this operation. Therefore, a failure\nof the asynchronous operation usually should result in the success\nof the current request (no exception) -- with appropriate information\nthat the asynchronous operation has failed.\nIn our exemple, we have decorated ``asynchronous_request`` with\n``transactional``. This way, it handles transaction management correctly\nin case of errors (the transaction gets aborted when the\nasynchronous operation should fail).\n\n\nHistory\n=======\n\n2.1\n\n ``transactional`` changes:\n\n * retries now for all ``transactional.interfaces.TransientError``\n (not just `ConflictError`)\n\n * a transactional function can now internally\n abort/commit the transaction.\n Note however, that this disables the detection of calls\n\tto nested transactional functions.\n\tUse the class method ``TransactionManager.begin``\n\tafter the ``abort/commit`` to reenable the detection.\n\n\n2.0\n Made Python3/ZODB4+/Zope4+ compatible.\n\n New `PersistentTransactionalScheduler`.\n\n1.x\n Targeting Python2/ZODB3/Zope2.10+", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://pypi.org/project/dm.zodb.asynchronous/", "keywords": "ZODB thread asynchronous utilities", "license": "BSD", "maintainer": "", "maintainer_email": "", "name": "dm.zodb.asynchronous", "package_url": "https://pypi.org/project/dm.zodb.asynchronous/", "platform": "", "project_url": "https://pypi.org/project/dm.zodb.asynchronous/", "project_urls": { "Homepage": "https://pypi.org/project/dm.zodb.asynchronous/" }, "release_url": "https://pypi.org/project/dm.zodb.asynchronous/2.1/", "requires_dist": null, "requires_python": "", "summary": "Utilities to implement asynchronous operations accessing the ZODB", "version": "2.1" }, "last_serial": 5670385, "releases": { "1.0": [ { "comment_text": "", "digests": { "md5": "46c59acd30c793ef25b58bf08a2526b1", "sha256": "735b59bf28864fc3a02a3440afc2ad10ce04d7560f790df60e68a290576f1659" }, "downloads": -1, "filename": "dm.zodb.asynchronous-1.0.tar.gz", "has_sig": false, "md5_digest": "46c59acd30c793ef25b58bf08a2526b1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19767, "upload_time": "2011-11-14T12:27:38", "url": "https://files.pythonhosted.org/packages/b9/37/0e1a08da6c6746a51cbb1018c5c6ad3ed80d155405255551f97899ba5910/dm.zodb.asynchronous-1.0.tar.gz" } ], "1.0.1": [ { "comment_text": "", "digests": { "md5": "e71c94adf65a6720503698799f632fbc", "sha256": "7da92f4fd44bb1a965662a6e3f62db2324e09d44260eeed3c2d8e4a3b4703991" }, "downloads": -1, "filename": "dm.zodb.asynchronous-1.0.1.tar.gz", "has_sig": false, "md5_digest": "e71c94adf65a6720503698799f632fbc", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19810, "upload_time": "2012-01-16T09:25:21", "url": "https://files.pythonhosted.org/packages/01/8f/573e4c4ae2870cbb0a33994324494903add6448671f1b0daae4e11530ea0/dm.zodb.asynchronous-1.0.1.tar.gz" } ], "1.0.2": [ { "comment_text": "", "digests": { "md5": "26394760a6d0d2a10b34d3f589228499", "sha256": "006e033d2b2e602b5a3ad9e89afe26997cd0ac5da7b417e60a373587c338b644" }, "downloads": -1, "filename": "dm.zodb.asynchronous-1.0.2.tar.gz", "has_sig": false, "md5_digest": "26394760a6d0d2a10b34d3f589228499", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19835, "upload_time": "2012-04-04T09:38:31", "url": "https://files.pythonhosted.org/packages/46/dc/2f8c0563cd6d87ce5349374816340c8889d5d3d1e353a45ce0e1cd66aa42/dm.zodb.asynchronous-1.0.2.tar.gz" } ], "2.0": [ { "comment_text": "", "digests": { "md5": "56e791aee366c197305dee517f74fc04", "sha256": "bcb833d3f5e221f7e52eeedb564244c26ef74750c8b00bf30a39ac839358b512" }, "downloads": -1, "filename": "dm.zodb.asynchronous-2.0.tar.gz", "has_sig": false, "md5_digest": "56e791aee366c197305dee517f74fc04", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22261, "upload_time": "2018-11-21T10:51:15", "url": "https://files.pythonhosted.org/packages/13/43/3cb9ec75081383a6cbdfd2e1a696e25a3492c185d5d93109a8af1653e198/dm.zodb.asynchronous-2.0.tar.gz" } ], "2.1": [ { "comment_text": "", "digests": { "md5": "f61773636bca2700e9ddbab19c9d35a5", "sha256": "30760276e9be33b7963793553dd1537b85d0b992112d0ba43d50972b9268782d" }, "downloads": -1, "filename": "dm.zodb.asynchronous-2.1.tar.gz", "has_sig": false, "md5_digest": "f61773636bca2700e9ddbab19c9d35a5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22698, "upload_time": "2019-08-13T06:47:39", "url": "https://files.pythonhosted.org/packages/53/57/900706d5406d76c74098fa7d30c28ff88ff555829481568b0c13445f2504/dm.zodb.asynchronous-2.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "f61773636bca2700e9ddbab19c9d35a5", "sha256": "30760276e9be33b7963793553dd1537b85d0b992112d0ba43d50972b9268782d" }, "downloads": -1, "filename": "dm.zodb.asynchronous-2.1.tar.gz", "has_sig": false, "md5_digest": "f61773636bca2700e9ddbab19c9d35a5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22698, "upload_time": "2019-08-13T06:47:39", "url": "https://files.pythonhosted.org/packages/53/57/900706d5406d76c74098fa7d30c28ff88ff555829481568b0c13445f2504/dm.zodb.asynchronous-2.1.tar.gz" } ] }