{ "info": { "author": "Zope Project", "author_email": "zope-dev@zope.org", "bugtrack_url": null, "classifiers": [], "description": "===================================================\nTwist: Talking to the ZODB in Twisted Reactor Calls\n===================================================\n\nThe twist package contains a few functions and classes, but primarily a\nhelper for having a deferred call on a callable persistent object, or on\na method on a persistent object. This lets you have a Twisted reactor\ncall or a Twisted deferred callback affect the ZODB. Everything can be\ndone within the main thread, so it can be full-bore Twisted usage,\nwithout threads. There are a few important \"gotchas\": see the Gotchas_\nsection below for details.\n\nThe main API is `Partial`. You can pass it a callable persistent object,\na method of a persistent object, or a normal non-persistent callable,\nand any arguments or keyword arguments of the same sort. DO NOT\nuse non-persistent data structures (such as lists) of persistent objects\nwith a database connection as arguments. This is your responsibility.\n\nIf nothing is persistent, the partial will not bother to get a connection,\nand will behave normally.\n\n >>> from zc.twist import Partial\n >>> def demo():\n ... return 42\n ...\n >>> Partial(demo)()\n 42\n\nNow let's imagine a demo object that is persistent and part of a\ndatabase connection. It has a `count` attribute that starts at 0, a\n`__call__` method that increments count by an `amount` that defaults to\n1, and an `decrement` method that reduces count by an `amount` that\ndefaults to 1 [#set_up]_. Everything returns the current value of count.\n\n >>> demo.count\n 0\n >>> demo()\n 1\n >>> demo(2)\n 3\n >>> demo.decrement()\n 2\n >>> demo.decrement(2)\n 0\n >>> import transaction\n >>> transaction.commit()\n\nNow we can make some deferred calls with these examples. We will use\n`transaction.begin()` to sync our connection with what happened in the\ndeferred call. Note that we need to have some adapters set up for this\nto work. The twist module includes implementations of them that we\nwill also assume have been installed [#adapters]_.\n\n >>> call = Partial(demo)\n >>> demo.count # hasn't been called yet\n 0\n >>> deferred = call()\n >>> demo.count # we haven't synced yet\n 0\n >>> t = transaction.begin() # sync the connection\n >>> demo.count # ah-ha!\n 1\n\nWe can use the deferred returned from the call to do something with the\nreturn value. In this case, the deferred is already completed, so\nadding a callback gets instant execution.\n\n >>> def show_value(res):\n ... print res\n ...\n >>> ignore = deferred.addCallback(show_value)\n 1\n\nWe can also pass the method.\n\n >>> call = Partial(demo.decrement)\n >>> deferred = call()\n >>> demo.count\n 1\n >>> t = transaction.begin()\n >>> demo.count\n 0\n\nThis also works for slot methods.\n\n >>> import BTrees\n >>> tree = root['tree'] = BTrees.family32.OO.BTree()\n >>> transaction.commit()\n >>> call = Partial(tree.__setitem__, 'foo', 'bar')\n >>> deferred = call()\n >>> len(tree)\n 0\n >>> t = transaction.begin()\n >>> tree['foo']\n 'bar'\n\nArguments are passed through.\n\n >>> call = Partial(demo)\n >>> deferred = call(2)\n >>> t = transaction.begin()\n >>> demo.count\n 2\n >>> call = Partial(demo.decrement)\n >>> deferred = call(amount=2)\n >>> t = transaction.begin()\n >>> demo.count\n 0\n\nThey can also be set during instantiation.\n\n >>> call = Partial(demo, 3)\n >>> deferred = call()\n >>> t = transaction.begin()\n >>> demo.count\n 3\n >>> call = Partial(demo.decrement, amount=3)\n >>> deferred = call()\n >>> t = transaction.begin()\n >>> demo.count\n 0\n\nArguments themselves can be persistent objects. Let's assume a new demo2\nobject as well.\n\n >>> demo2.count\n 0\n >>> def mass_increment(d1, d2, value=1):\n ... d1(value)\n ... d2(value)\n ...\n >>> call = Partial(mass_increment, demo, demo2, value=4)\n >>> deferred = call()\n >>> t = transaction.begin()\n >>> demo.count\n 4\n >>> demo2.count\n 4\n >>> demo.count = demo2.count = 0 # cleanup\n >>> transaction.commit()\n\nConflictErrors make it retry.\n\nIn order to have a chance to simulate a ConflictError, this time imagine\nwe have a runner that can switch execution from the call to our code\nusing `pause`, `retry` and `resume` (this is just for tests--remember,\ncalls used in non-threaded Twisted should be non-blocking!)\n[#conflict_error_setup]_.\n\n >>> import sys\n >>> demo.count\n 0\n >>> call = Partial(demo)\n >>> runner = Runner(call) # it starts paused in the middle of an attempt\n >>> call.attempt_count\n 1\n >>> demo.count = 5 # now we will make a conflicting transaction...\n >>> transaction.commit()\n >>> runner.retry()\n >>> call.attempt_count # so it has to retry\n 2\n >>> t = transaction.begin()\n >>> demo.count # our value hasn't changed...\n 5\n >>> runner.resume() # but now call will be successful on the second attempt\n >>> call.attempt_count\n 2\n >>> t = transaction.begin()\n >>> demo.count\n 6\n\nBy default, after five ConflictError retries, the partial fails, raising the\nlast ConflictError. This is returned to the deferred. The failure put\non the deferred will have a sanitized traceback. Here, imagine we have\na deferred (named `deferred`) created from such a an event\n[#conflict_error_failure]_.\n\n >>> res = None\n >>> def get_result(r):\n ... global res\n ... res = r # we return None to quiet Twisted down on the command line\n ...\n >>> d = deferred.addErrback(get_result)\n >>> print res.getTraceback() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE\n Traceback (most recent call last):\n ...\n ZODB.POSException.ConflictError: database conflict error...\n\nYou can control how many ConflictError (and other transaction error) retries\nshould be performed by setting the ``max_transaction_errors`` attribute\n[#max_transaction_errors]_.\n\nZEO ClientDisconnected errors are always retried, with a backoff that, by\ndefault begins at 5 seconds and is never greater than 60 seconds\n[#relies_on_twisted_reactor]_ [#use_original_demo]_ [#client_disconnected]_.\n\nOther errors are returned to the deferred, like a transaction error that has\nexceeded its available retries, as sanitized failures.\n\n >>> call = Partial(demo)\n >>> d = call('I do not add well with integers')\n >>> d = d.addErrback(get_result)\n >>> print res.getTraceback() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE\n Traceback (most recent call last):\n ...\n ...TypeError: unsupported operand type(s) for +=: 'int' and 'str'\n\nThe failure is sanitized in that the traceback is gone and the frame values\nare turned in to reprs. If you pickle the failure then it truncates the\nreprs to a maximum of 20 characters plus \"[...]\" to indicate the\ntruncation [#show_sanitation]_.\n\nThe call tries to be a good connection citizen, waiting for a connection\nif the pool is at its maximum size. This code relies on the twisted\nreactor; we'll use a `time_flies` function, which takes seconds to move\nahead, to simulate time passing in the reactor.\n\nWe use powers of 2 for the floating-points numbers (e.g. 0.125) to avoid\na floating-point additive accumulation error that happened in the tests\nwhen values such as 0.1 were used.\n\n >>> db.setPoolSize(1)\n >>> db.getPoolSize()\n 1\n >>> demo.count = 0\n >>> transaction.commit()\n >>> call = Partial(demo)\n >>> res = None\n >>> deferred = call()\n >>> d = deferred.addCallback(get_result)\n >>> call.attempt_count\n 0\n >>> time_flies(.125) >= 1 # returns number of connection attempts\n True\n >>> call.attempt_count\n 0\n >>> res # None\n >>> db.setPoolSize(2)\n >>> db.getPoolSize()\n 2\n >>> time_flies(.25) >= 1\n True\n >>> call.attempt_count > 0\n True\n >>> res\n 1\n >>> t = transaction.begin()\n >>> demo.count\n 1\n\nIf it takes more than a second or two, it will eventually just decide to grab\none. This behavior may change.\n\n >>> db.setPoolSize(1)\n >>> db.getPoolSize()\n 1\n >>> call = Partial(demo)\n >>> res = None\n >>> deferred = call()\n >>> d = deferred.addCallback(get_result)\n >>> call.attempt_count\n 0\n >>> time_flies(.125) >= 1\n True\n >>> call.attempt_count\n 0\n >>> res # None\n >>> time_flies(2) >= 2 # for a total of at least 3\n True\n >>> res\n 2\n >>> t = transaction.begin()\n >>> demo.count\n 2\n\nWithout a running reactor, this functionality will not work\n[#teardown_monkeypatch]_.\n\nYou can also specify a reactor for the partial using ``setReactor``, if\nyou don't want to use the standard one installed by twisted in\n``twisted.internet.reactor``. [#setReactor]_\n\nGotchas\n-------\n\nFor a certain class of jobs, you won't have to think much about using\nthe twist Partial. For instance, if you are putting a result gathered by\nwork done by deferreds into the ZODB, and that's it, everything should be\npretty simple. However, unfortunately, you have to think a bit harder for\nother common use cases.\n\n* As already mentioned, do not use arguments that are non-persistent\n collections (or even persistent objects without a connection) that hold\n any persistent objects with connections.\n\n* Using persistent objects with connections but that have not been\n committed to the database will cause problems when used (as callable\n or argument), perhaps intermittently (if a commit happens before the\n partial is called, it will work). Don't do this.\n\n* Do not return values that are persistent objects tied to a connection.\n\n* If you plan on firing off another reactor call on the basis of your\n work in the callable, realize that the work hasn't really \"happened\"\n until you commit the transaction. The partial typically handles commits\n for you, committing if you return any result and aborting if you raise\n an error. But if you want to send off a reactor call on the basis of a\n successful transaction, you'll want to (a) do the work, then (b)\n commit, then (c) send off the reactor call. If the commit fails,\n you'll get the standard abort and retry.\n\n* If you want to handle your own transactions, do not use the thread\n transaction manager that you get from importing transaction. This\n will cause intermittent, hard-to-debug, unexpected problems. Instead,\n adapt any persistent object you get to\n transaction.interfaces.ITransactionManager, and use that manager for\n commits and aborts.\n\n=========\nFootnotes\n=========\n\n.. [#set_up] We'll actually create the state that the text describes here.\n\n >>> import persistent\n >>> class Demo(persistent.Persistent):\n ... count = 0\n ... def __call__(self, amount=1):\n ... self._p_deactivate() # to be able to trigger ClientDisconnected\n ... self.count += amount\n ... return self.count\n ... def decrement(self, amount=1):\n ... self.count -= amount\n ... return self.count\n ...\n >>> from ZODB.tests.util import DB\n >>> db = DB()\n >>> conn = db.open()\n >>> root = conn.root()\n >>> demo = root['demo'] = Demo()\n >>> demo2 = root['demo2'] = Demo()\n >>> import transaction\n >>> transaction.commit()\n\n.. [#adapters] You must have two adapter registrations: IConnection to\n ITransactionManager, and IPersistent to IConnection. We will also\n register IPersistent to ITransactionManager because the adapter is\n designed for it.\n\n >>> from zc.twist import transactionManager, connection\n >>> import zope.component\n >>> zope.component.provideAdapter(transactionManager)\n >>> zope.component.provideAdapter(connection)\n >>> import ZODB.interfaces\n >>> zope.component.provideAdapter(\n ... transactionManager, adapts=(ZODB.interfaces.IConnection,))\n\n This quickly tests the adapters:\n\n >>> ZODB.interfaces.IConnection(demo) is conn\n True\n >>> import transaction.interfaces\n >>> transaction.interfaces.ITransactionManager(demo) is transaction.manager\n True\n >>> transaction.interfaces.ITransactionManager(conn) is transaction.manager\n True\n\n.. [#conflict_error_setup] We also use this runner in the footnote below.\n\n >>> import threading\n >>> _main = threading.Lock()\n >>> _thread = threading.Lock()\n >>> class AltDemo(persistent.Persistent):\n ... count = 0\n ... def __call__(self, amount=1):\n ... self.count += amount\n ... assert _main.locked()\n ... _main.release()\n ... _thread.acquire()\n ... return self.count\n ...\n >>> demo = root['altdemo'] = AltDemo()\n >>> transaction.commit()\n >>> class Runner(object):\n ... def __init__(self, call):\n ... self.call = call\n ... self.thread = threading.Thread(target=self.run)\n ... self.thread.setDaemon(True)\n ... _thread.acquire()\n ... _main.acquire()\n ... self.thread.start()\n ... _main.acquire()\n ... def run(self):\n ... self.running = True\n ... self.result = self.call()\n ... assert _main.locked()\n ... assert _thread.locked()\n ... _thread.release()\n ... self.running = False\n ... _main.release()\n ... def retry(self):\n ... assert _thread.locked()\n ... _thread.release()\n ... _main.acquire()\n ... def resume(self):\n ... while self.running:\n ... self.retry()\n ... self.thread.join()\n ... assert not _thread.locked()\n ... assert _main.locked()\n ... _main.release()\n\n.. [#conflict_error_failure] Here we create five consecutive conflict errors,\n which causes the call to give up.\n\n >>> call = Partial(demo)\n >>> runner = Runner(call)\n >>> for i in range(5):\n ... demo.count = i\n ... transaction.commit() # creates a write conflict\n ... runner.retry()\n ...\n >>> demo.count\n 4\n\n When we resume without a conflict error, it is too late: the result is a\n ConflictError. The ConflictError is actually shown in the main text.\n\n >>> runner.resume()\n >>> demo.count\n 4\n >>> call.attempt_count\n 5\n >>> deferred = runner.result\n\n.. [#max_transaction_errors] As the main text mentions, the\n ``max_transaction_errors`` attribute lets you set how many conflict errors\n should be retried.\n\n >>> call = Partial(demo)\n >>> call.max_transaction_errors\n 5\n >>> call.max_transaction_errors = 10\n >>> call.max_transaction_errors\n 10\n >>> runner = Runner(call)\n >>> for i in range(10):\n ... demo.count = i\n ... transaction.commit()\n ... runner.retry()\n ...\n\n When we resume without a conflict error, it is too late: the result is a\n ConflictError.\n\n >>> runner.resume()\n >>> demo.count\n 9\n >>> call.attempt_count\n 10\n >>> deferred = runner.result\n\n >>> res = None\n >>> def get_result(r):\n ... global res\n ... res = r # we return None to quiet Twisted down on the command line\n ...\n >>> d = deferred.addErrback(get_result)\n >>> print res.getTraceback() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE\n Traceback (most recent call last):\n ...\n ZODB.POSException.ConflictError: database conflict error...\n\n Setting ``None`` means to retry conflict errors forever. For our example,\n we will arbitrarily choose 50 iterations to show \"forever\".\n\n >>> call = Partial(demo)\n >>> call.max_transaction_errors\n 5\n >>> call.max_transaction_errors = None\n >>> print call.max_transaction_errors\n None\n >>> runner = Runner(call)\n >>> for i in range(50):\n ... demo.count = i\n ... transaction.commit()\n ... runner.retry()\n ...\n\n Now when we resume without a conflict error, we get a successful result: it\n never gave up.\n\n >>> runner.resume()\n >>> runner.result # doctest: +ELLIPSIS\n \n >>> _ = transaction.begin() # we need to sync to get changes\n >>> demo.count # notice, 49 + 1\n 50\n\n.. [#relies_on_twisted_reactor] We monkeypatch twisted.internet.reactor\n (and revert it in another footnote below).\n\n >>> import twisted.internet.reactor\n >>> oldCallLater = twisted.internet.reactor.callLater\n >>> import bisect\n >>> class FauxReactor(object):\n ... def __init__(self):\n ... self.time = 0\n ... self.calls = []\n ... def callLater(self, delay, callable, *args, **kw):\n ... res = (delay + self.time, callable, args, kw)\n ... bisect.insort(self.calls, res)\n ... # normally we're supposed to return something but not needed\n ... def time_flies(self, time):\n ... end = self.time + time\n ... ct = 0\n ... while self.calls and self.calls[0][0] <= end:\n ... self.time, callable, args, kw = self.calls.pop(0)\n ... callable(*args, **kw) # normally this would get try...except\n ... ct += 1\n ... self.time = end\n ... return ct\n ...\n >>> faux = FauxReactor()\n >>> twisted.internet.reactor.callLater = faux.callLater\n >>> time_flies = faux.time_flies\n\n.. [#use_original_demo] The second demo has too much thread code in it:\n we'll use the old demo for the rest of the discussion.\n\n >>> demo = root['demo']\n\n.. [#client_disconnected] As the main text describes,\n ZEO.Exceptions.ClientDisconnected errors will always be retried, but with a\n backoff.\n\n First we'll mimic a disconnected ZEO at the start of a transaction.\n\n >>> from ZEO.Exceptions import ClientDisconnected\n >>> raise_error = [1]\n >>> storage_class = db._storage.__class__\n >>> original_load = storage_class.load\n >>> def load(self, oid, version):\n ... if raise_error:\n ... raise_error.pop()\n ... raise ClientDisconnected()\n ... else:\n ... return original_load(self, oid, version)\n ...\n >>> _ = transaction.begin()\n >>> demo.count\n 0\n >>> call = Partial(demo)\n >>> storage_class.load = load\n\n We rely on a reactor to implement delayed calls. We have a fake reactor\n called ``faux`` for these examples. It has a list of pending calls, and\n we can call ``time_flies`` to make time appear to pass.\n\n >>> len(faux.calls)\n 0\n\n When we first call the partial, it will fail, and reschedule for later.\n\n >>> len(faux.calls)\n 0\n >>> deferred = call()\n >>> deferred.called\n 0\n >>> len(faux.calls)\n 1\n\n The rescheduling is initially for five seconds later, by default. In this\n first example, after the first retry, the call will succeed.\n\n >>> faux.calls[0][0] - faux.time\n 5\n >>> time_flies(1) # 1 second\n 0\n >>> deferred.called\n 0\n >>> time_flies(1) # 1 second\n 0\n >>> deferred.called\n 0\n >>> time_flies(1) # 1 second\n 0\n >>> deferred.called\n 0\n >>> time_flies(1) # 1 second\n 0\n >>> deferred.called\n 0\n >>> time_flies(1) # 1 second\n 1\n >>> deferred.called\n True\n >>> deferred.result\n 1\n >>> len(faux.calls)\n 0\n\n By default, the rescheduling backoff increases by five seconds for every\n retry, to a maximum of a 60 second backoff.\n\n >>> call = Partial(demo)\n >>> raise_error.extend([1] * 30)\n >>> len(faux.calls)\n 0\n >>> deferred = call()\n >>> deferred.called\n 0\n >>> len(faux.calls)\n 1\n >>> def run(deferred):\n ... sleeps = []\n ... for i in range(31):\n ... if deferred.called:\n ... break\n ... else:\n ... sleep = faux.calls[0][0] - faux.time\n ... sleeps.append(sleep)\n ... time_flies(sleep)\n ... else:\n ... print 'oops'\n ... return sleeps\n ...\n >>> sleeps = run(deferred)\n >>> deferred.result\n 2\n >>> len(sleeps)\n 30\n >>> sleeps # doctest: +ELLIPSIS\n [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 60, 60,..., 60, 60, 60, 60]\n\n The default backoff values can be changed by setting the instance attributes\n ``initial_backoff``, ``backoff_increment``, and ``max_backoff``.\n\n >>> call = Partial(demo)\n >>> call.initial_backoff\n 5\n >>> call.backoff_increment\n 5\n >>> call.max_backoff\n 60\n >>> call.initial_backoff = 4\n >>> call.backoff_increment = 2\n >>> call.max_backoff = 21\n >>> raise_error.extend([1] * 30)\n >>> len(faux.calls)\n 0\n >>> deferred = call()\n >>> deferred.called\n 0\n >>> len(faux.calls)\n 1\n >>> sleeps = run(deferred)\n >>> deferred.result\n 3\n >>> len(sleeps)\n 30\n >>> sleeps # doctest: +ELLIPSIS\n [4, 6, 8, 10, 12, 14, 16, 18, 20, 21, 21, 21, 21, 21, 21,..., 21, 21, 21]\n\n A ClientDisconnected error can also occur during transaction commit.\n\n >>> storage_class.load = original_load\n\n >>> from transaction import TransactionManager\n >>> old_commit = TransactionManager.commit\n >>> commit_count = 0\n >>> error = None\n >>> max = 2\n >>> raise_error = [1] # change state to active\n >>> def new_commit(self):\n ... if raise_error:\n ... raise_error.pop()\n ... raise ClientDisconnected()\n ... else:\n ... old_commit(self) # changing state to \"active\" or similar\n ...\n >>> TransactionManager.commit = new_commit\n >>> call = Partial(demo)\n >>> len(faux.calls)\n 0\n >>> deferred = call()\n >>> deferred.called\n 0\n >>> len(faux.calls)\n 1\n\n >>> faux.calls[0][0] - faux.time\n 5\n >>> time_flies(4) # 4 seconds\n 0\n >>> deferred.called\n 0\n >>> time_flies(1) # 1 second\n 1\n >>> deferred.called\n True\n >>> deferred.result\n 4\n >>> len(faux.calls)\n 0\n\n >>> TransactionManager.commit = old_commit\n >>> _ = transaction.begin()\n\n.. [#show_sanitation] Before pickling, the failure includes full information\n about before and after the exception was caught, as well as locals and\n globals. Everything has been repr'd, though, and the traceback object\n removed.\n\n >>> print res.getTraceback() # doctest: +ELLIPSIS\n Traceback (most recent call last):\n File \".../zc/twist/__init__.py\", line ..., in __call__...\n File \".../twisted/internet/defer.py\", line ..., in addCallback...\n --- ---\n File \".../zc/twist/__init__.py\", line ..., in _call...\n File \"\", line ..., in __call__...\n exceptions.TypeError: unsupported operand type(s) for +=: 'int' and 'str'\n \n\n (The failure traceback at \"verbose\" detail is wildly verbose--this example\n takes out more than 90% of the text, just so you know.)\n\n >>> print res.getTraceback(detail='verbose') # doctest: +ELLIPSIS\n *--- Failure #... (pickled) ---\n .../zc/twist/__init__.py:...: __call__(...)\n [ Locals ]...\n args : \"('I do not add well with integers',)...\n ( Globals )...\n Partial : \"...\n .../twisted/internet/defer.py:...: addCallback(...)\n [ Locals ]...\n args : '( ---\n .../zc/twist/__init__.py:...: _call(...)\n [ Locals ]...\n args : \"['I do not add well with integers']...\n ( Globals )...\n Partial : \"...\n :...: __call__(...)\n [ Locals ]...\n amount : \"'I do not add well with integers'...\n ( Globals )...\n Partial : \"...\n exceptions.TypeError: unsupported operand type(s) for +=: 'int' and 'str'\n *--- End of Failure #... ---\n \n\n After pickling, the failure only includes information for when the\n exception was caught and beyond (after the \"--- ---\" lines above), does not have globals, and has local reprs\n truncated to a maximum of 20 characters plus \"[...]\" to indicate the\n truncation. This addresses past problems of large pickle size for\n failures, which can cause performance problems.\n\n >>> import pickle\n >>> print pickle.loads(pickle.dumps(res)).getTraceback()\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n File \".../zc/twist/__init__.py\", line ..., in _call\n res = call(*args, **kwargs)\n File \"\", line ..., in __call__\n self.count += amount\n exceptions.TypeError: unsupported operand type(s) for +=: 'int' and 'str'\n \n\n >>> print pickle.loads(pickle.dumps(res)).getTraceback(detail='verbose')\n ... # doctest: +ELLIPSIS\n *--- Failure #... (pickled) ---\n .../src/zc/twist/__init__.py:...: _call(...)\n [ Locals...\n self : ':...: __call__(...)\n [ Locals...\n amount : \"'I do not add well wi[...]...\n ( Globals )\n exceptions.TypeError: unsupported operand type(s) for +=: 'int' and 'str'\n *--- End of Failure #... ---\n \n\n In some cases, it is possible that a Failure object may include references\n to itself, for example, indirectly through a zc.async job whose result\n is a Failure. Failure's __getstate__ method used to use deepcopy, which\n in cases like this could result in infinite recursion.\n\n >>> import zc.twist\n >>> class Kaboom(Exception):\n ... pass\n >>> class Foo(object):\n ... failure = None\n ... def fail(self):\n ... raise Kaboom, self\n >>> foo = Foo()\n >>> try:\n ... foo.fail()\n ... except Kaboom:\n ... foo.failure = zc.twist.Failure()\n >>> ignored = foo.failure.__getstate__() # used to cause RunTimeError.\n\n.. [#teardown_monkeypatch]\n\n >>> twisted.internet.reactor.callLater = oldCallLater\n\n.. [#setReactor]\n\n >>> db.setPoolSize(1)\n >>> db.getPoolSize()\n 1\n >>> demo.count = 0\n >>> transaction.commit()\n >>> call = Partial(demo).setReactor(faux)\n >>> res = None\n >>> deferred = call()\n >>> d = deferred.addCallback(get_result)\n >>> call.attempt_count\n 0\n >>> time_flies(.125) >= 1 # returns number of connection attempts\n True\n >>> call.attempt_count\n 0\n >>> res # None\n >>> db.setPoolSize(2)\n >>> db.getPoolSize()\n 2\n >>> time_flies(.25) >= 1\n True\n >>> call.attempt_count > 0\n True\n >>> res\n 1\n >>> t = transaction.begin()\n >>> demo.count\n 1\n\nIf it takes more than a second or two, it will eventually just decide to grab\none. This behavior may change.\n\n >>> db.setPoolSize(1)\n >>> db.getPoolSize()\n 1\n >>> call = Partial(demo).setReactor(faux)\n >>> res = None\n >>> deferred = call()\n >>> d = deferred.addCallback(get_result)\n >>> call.attempt_count\n 0\n >>> time_flies(.125) >= 1\n True\n >>> call.attempt_count\n 0\n >>> res # None\n >>> time_flies(2) >= 2 # for a total of at least 3\n True\n >>> res\n 2\n >>> t = transaction.begin()\n >>> demo.count\n 2\n\n\n=======\nChanges\n=======\n\n1.3.1 (2009-11-15)\n------------------\n\n* Added missing import of twisted.python.failure.\n\n* Use db.pool with ZODB >= 3.9 and db._pools with ZODB < 3.9.\n The tests now pass with ZODB 3.9.\n\n* Tests pass in Python 2.6.\n\n1.3 (2008-06-19)\n----------------\n\n* Handle ZEO.Exceptions.ClientDisconnected errors: retry forever, with a\n backoff, defaulting to a max backoff of 60 seconds.\n\n* Make number of times that ConflictErrors are retried configurable.\n\n1.2 (2008-04-09)\n----------------\n\n* New subclass of twisted.python.failure.Failure begins with only reprs,\n and it pickles to exclude the stack, exclude the global vars in the frames,\n and truncate the reprs of the local vars in the frames. The goal is to\n keep the pickle size of Failures down to a manageable size. ``sanitize``\n now uses this class.\n\n1.1 (2008-03-27)\n----------------\n\n* Now depends on twisted 8.0.1 or higher, which is newly setuptools\n compatible. The twisted build is a little frightening, at least with\n Py2.4, with multiple warnings and errors, but works. The dependency\n change is the reason for the bump to 1.1; this release has no new\n features.\n\n* setup.py uses os.path\n\n* C extension uses older comment style and has less cruft.\n\n1.0.1 (2008-03-14)\n------------------\n\n* Bugfix: if you passed a slot method like a BTree.__setitem__, bad things\n would happen.\n\n1.0.0 (2008-03-13)\n------------------\n\n* Add ability to specify an alternate reactor\n\n* Use bootstrap external\n\n0.1.1 (?)\n---------\n\n* use zc.twisted, not twisted in setup.py, until twisted is setuptools-friendly\n\n0.1 (?)\n-------\n\n* Initial release-ish", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://pypi.python.org/pypi/zc.twist", "keywords": null, "license": "ZPL", "maintainer": null, "maintainer_email": null, "name": "zc.twist", "package_url": "https://pypi.org/project/zc.twist/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/zc.twist/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://pypi.python.org/pypi/zc.twist" }, "release_url": "https://pypi.org/project/zc.twist/1.3.1/", "requires_dist": null, "requires_python": null, "summary": "Mixing Twisted and ZODB", "version": "1.3.1" }, "last_serial": 802213, "releases": { "1.0": [ { "comment_text": "", "digests": { "md5": "d539532a694ab60c5b655fc86e9058b1", "sha256": "df90b38e356f80758a9a1527d4c7124a46bc03d714e66e72358719ea5c6e0ba2" }, "downloads": -1, "filename": "zc.twist-1.0.tar.gz", "has_sig": false, "md5_digest": "d539532a694ab60c5b655fc86e9058b1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 15392, "upload_time": "2008-03-13T16:07:19", "url": "https://files.pythonhosted.org/packages/c5/da/400eb978362350ea963e7ef47b330ac9207a7fa7ac365fdf8c369efe5d12/zc.twist-1.0.tar.gz" } ], "1.0.1": [ { "comment_text": "", "digests": { "md5": "5402922c70f52b3fbcd6e87c5a1110de", "sha256": "5770ca0dde04f29b398d6eaf69bf82ad513e11ddcf5b29d7378e8d46cfdaeb31" }, "downloads": -1, "filename": "zc.twist-1.0.1.tar.gz", "has_sig": false, "md5_digest": "5402922c70f52b3fbcd6e87c5a1110de", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 16554, "upload_time": "2008-03-14T22:31:51", "url": "https://files.pythonhosted.org/packages/c2/8d/58f9a1433c82a4522cf827f505a2dcf63bbecdcc9967ebd29e08a12093dc/zc.twist-1.0.1.tar.gz" } ], "1.1": [ { "comment_text": "", "digests": { "md5": "7bfb27435f9dd25a7bb90ca79be166c5", "sha256": "0f2c4e4c3c5280de731fdd4abcc2867b1c3ab3ec4faf53dda0d8d24a5fc50618" }, "downloads": -1, "filename": "zc.twist-1.1.tar.gz", "has_sig": false, "md5_digest": "7bfb27435f9dd25a7bb90ca79be166c5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 17116, "upload_time": "2008-03-27T16:46:07", "url": "https://files.pythonhosted.org/packages/4b/85/2db976f15d93726e9454871d3129c176c237cf7cea92185a2935a78497f0/zc.twist-1.1.tar.gz" } ], "1.2": [ { "comment_text": "", "digests": { "md5": "5f43341462ae2b4cf7364c756643bc63", "sha256": "a57ab65fdd24c9ed26475351267a7ee3c0042228d9833c4b4759bde971d1beb0" }, "downloads": -1, "filename": "zc.twist-1.2.tar.gz", "has_sig": false, "md5_digest": "5f43341462ae2b4cf7364c756643bc63", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19985, "upload_time": "2008-04-09T15:52:31", "url": "https://files.pythonhosted.org/packages/1c/92/f1bdd9f40a7f175a2242687140a6b3f182dc7a21199dd0de8d667f023f2a/zc.twist-1.2.tar.gz" } ], "1.3": [ { "comment_text": "", "digests": { "md5": "fcb8eaa7aa623293eaef232d46e1a5dd", "sha256": "448603fbb3bd91d1d5597299ee1f358dab294d9cbdb37fb5b4863bf7eb0af6c8" }, "downloads": -1, "filename": "zc.twist-1.3.tar.gz", "has_sig": false, "md5_digest": "fcb8eaa7aa623293eaef232d46e1a5dd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27363, "upload_time": "2008-06-19T21:29:15", "url": "https://files.pythonhosted.org/packages/15/d9/55192d3016c89a80e38d9d225abcbf42de5634dc867e653075b07440f72c/zc.twist-1.3.tar.gz" } ], "1.3.1": [ { "comment_text": "", "digests": { "md5": "60bc9506455d9675a280880b74cd4427", "sha256": "99cbf5b4a7c57b8c475a89c6619f21cebbfe561ec9f5b043200337f6c3a3327f" }, "downloads": -1, "filename": "zc.twist-1.3.1.tar.gz", "has_sig": false, "md5_digest": "60bc9506455d9675a280880b74cd4427", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29479, "upload_time": "2009-11-16T02:48:48", "url": "https://files.pythonhosted.org/packages/37/24/5c7b7bb96b57b732e7cd1c48d8a66f7ade89d035a449fe81711f9812fa47/zc.twist-1.3.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "60bc9506455d9675a280880b74cd4427", "sha256": "99cbf5b4a7c57b8c475a89c6619f21cebbfe561ec9f5b043200337f6c3a3327f" }, "downloads": -1, "filename": "zc.twist-1.3.1.tar.gz", "has_sig": false, "md5_digest": "60bc9506455d9675a280880b74cd4427", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 29479, "upload_time": "2009-11-16T02:48:48", "url": "https://files.pythonhosted.org/packages/37/24/5c7b7bb96b57b732e7cd1c48d8a66f7ade89d035a449fe81711f9812fa47/zc.twist-1.3.1.tar.gz" } ] }