#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Support for data managers.
"""
from __future__ import print_function, absolute_import, division
__docformat__ = "restructuredtext en"
logger = __import__('logging').getLogger(__name__)
from zope import interface
import transaction
from transaction.interfaces import IDataManagerSavepoint
from transaction.interfaces import ISavepointDataManager
__all__ = [
'ObjectDataManager',
'OrderedNearEndObjectDataManager',
'do',
'do_near_end',
]
[docs]@interface.implementer(ISavepointDataManager, IDataManagerSavepoint)
class ObjectDataManager(object):
"""
A generic (and therefore relatively expensive)
:class:`transaction.interfaces.IDataManager` that invokes a
callable (usually a method of an object) when the transaction
finishes successfully. The method should not raise exceptions when
invoked, as they will be caught and ignored (to preserve
consistency with the rest of the data managers). If there's a
chance the method could fail, then whatever actions it does take
should not have side-effects.
These data managers have no guaranteed relationship to other data
managers in terms of the order of which they commit, except as
documented with :meth:`sortKey`.
Because these data managers execute exactly one operation on a
complete transaction commit, implementing a savepoint is trivial:
do nothing when it is rolled back. A savepoint is created to checkpoint
a transaction and rolled back to reverse actions taken *after* the
checkpoint. Only data managers that were active (joined) at the
time the transaction savepoint is created are asked to create
their own savepoint, and then potentially to roll it back. We do
no work until the transaction is committed, so we have nothing
to rollback. Moroever, if a transaction savepoint is activated
before a manager joins, then that manager is not asked for its own
savepoint: it is simply aborted and unjoined from the transaction if
the previous savepoint is rolledback.
"""
_EMPTY_KWARGS = {}
def __init__(self, target=None, method_name=None, call=None,
vote=None, args=(), kwargs=None):
"""
Construct a data manager. You must pass either the `target` and `method_name` arguments
or the `call` argument. (You may always pass the `target` argument, which will
be made available on this object for the use of :meth:`tpc_vote`. )
:param target: An object. Optional if `call` is given. If provided, will be used
to compute the :meth:`sortKey`.
:param string method_name: The name of the attribute to get from `target`.
Optional if `callable` is given.
:param callable call: A callable object. Ignored if `target` *and* `method_name` are given.
:param callable vote: If given, then a callable that will be called during the voting phase.
It should raise an exception if the transaction will fail.
:param args: A sequence of arguments to pass to the callable object. Optional.
:param kwargs: A dictionary of arguments to pass to the callable object. Optional.
"""
self.target = target
if method_name:
self.callable = getattr(target, method_name)
else:
self.callable = call
assert self.callable is not None
self.args = args
self.kwargs = kwargs or self._EMPTY_KWARGS
self.vote = vote
# Use the default thread transaction manager.
self.transaction_manager = transaction.manager
def commit(self, tx):
pass
def abort(self, tx):
pass
[docs] def sortKey(self):
"""
Return the string value that, when sorted, determines the
order in which data managers will get to vote and commit at
the end of a transaction. (See
:meth:`transaction.interfaces.IDataManager.sortKey`).
The default implementation of this method uses the ID of
either the ``target`` object we were initialized with or the ID of
the actual callable we will call. This has the property of
ensuring that *all* calls to methods of a particular object
instance (when ``target`` is given), or calls to a particular callable
(when ``target`` is not given) will execute in the order in which they were
added to the transaction.
.. note:: This relies on an implementation detail of the
transaction package, which sorts using :meth:`list.sort`,
which is guaranteed to be stable: thus objects using the
same key remain in the same relative order. (See
:meth:`transaction._transaction.Transaction._commitResources`.)
To execute only calls to a particular method of a particular instance
in the order they are added to the transaction, but allow other
methods to execute before or after them, do not provide the ``target``.
It is not advisable to use the ID of this object (``self``) in
the implementation of this method, because the ID values are
not guaranteed to be monotonically increasing and thus
instances of a particular class that did this would execute in
"random" order.
"""
# It's not clearly documented, but this is supposed to be a string
return str(id(self.target) if self.target is not None else id(self.callable))
# No subtransaction support.
[docs] def abort_sub(self, tx):
"Does nothing"
commit_sub = abort_sub
[docs] def beforeCompletion(self, tx):
"Does nothing"
afterCompletion = beforeCompletion
def tpc_begin(self, tx, subtransaction=False): # pylint:disable=unused-argument
assert not subtransaction
def tpc_vote(self, tx): # pylint:disable=unused-argument
if self.vote:
self.vote()
def tpc_finish(self, tx): # pylint:disable=unused-argument
self.callable(*self.args, **self.kwargs)
tpc_abort = abort
def __repr__(self):
return '<%s.%s at %s for %r>' % (self.__class__.__module__, self.__class__.__name__,
id(self),
self.callable)
# ISavepointDataManager
def savepoint(self):
return self
# IDatamanagerSavepoint
def rollback(self):
# See class comments: we have nothing to rollback
# from because we take no action until commit
# anyway.
pass
[docs]class OrderedNearEndObjectDataManager(ObjectDataManager):
"""
A special extension of :class:`ObjectDataManager` that attempts to execute
after all other data managers have executed. This is useful when an
operation relies on the execution of other data managers.
.. versionadded:: 1.1
"""
[docs] def sortKey(self):
"""
Sort prepended with z's in an attempt to execute after other data
managers.
"""
parent_key = super(OrderedNearEndObjectDataManager, self).sortKey()
sort_str = str(self.target) if self.target is not None else str(self.callable)
return 'zzz%s:%s' % (sort_str, parent_key)
[docs]def do(*args, **kwargs):
"""
Establishes a IDataManager in the current transaction.
See :class:`ObjectDataManager` for the possible arguments.
"""
klass = kwargs.pop('datamanager_class', ObjectDataManager)
result = klass(*args, **kwargs)
transaction.get().join(result)
return result
[docs]def do_near_end(*args, **kwargs):
"""
Establishes a IDataManager in the current transaction that will attempt to
execute *after* all other DataManagers have had their say.
See :class:`ObjectDataManager` for the possible arguments.
.. versionadded:: 1.1
"""
kwargs['datamanager_class'] = OrderedNearEndObjectDataManager
return do(*args, **kwargs)