-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hi there,

I wrote a document, that is still a draft, describing the ZODB
transaction extensions CPSCore is defining. You will find the what,
when, why and how.

http://svn.nuxeo.org/trac/pub/file/CPSCore/trunk/doc/transaction_managers.txt

Note, some parts are not finished yet.

Thanks to Anahide for the review.

Cheers,

        J.

P.S : I joined the document to this mail for the lazy ones who don't
want to click on the link ;)

- --
Julien Anguenot | Nuxeo R&D (Paris, France)
CPS Platform : http://www.cps-project.org
Zope3 / ECM   : http://www.z3lab.org
mail: anguenot at nuxeo.com; tel: +33 (0) 6 72 57 57 66
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.1 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iD8DBQFDovsCGhoG8MxZ/pIRAo07AJ9lbi1//GZsBDSJyUolAKSozDOvrQCdG4Ns
ZbI5rXHNPML5WEaKKt6a4ns=
=0qZO
-----END PGP SIGNATURE-----
==========================
CPS Transaction extensions
==========================

:Author: Julien Anguenot
:Revision: $Id: transaction_managers.txt 30754 2005-12-16 17:30:25Z janguenot $

Status, Draft v1

OUTLINE

1. Introduction and motivation

2. ZODB before commit hooks
  2.1 Introduction
  2.2 Transaction API
  2.3 Examples

3. CPS before commit hook extensions
  3.1 Simple ordering policy for execution
  3.2 CPS default subscribers
  3.3 Design
    3.3.2 IBaseManager / BaseManager
    3.3.3 TransactionManager
    3.3.4 IndexationManager
    3.3.5 EventManager
    3.3.6 TreeCacheManager

4. FAQ
  o Which version of ZODB do I have to use ?
  o Why the ZODB policy is not part of the core ZODB transaction ?
  o How can I have back a real time indexation within my tests ?
  o How can I deactivate a given transaction subscriber ?
  o How can I define and register my own subscriber ?
  o Can I launch non transactional operations using before commit hook
    subscribers ?

CONTENTS

1. Introduction and motivations

  The first motivation behind this extension was performance. On large scale
  projects, where a huge amount of items are stored within the catalog, we
  realized that operations like creation, edition, deletion or workflow
  actions were taking more and more time as the catalog grew up.

  The first bottleneck were the CMF and DCWorkflow that are performing a lot
  of non optimum or even useless indexation, all along transactions. The
  first reflex I had was to check if I could reduce the indexation calls and
  see how many indexation, at most, I could get. It ended up being an hack
  and I realized, that anyway, I couldn't be sure of the amount of
  indexation that would occur for a given transaction: I couldn't prevent
  third party code using the CPS framework from making indexation calls,
  thus decreasing the overall CPS based application performance.

  The first step was to extend the ZODB transaction mechanism to support
  commit hooks that would execute transactional code just *before* the actual
  transaction commit time, but at the end of the "regular" transaction
  execution. See section 2 for more information about this.

  With this ZODB transaction extension, we defined the indexation manager
  which was a real win for performance, since we ended up having an atomic
  indexation, whatever the transaction was dealing with.

  We realized soon after that we could use additional commit hooks to manage
  other non optimum internal CPS tasks. CPS extends the CMF and provides an
  event service machinery allowing to receive events, and dispatch them to
  interested parties. So we defined an event manager, dispatching user
  notifications based on events, and a tree cache manager, maintaining a
  tree cache for evident performance issues. It seems that the Plone guys
  never really get that<wink>. The main idea is to sort, filter and process
  events as *less* as possible, being aware that events are sent several
  times in the event channel at runtime, without having a complete control
  on this flow: think about third party code.

  We defined a commit hook ordering policy for our CPS managers it
  was clear that we would be faced to subscriber execution dependencies. See
  section 3 for more information about this.

  The section 4 is a small devel FAQ.

  Note about the terms used in this document

   - before commit hook : this is the before commit hooks that are registered
     on the ZODB transaction itself. (see section 2)

   - transaction subscriber : in the context of the CPSCore transaction
     manager it refers to a commit hook that is registered on the
     CPSCore transaction manager itself and not on the ZODB. (see section 3)

2. ZODB before commit hooks

  2.1 Introduction

    ZODB transaction allows to register hooks to be called before the
    transaction is committed.

    The specified hook functions will be called after the transaction commit
    method has been called, but before the commit process has been started.

    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. If the transaction is
    aborted, hooks are not called, and are discarded. Calling a hook
    "consumes" its registration too: hook registrations do not persist
    across transactions.

  2.2 Transaction API

    The ZODB transaction provides the following public API related to
    before commit hooks::

    >>> def addBeforeCommitHook(hook, args=(), kws=None):
    ...     """Register a hook to call before the transaction is committed.
    ...     """
    >>>

    >>> def getBeforeCommitHooks():
    ...     """Return iterable producing the registered addBeforeCommit 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.
    ...     """
    >>>

   2.3 Examples

     Let's check some examples of use extracted from the ZODB tests.

     Let's define a hook to call, and a way to see that it was called.

        >>> log = []
        >>> def reset_log():
        ...     del log[:]

        >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
        ...     log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))

      Now register the hook with a transaction.

        >>> import transaction
        >>> t = transaction.begin()
        >>> t.addBeforeCommitHook(hook, '1')

      We can see that the hook is indeed registered.

        >>> [(hook.func_name, args, kws)
        ...  for hook, args, kws in t.getBeforeCommitHooks()]
        [('hook', ('1',), {})]

      When transaction commit starts, the hook is called, with its
      arguments.

        >>> log
        []
        >>> t.commit()
        >>> log
        ["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.getBeforeCommitHooks()))
        0
        >>> transaction.commit()
        >>> log
        []

      The hook is only called for a full commit, not for a savepoint or
      subtransaction.

        >>> t = transaction.begin()
        >>> t.addBeforeCommitHook(hook, 'A', dict(kw1='B'))
        >>> dummy = t.savepoint()
        >>> log
        []
        >>> t.commit(subtransaction=True)
        >>> log
        []
        >>> t.commit()
        >>> log
        ["arg 'A' kw1 'B' kw2 'no_kw2'"]
        >>> reset_log()

      If a transaction is aborted, no hook is called.

        >>> t = transaction.begin()
        >>> t.addBeforeCommitHook(hook, ["OOPS!"])
        >>> transaction.abort()
        >>> log
        []
        >>> transaction.commit()
        >>> log
        []

      The hook is called before the commit does anything, 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 tpc_abort(self, txn):
        ...         pass
        ...     def abort(self, txn):
        ...         pass

        >>> t = transaction.begin()
        >>> t.join(FailingDataManager())

        >>> t.addBeforeCommitHook(hook, '2')
        >>> t.commit()
        Traceback (most recent call last):
        ...
        CommitFailure
        >>> log
        ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
        >>> reset_log()

      Let's register several hooks.

        >>> t = transaction.begin()
        >>> t.addBeforeCommitHook(hook, '4', dict(kw1='4.1'))
        >>> t.addBeforeCommitHook(hook, '5', dict(kw2='5.2'))

      They are returned in the same order by getBeforeCommitHooks.

        >>> [(hook.func_name, args, kws)     #doctest: +NORMALIZE_WHITESPACE
        ...  for hook, args, kws in t.getBeforeCommitHooks()]
        [('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
        ["arg '4' kw1 '4.1' kw2 'no_kw2'",
         "arg '5' kw1 'no_kw1' kw2 '5.2'"]
        >>> reset_log()


3. CPS before commit hook extensions

  3.1 Simple ordering policy for subscriber execution

    The ZODB transaction has an implicit policy dealing with the order of
    execution of its before commit hooks: it is the order in which they have
    been registered. Despite the fact that you can register several hooks on
    the transaction, you have no way to order them. In fact, you can rarely
    predict in which order the hooks will be registered at runtime.

    CPS transaction manager defines a trivial and senseful ordering policy
    using an order of registration as an integer value specifying the order
    level for each hook.

    For instance, a hook registered with order=1 will be invoked after
    another hook registered with order=-1 and before another registered with
    order=2, regardless of which was registered first. When two hooks are
    registered with the same order, the first one registered is called
    first.


  3.2 CPS default subscribers and execution dependency

    CPS defines ans use three (3) default hooks.

     o IndexationManager
     o TreeCacheManager
     o EventManager

    They are executed in the following order :

     1. IndexationManager (order -100)
     2. TreeCacheManager  (order 0)
     3. EventManager      (order 100)

    Remember this if you want to define, register and use your own
    manager. (See last section, it contains an example of such a definition)

    Let's explain a bit why we choose this order of execution.

    It's pretty clear that the EventManager should be executed after
    the IndexationManager and the TreeCacheManager since this manager
    deals with the results of all the events that were sent during the
    transaction. Thus, the EventManager can be interested with the
    events the IndexationManager and the TreeCacheManager can throw on
    the channel.

    The IndexationManager is called at first because the
    TreeCacheManager might be interested of asking the catalog before
    rebuilding the tree for information.

    ATTENTION ! An important point here is the fact the the above CPS
    managers are not registered on the ZODB transaction itself but on
    the CPS transaction manager. You need to understand that the only
    before commit hook that CPS is adding on the ZODB transaction
    itself is the CPS transaction manager because it is the one
    defining the order of execution. (see section 4 to see why the
    ZODB transaction doesn't deal with order of execution)

  3.3 Design

    3.3.1 IBaseManager / BaseManager

    This is the default interface and base class for all the
    before commit hooks including the transaction manager (see below)

    Here is the IBaseInterface interface definition.

        >>> import zope.interface
        >>> class IBaseManager(zope.interface.Interface):
        ...     """Base Manager definition
        ...
        ...     Provides a base interface for before commit hooks definitions
        ...     """
        ...
        ...     DEFAULT_SYNC = zope.interface.Attribute('DEFAULT_SYNC',
        ...     "Default sync mode")
        ...
        ...     DEFAULT_STATUS = zope.interface.Attribute('DEFAULT_STATUS',
        ...     "Default status")
        ...
        ...     def setSynchronous(sync):
        ...         """Set queuing mode.
        ...         """
        ...
        ...     def isSynchronous():
        ...         """Get queuing mode.
        ...         """
        ...
        ...     def __call__():
        ...         """Called when transaction commits.
        ...
        ...         Does the actual manager work.
        ...         """
        ...
        ...     def enable():
        ...         """Enable the manager
        ...         """
        ...
        ...     def disable():
        ...         """Disable the manager
        ...         """

      It provides the following behavior :

        o sync mode
           - sync = False (end of the transaction)
           - sync = True (real time)

        o status
           - status = True (enabled)
           ¯ status = False (disabled)

        o __call__()
           - method that does the actual job.

      This is what you should be implementing if you want to define
      your own subscriber to benefit from the CPS integration. (sync,
      status, etc...)

      I guess, this part could be shared at the Zope layer. I'll try
      to sketch a proposal someday on Zope3.

    3.3.2 TransactionManager

      o What is it ?

        Transaction manager for CPS that will be responsible for the
        execution of ZODB transaction hooks registred by CPS. As well, note
        that the transaction manager is responsible for the execution of
        hooks (not the ZODB transaction itself !). This object will be
        registred as a ZODB before commit hook.

        Multiple hooks can be registered and will be called in the order
        they were registered (first registered, first called), except that
        hooks registered with different `order` arguments are invoked from
        smallest `order` value to largest. `order` must be an integer, and
        defaults to 0.

      o API of the TransactionManager class

        >>> def addBeforeCommitHook(self, hook, args=(), kws=None, order=0):
        ...     """Register a hook to call before the transaction is committed.
        ...
        ...     The specified hook function will be called after the
        ...     transaction's commit method has been called, but before
        ...     the commit process has been started.  The hook will be
        ...     passed the specified positional (`args`) and keyword
        ...     (`kws`) arguments.  `args` is a sequence of positional
        ...     arguments to be passed, defaulting to an empty tuple (no
        ...     positional arguments are 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), except that hooks registered with different
        ...     `order` arguments are invoked from smallest `order` value
        ...     to largest.  `order` must be an integer, and defaults to
        ...     0.
        ...
        ...     For instance, a hook registered with order=1 will be
        ...     invoked after another hook registered with order=-1 and
        ...     before another registered with order=2, regardless of
        ...     which was registered first.  When two hooks are registered
        ...     with the same order, the first one registered is called
        ...     first.
        ...
        ...     Hooks are called only for a top-level commit.  A
        ...     subtransaction commit or savepoint creation does not call
        ...     any hooks.  If the transaction is aborted, hooks are not
        ...     called, and are discarded.  Calling a hook "consumes" its
        ...     registration too: hook registrations do not persist across
        ...     transactions.  If it's desired to call the same hook on
        ...     every transaction commit, then addBeforeCommitHook() 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.
        ...     """


      The CPSCore.TransactionManager module provides an helper as well
      creating a TransactionManager instance, on the transaction itself, and
      registers it as a ZODB before commit hook.

        >>> from Products.CPSCore.TransactionManager import \
        ... get_transaction_manager
        >>> txn_manager = get_transaction_manager()

      Let's take an example :

        >>> log = []
        >>> def reset_log():
        ...     del log[:]

        >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
        ...     log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))

        >>> from Products.CPSCore.TransactionManager import \
        ... get_transaction_manager
        >>> mgr = get_transaction_manager()

        Register hooks

          >>> mgr.addBeforeCommitHook(hook, '1', order=0)
          >>> mgr.addBeforeCommitHook(hook, '2', order=-999999)
          >>> mgr.addBeforeCommitHook(hook, '3', order=999999)
          >>> mgr.addBeforeCommitHook(hook, '4', order=0)
          >>> mgr.addBeforeCommitHook(hook, '5', order=999999)
          >>> mgr.addBeforeCommitHook(hook, '6', order=-999999)
          >>> mgr.addBeforeCommitHook(hook, '7', order=0)

        Execute

          >>> mgr()

        The hooks are executed in the order of execution

          >>> log
          ["arg '2' kw1 'no_kw1' kw2 'no_kw2'", "arg '6' kw1 'no_kw1' kw2 
'no_kw2'", "arg '1' kw1 'no_kw1' kw2 'no_kw2'", "arg '4' kw1 'no_kw1' kw2 
'no_kw2'", "arg '7' kw1 'no_kw1' kw2 'no_kw2'", "arg '3' kw1 'no_kw1' kw2 
'no_kw2'", "arg '5' kw1 'no_kw1' kw2 'no_kw2'"]

          >>> reset_log()

    3.3.3 IndexationManager

      o Goal

        During a transaction we want always a single indexation per object,
        including the security indexe update, no matter what appends during
        this transaction. We assume third party code can request indexation
        itself and we want the same result in this case too. ONE SINGLE
        INDEXATION PER TRANSACTION NO MATTER WHAT APPENDS.

      o Introduction

        Manager for indexation that can be delayed until commit time.
        Order of execution -100. This is the first hook called from the
        CPS default ones within a stock CPS instance.

      o Get the indexation manager

        >>> from Products.CPSCore.IndexationManager import \
        ... get_indexation_manager
        >>> mgr = get_indexation_manager()

      o Public API

        >>> def push(self, ob, idxs=None, with_security=False):
        ...     """Add / update an object to reindex to the reindexing queue.
        ...
        ...     Copes with security reindexation as well.
        ...     """

        You may find interesting to check the module unit tests over there
        for practical examples : CPSCore/tests/test_indexation_manager.py

      o CPS Integration

        This manager required us to change a bit our infrastructure. CPS
        proxies inherits from ProxyBase.ProxyBase and redefine the following
        CMFCore.CMFCatalogAware methods reindexObject() and
        reindexObjectSecurity().

        We changed the ProxyBase.reindexObject() method this way :

        >>> def reindexObject(self, idxs=[]):
        ...     get_indexation_manager().push(self, idxs=idxs)
        >>>

        And the reindexObjectSecurity() one :

        >>> def reindexObjectSecurity(self):
        ...     get_indexation_manager().push(self, with_security=True)
        >>>

        Both methods are now just invoking the indexation manager to
        schedule their own indexation.

        The methods doing the actual reindexation are now private on the
        ProxyBase class :

          def _reindexObjectSecurity(self, skip_self=False):
              # Implementation

          def reindexObject(self, idxs=[]):
              # Implementation

        The Indexation Manager will call this method when processing objects
        that have been pushed for indexation.

    3.3.4 TreeCacheManager

      o Goal

        We want only one single tree cache rebuilt per object per
        transaction.

      o Introduction

        CPS defines a Trees Tool, that caches some information about
        the site's hierarchies for evident performance issues. Trees
        Tool updates are realized after some event have been sent on
        the CPS event channel. The events can be thrown several times
        with different informations.

     XXX to finish

     o CPSCore/TreeCacheManager

      Manager for tree cache that can be delayed until commit time.
      Order of execution 0

      CPSCore/TreeCacheManager.py
      CPSCore/tests/test_treecache_manager.py

    3.3.5 EventManager (CPSSubscriptions)

      Manager for events with a delayed processing until commit time
      Order of execution 100

      CPSSubscriptions/EventManager.py
      CPSSusbcriptions/test_event_manager.py

      XXX to finish

4. FAQ

  o Which version of ZODB do I have to use ?

    The implementation of the transaction before commit hook as
    described above is included within the release 3.6 of ZODB.

    Zope-2.8.x has ZODB 3.4.x shipped within but CPS provides a compat
    product adding the 3.6 implementation to 3.4.
    See CPSCompat/trunk/PatchZODBTransaction.py

    You can thus benefit from this extension using latest CPS-3.3.x
    with Zope-2.8.x

  o Why the trivial before commit hook subscribers ordering policy, that
    CPS is providing, is not part of the ZODB core transaction ?

    Hum,... You may wonder ? An endless discussion occurred on the
    zodb-dev mailing list. You may Check this thread if you're
    interested :

    http://mail.zope.org/pipermail/zodb-dev/2005-August/009090.html

    We should be able to propose having a part of the CPS extension at
    the Zope layer for better code reuse across frameworks on top of
    Zope.

  o How can I have back a real time indexation within my tests ?

    You can change the SYNC status of a subscribers to True.

      >>> from Products.CPSCore.IndexationManager import get_indexation_manager
      >>> get_indexation_manager().setSynchronous(True)

    After this the indexation will behave as if no subscriber had been
    registered for the indexation.

    Note, you can do this for all all the subscribers implementing
    IBaseManager.

  o How can I deactivate a given transaction subscriber ?

    You may disable for a certain time, within the transaction, a subscriber.

    For instance, how to deactivate the indexation during a period of
    the transaction :

      >>> from Products.CPSCore.IndexationManager import get_indexation_manager
      >>> get_indexation_manager().disable()

    Here no object will be scheduled for indexation anymore. It means,
    the indexation that should have occurred will *not* be during this time.

      >>> get_indexation_manager().enable()

    Here, the subscriber will queue back objects and processed them
    for indexation.

  o How can I define, register and use my own subscriber ?

    You can of course define and register your own subscribers

    Define it. It implements IBaseManager

      >>> from Products.CPSCore.interfaces import IBaseManager
      >>> from Products.CPSCore.BaseManager import BaseManager
      >>> import zope.interface

      This manager will be executed just before the
      IndexationManager. (Check the section 3 for the CPS defined
      orders) It's order will be -101

      >>> _TXN_MGR_ORDER = -101

      >>> class MessageManager(BaseManager):
      ...     zope.interface.implements(IBaseManager)
      ...
      ...     def __init__(self, mgr):
      ...         """Initialize and register this manager within the
      ...         transaction.
      ...         Here, we specified the order of this subscriber
      ...         """
      ...         BaseManager.__init__(self, mgr, order=_TXN_MGR_ORDER)
      ...         self._queue = []
      ...
      ...     def push(self, str_):
      ...         """Push the string within the queue."""
      ...         if not self._status:
      ...             return
      ...         if self._sync:
      ...             print str_
      ...         if str_ not in self._queue:
      ...             self._queue.append(str_)
      ...
      ...     def __call__(self):
      ...         # do the actual async job
      ...         print ' '.join(self._queue)
      >>>

    As we saw in section3 , the CPS TransactionManager is responsible
    of the CPS subscriber execution. Thus we need to register the
    MessageManager on the CPS TransactionManager.

      >>> from Products.CPSCore.TransactionManager import \
      ... get_transaction_manager

    As well, we need to create an instance of the Manager at some
    point. We choose to store it on the transaction iself. Don't
    forget it's a *non* persistent object. We will store this instance
    under the following id.

      >>> _TXN_MY_MANAGER = '_cps_my_manager'

    We need to define a method that will register this hook against
    the CPS transaction manager and create an instance of the Manager
    on the transaction itself.

      >>> import transaction
      >>> def get_message_manager():
      ...     """Get the  manager.
      ...
      ...     Creates it *if* needed.
      ...     """
      ...     txn = transaction.get()
      ...     mgr = getattr(txn, _TXN_MY_MANAGER, None)
      ...     if mgr is None:
      ...         mgr = MessageManager(get_transaction_manager())
      ...         setattr(txn, _TXN_MY_MANAGER, mgr)
      ...         return mgr
      >>>

    Now, you can use it within your code.
    To get the manager let's use the helper function :

      >>> txn_manager = get_message_manager()
      >>> isinstance(txn_manager, MessageManager)
      True

    You can then push strings within your manager

      >>> txn_manager.push('CPS')
      >>> txn_manager.push('rocks')

    You can deactivate the manager during the transaction

      >>> txn_manager.disable()
      >>> txn_manager.push('sucks')

    You can activate it back and then continue using it.

      >>> txn_manager.enable()
      >>> txn_manager.push('!')

    Then when the transaction will be committed the manager will be
    executed.

     >>> transaction.commit()
     CPS rocks !

   You can use the Manager in real time

     >>> txn_manager = get_message_manager()

   To change the sync mode use this API :

     >>> txn_manager.setSynchronous(True)
     <BLANKLINE>

   Then pushing display the result directly

     >>> txn_manager.push('CPS is great !')
     CPS is great !

  o Can I launch non transactional operations using before commit hook
    subscribers ?

    You *cannot* execute non transactional code using before commit
    hook subscribers since the execution of the subscribers are done
    at the end of the transaction but not *after* the
    transaction. ZODB should be supporting another API soon for this
    kind of use case. Think about committing on a non transactional RDF
    database for instance.

_______________________________________________
cps-devel mailing list
http://lists.nuxeo.com/mailman/listinfo/cps-devel

Reply via email to