Transaction convenience support =============================== (We *really* need to write proper documentation for the transaction package, but I don't want to block the conveniences documented here for that.) with support ------------ We can now use the with statement to define transaction boundaries. >>> import transaction.tests.savepointsample >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager() >>> dm.keys() [] We can use the transaction module directly: >>> with transaction as t: ... dm['z'] = 1 ... t.note('test 1') >>> dm['z'] 1 >>> dm.last_note 'test 1' >>> with transaction: ... dm['z'] = 2 ... xxx Traceback (most recent call last): ... NameError: name 'xxx' is not defined >>> dm['z'] 1 We can use it with a manager: >>> with transaction.manager as t: ... dm['z'] = 3 ... t.note('test 3') >>> dm['z'] 3 >>> dm.last_note 'test 3' >>> with transaction: ... dm['z'] = 4 ... xxx Traceback (most recent call last): ... NameError: name 'xxx' is not defined >>> dm['z'] 3 Retries ------- Commits can fail for transient reasons, especially conflicts. Applications will often retry transactions some number of times to overcome transient failures. This typically looks something like:: for i in range(3): try: with transaction: ... some something ... except SomeTransientException: contine else: break This is rather ugly. Transaction managers provide a helper for this case. To show this, we'll use a contrived example: >>> ntry = 0 >>> with transaction: ... dm['ntry'] = 0 >>> import transaction.interfaces >>> class Retry(transaction.interfaces.TransientError): ... pass >>> for attempt in transaction.manager.attempts(): ... with attempt as t: ... t.note('test') ... print dm['ntry'], ntry ... ntry += 1 ... dm['ntry'] = ntry ... if ntry % 3: ... raise Retry(ntry) 0 0 0 1 0 2 The raising of a subclass of TransientError is critical here. It's what signals that the transaction should be retried. It is generally up to the data manager to signal that a transaction should try again by raising a subclass of TransientError (or TransientError itself, of course). You shouldn't make any assumptions about the object returned by the iterator. (It isn't a transaction or transaction manager, as far as you know. :) If you use the ``as`` keyword in the ``with`` statement, a transaction object will be assigned to the variable named. By default, it tries 3 times. We can tell it how many times to try: >>> for attempt in transaction.manager.attempts(2): ... with attempt: ... ntry += 1 ... if ntry % 3: ... raise Retry(ntry) Traceback (most recent call last): ... Retry: 5 It it doesn't succeed in that many times, the exception will be propagated. Of course, other errors are propagated directly: >>> ntry = 0 >>> for attempt in transaction.manager.attempts(): ... with attempt: ... ntry += 1 ... if ntry == 3: ... raise ValueError(ntry) Traceback (most recent call last): ... ValueError: 3 We can use the default transaction manager: >>> for attempt in transaction.attempts(): ... with attempt as t: ... t.note('test') ... print dm['ntry'], ntry ... ntry += 1 ... dm['ntry'] = ntry ... if ntry % 3: ... raise Retry(ntry) 3 3 3 4 3 5 Sometimes, a data manager doesn't raise exceptions directly, but wraps other other systems that raise exceptions outside of it's control. Data managers can provide a should_retry method that takes an exception instance and returns True if the transaction should be attempted again. >>> class DM(transaction.tests.savepointsample.SampleSavepointDataManager): ... def should_retry(self, e): ... if 'should retry' in str(e): ... return True >>> ntry = 0 >>> dm2 = DM() >>> with transaction: ... dm2['ntry'] = 0 >>> for attempt in transaction.manager.attempts(): ... with attempt: ... print dm['ntry'], ntry ... ntry += 1 ... dm['ntry'] = ntry ... dm2['ntry'] = ntry ... if ntry % 3: ... raise ValueError('we really should retry this') 6 0 6 1 6 2 >>> dm2['ntry'] 3