Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Z
zodb
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Joshua
zodb
Commits
195ab817
Commit
195ab817
authored
Jan 05, 2006
by
Julien Anguenot
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
merge
http://svn.zope.org/ZODB/branches/anguenot-after_commit_hooks/
branch
parent
36c47721
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
384 additions
and
12 deletions
+384
-12
NEWS.txt
NEWS.txt
+12
-0
src/transaction/_transaction.py
src/transaction/_transaction.py
+71
-9
src/transaction/interfaces.py
src/transaction/interfaces.py
+37
-0
src/transaction/tests/test_transaction.py
src/transaction/tests/test_transaction.py
+264
-3
No files found.
NEWS.txt
View file @
195ab817
...
...
@@ -46,3 +46,15 @@ Connection
- (3.7a1) An optimization for loading non-current data (MVCC) was
inadvertently disabled in ``_setstate()``; this has been repaired.
After Commit hooks
------------------
- (3.7a1) Transaction objects have a new method,
``addAfterCommitHook(hook, *args, **kws)``. Hook functions
registered with a transaction are called after the transaction
commits or aborts. For example, one might want to launch non
transactional or asynchrnonous code after a successful, or aborted,
commit. See ``test_afterCommitHook()`` in
``transaction/tests/test_transaction.py`` for a tutorial doctest,
and the ``ITransaction`` interface for details.
src/transaction/_transaction.py
View file @
195ab817
...
...
@@ -115,6 +115,20 @@ pre-commit hook is available for such use cases: use addBeforeCommitHook(),
passing it a callable and arguments. The callable will be called with its
arguments at the start of the commit (but not for substransaction commits).
After-commit hook
------------------
Sometimes, applications want to execute code after a transaction is
committed or aborted. For example, one might want to launch non
transactional code after a successful commit. Or still someone might
want to launch asynchronous code after. A post-commit hook is
available for such use cases: use addAfterCommitHook(), passing it a
callable and arguments. The callable will be called with a Boolean
value representing the status of the commit operation as first
argument (true if successfull or false iff aborted) preceding its
arguments at the start of the commit (but not for substransaction
commits).
Error handling
--------------
...
...
@@ -241,6 +255,9 @@ class Transaction(object):
# List of (hook, args, kws) tuples added by addBeforeCommitHook().
self
.
_before_commit
=
[]
# List of (hook, args, kws) tuples added by addAfterCommitHook().
self
.
_after_commit
=
[]
# Raise TransactionFailedError, due to commit()/join()/register()
# getting called when the current transaction has already suffered
# a commit/savepoint failure.
...
...
@@ -292,7 +309,7 @@ class Transaction(object):
savepoint
=
Savepoint
(
self
,
optimistic
,
*
self
.
_resources
)
except
:
self
.
_cleanup
(
self
.
_resources
)
self
.
_saveCommitishError
()
# reraises!
self
.
_save
AndRaise
CommitishError
()
# reraises!
if
self
.
_savepoint2index
is
None
:
self
.
_savepoint2index
=
weakref
.
WeakKeyDictionary
()
...
...
@@ -376,16 +393,19 @@ class Transaction(object):
try
:
self
.
_commitResources
()
self
.
status
=
Status
.
COMMITTED
except
:
self
.
_saveCommitishError
()
# This raises!
self
.
status
=
Status
.
COMMITTED
if
self
.
_manager
:
self
.
_manager
.
free
(
self
)
self
.
_synchronizers
.
map
(
lambda
s
:
s
.
afterCompletion
(
self
))
t
,
v
,
tb
=
self
.
_saveAndGetCommitishError
()
self
.
_callAfterCommitHooks
(
status
=
False
)
raise
t
,
v
,
tb
else
:
if
self
.
_manager
:
self
.
_manager
.
free
(
self
)
self
.
_synchronizers
.
map
(
lambda
s
:
s
.
afterCompletion
(
self
))
self
.
_callAfterCommitHooks
(
status
=
True
)
self
.
log
.
debug
(
"commit"
)
def
_saveCommitishError
(
self
):
def
_save
AndGet
CommitishError
(
self
):
self
.
status
=
Status
.
COMMITFAILED
# Save the traceback for TransactionFailedError.
ft
=
self
.
_failure_traceback
=
StringIO
()
...
...
@@ -396,6 +416,10 @@ class Transaction(object):
traceback
.
print_tb
(
tb
,
None
,
ft
)
# Append the exception type and value.
ft
.
writelines
(
traceback
.
format_exception_only
(
t
,
v
))
return
t
,
v
,
tb
def
_saveAndRaiseCommitishError
(
self
):
t
,
v
,
tb
=
self
.
_saveAndGetCommitishError
()
raise
t
,
v
,
tb
def
getBeforeCommitHooks
(
self
):
...
...
@@ -421,6 +445,44 @@ class Transaction(object):
hook
(
*
args
,
**
kws
)
self
.
_before_commit
=
[]
def
getAfterCommitHooks
(
self
):
return
iter
(
self
.
_after_commit
)
def
addAfterCommitHook
(
self
,
hook
,
args
=
(),
kws
=
None
):
if
kws
is
None
:
kws
=
{}
self
.
_after_commit
.
append
((
hook
,
tuple
(
args
),
kws
))
def
_callAfterCommitHooks
(
self
,
status
=
True
):
# Avoid to abort anything at the end if no hooks are registred.
if
not
self
.
_after_commit
:
return
# Call all hooks registered, allowing further registrations
# during processing. Note that calls to addAterCommitHook() may
# add additional hooks while hooks are running, and iterating over a
# growing list is well-defined in Python.
for
hook
,
args
,
kws
in
self
.
_after_commit
:
# The first argument passed to the hook is a Boolean value,
# true if the commit succeeded, or false if the commit aborted.
try
:
hook
(
status
,
*
args
,
**
kws
)
except
:
# We need to catch the exceptions if we want all hooks
# to be called
self
.
log
.
error
(
"Error in after commit hook exec in %s "
,
hook
,
exc_info
=
sys
.
exc_info
())
# The transaction is already committed. It must not have
# further effects after the commit.
for
rm
in
self
.
_resources
:
try
:
rm
.
abort
(
self
)
except
:
# XXX should we take further actions here ?
self
.
log
.
error
(
"Error in abort() on manager %s"
,
rm
,
exc_info
=
sys
.
exc_info
())
self
.
_after_commit
=
[]
self
.
_before_commit
=
[]
def
_commitResources
(
self
):
# Execute the two-phase commit protocol.
...
...
@@ -687,7 +749,7 @@ class Savepoint:
savepoint
.
rollback
()
except
:
# Mark the transaction as failed.
transaction
.
_saveCommitishError
()
# reraises!
transaction
.
_save
AndRaise
CommitishError
()
# reraises!
class
AbortSavepoint
:
...
...
src/transaction/interfaces.py
View file @
195ab817
...
...
@@ -232,6 +232,43 @@ class ITransaction(zope.interface.Interface):
by a top-level transaction commit.
"""
def
addAfterCommitHook
(
hook
,
args
=
(),
kws
=
None
):
"""Register a hook to call after a transaction commit attempt.
The specified hook function will be called after the transaction
commit succeeds or aborts. The first argument passed to the hook
is a Boolean value, true if the commit succeeded, or false if the
commit aborted. `args` specifies additional positional, and `kws`
keyword, arguments to pass to the hook. `args` is a sequence of
positional arguments to be passed, defaulting to an empty tuple
(only the true/false success argument is passed). `kws` is a
dictionary of keyword argument names and values to be passed, or
the default None (no keyword arguments are passed).
Multiple hooks can be registered and will be called in the order they
were registered (first registered, first called). This method can
also be called from a hook: an executing hook can register more
hooks. Applications should take care to avoid creating infinite loops
by recursively registering hooks.
Hooks are called only for a top-level commit. A subtransaction
commit or savepoint creation does not call any hooks. Calling a
hook "consumes" its registration: hook registrations do not
persist across transactions. If it's desired to call the same
hook on every transaction commit, then addAfterCommitHook() must be
called with that hook during every transaction; in such a case
consider registering a synchronizer object via a TransactionManager's
registerSynch() method instead.
"""
def
getAfterCommitHooks
():
"""Return iterable producing the registered addAfterCommit hooks.
A triple (hook, args, kws) is produced for each registered hook.
The hooks are produced in the order in which they would be invoked
by a top-level transaction commit.
"""
class
ITransactionDeprecated
(
zope
.
interface
.
Interface
):
"""Deprecated parts of the transaction API."""
...
...
src/transaction/tests/test_transaction.py
View file @
195ab817
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# Copyright (c) 2001, 2002
, 2005
Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
...
...
@@ -11,7 +11,7 @@
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""Test tran
as
ction behavior for variety of cases.
"""Test tran
sa
ction behavior for variety of cases.
I wrote these unittests to investigate some odd transaction
behavior when doing unittests of integrating non sub transaction
...
...
@@ -241,7 +241,6 @@ class TransactionTests(unittest.TestCase):
assert
self
.
nosub1
.
_p_jar
.
ctpc_abort
==
1
# last test, check the hosing mechanism
## def testHoserStoppage(self):
...
...
@@ -728,6 +727,268 @@ def test_addBeforeCommitHook():
"arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec0']
>>> reset_log()
When modifing persitent objects within before commit hooks
modifies the objects, of course :)
Start a new transaction
>>> t = transaction.begin()
Create a DB instance and add a IOBTree within
>>> from ZODB.tests.util import DB
>>> from ZODB.tests.util import P
>>> db = DB()
>>> con = db.open()
>>> root = con.root()
>>> root['p'] = P('julien')
>>> p = root['p']
>>> p.name
'julien'
This hook will get the object from the `DB` instance and change
the flag attribute.
>>> def hookmodify(status, arg=None, kw1='no_kw1', kw2='no_kw2'):
... p.name = 'jul'
Now register this hook and commit.
>>> t.addBeforeCommitHook(hookmodify, (p, 1))
>>> transaction.commit()
Nothing should have changed since it should have been aborted.
>>> p.name
'jul'
>>> db.close()
"""
def
test_addAfterCommitHook
():
"""Test addAfterCommitHook.
Let's define a hook to call, and a way to see that it was called.
>>> log = []
>>> def reset_log():
... del log[:]
>>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
... log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2))
Now register the hook with a transaction.
>>> import transaction
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, '1')
We can see that the hook is indeed registered.
>>> [(hook.func_name, args, kws)
... for hook, args, kws in t.getAfterCommitHooks()]
[('hook', ('1',), {})]
When transaction commit is done, the hook is called, with its
arguments.
>>> log
[]
>>> t.commit()
>>> log
["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
A hook's registration is consumed whenever the hook is called. Since
the hook above was called, it's no longer registered:
>>> len(list(t.getAfterCommitHooks()))
0
>>> transaction.commit()
>>> log
[]
The hook is only called after a full commit, not for a savepoint or
subtransaction.
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, 'A', dict(kw1='B'))
>>> dummy = t.savepoint()
>>> log
[]
>>> t.commit(subtransaction=True)
>>> log
[]
>>> t.commit()
>>> log
["True arg 'A' kw1 'B' kw2 'no_kw2'"]
>>> reset_log()
If a transaction is aborted, no hook is called.
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, ["OOPS!"])
>>> transaction.abort()
>>> log
[]
>>> transaction.commit()
>>> log
[]
The hook is called after the commit is done, so even if the
commit fails the hook will have been called. To provoke failures in
commit, we'll add failing resource manager to the transaction.
>>> class CommitFailure(Exception):
... pass
>>> class FailingDataManager:
... def tpc_begin(self, txn, sub=False):
... raise CommitFailure
... def abort(self, txn):
... pass
>>> t = transaction.begin()
>>> t.join(FailingDataManager())
>>> t.addAfterCommitHook(hook, '2')
>>> t.commit()
Traceback (most recent call last):
...
CommitFailure
>>> log
["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
Let's register several hooks.
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, '4', dict(kw1='4.1'))
>>> t.addAfterCommitHook(hook, '5', dict(kw2='5.2'))
They are returned in the same order by getAfterCommitHooks.
>>> [(hook.func_name, args, kws) #doctest: +NORMALIZE_WHITESPACE
... for hook, args, kws in t.getAfterCommitHooks()]
[('hook', ('4',), {'kw1': '4.1'}),
('hook', ('5',), {'kw2': '5.2'})]
And commit also calls them in this order.
>>> t.commit()
>>> len(log)
2
>>> log #doctest: +NORMALIZE_WHITESPACE
["True arg '4' kw1 '4.1' kw2 'no_kw2'",
"True arg '5' kw1 'no_kw1' kw2 '5.2'"]
>>> reset_log()
While executing, a hook can itself add more hooks, and they will all
be called before the real commit starts.
>>> def recurse(status, txn, arg):
... log.append('rec' + str(arg))
... if arg:
... txn.addAfterCommitHook(hook, '-')
... txn.addAfterCommitHook(recurse, (txn, arg-1))
>>> t = transaction.begin()
>>> t.addAfterCommitHook(recurse, (t, 3))
>>> transaction.commit()
>>> log #doctest: +NORMALIZE_WHITESPACE
['rec3',
"True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec2',
"True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec1',
"True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec0']
>>> reset_log()
If an after commit hook is raising an exception then it will log a
message at error level so that if other hooks are registered they
can be executed. We don't support execution dependencies at this level.
>>> mgr = transaction.TransactionManager()
>>> do = DataObject(mgr)
>>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
... raise TypeError("Fake raise")
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, ('-', 1))
>>> t.addAfterCommitHook(hookRaise, ('-', 2))
>>> t.addAfterCommitHook(hook, ('-', 3))
>>> transaction.commit()
>>> log
["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"]
>>> reset_log()
Test that the associated transaction manager has been cleanup when
after commit hooks are registered
>>> mgr = transaction.TransactionManager()
>>> do = DataObject(mgr)
>>> t = transaction.begin()
>>> len(t._manager._txns)
1
>>> t.addAfterCommitHook(hook, ('-', 1))
>>> transaction.commit()
>>> log
["True arg '-' kw1 1 kw2 'no_kw2'"]
>>> len(t._manager._txns)
0
>>> reset_log()
The transaction is already committed when the after commit hooks
will be executed. Executing the hooks must not have further
effects on persistent objects.
Start a new transaction
>>> t = transaction.begin()
Create a DB instance and add a IOBTree within
>>> from ZODB.tests.util import DB
>>> from ZODB.tests.util import P
>>> db = DB()
>>> con = db.open()
>>> root = con.root()
>>> root['p'] = P('julien')
>>> p = root['p']
>>> p.name
'julien'
This hook will get the object from the `DB` instance and change
the flag attribute.
>>> def badhook(status, arg=None, kw1='no_kw1', kw2='no_kw2'):
... p.name = 'jul'
Now register this hook and commit.
>>> t.addAfterCommitHook(badhook, (p, 1))
>>> transaction.commit()
Nothing should have changed since it should have been aborted.
>>> p.name
'julien'
>>> db.close()
"""
def
test_suite
():
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment