server/hook.py
changeset 2835 04034421b072
child 2840 06daf13195d4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/hook.py	Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,311 @@
+"""Hooks management
+
+This module defined the `Hook` class and registry and a set of abstract classes
+for operations.
+
+
+Hooks are called before / after any individual update of entities / relations
+in the repository and on special events such as server startup or shutdown.
+
+
+Operations may be registered by hooks during a transaction, which will  be
+fired when the pool is commited or rollbacked.
+
+
+Entity hooks (eg before_add_entity, after_add_entity, before_update_entity,
+after_update_entity, before_delete_entity, after_delete_entity) all have an
+`entity` attribute
+
+Relation (eg before_add_relation, after_add_relation, before_delete_relation,
+after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes.
+
+Server start/stop hooks (eg server_startup, server_shutdown) have a `repo`
+attribute, but *their `cw_req` attribute is None*.
+
+Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a
+`timestamp` attributes, but *their `cw_req` attribute is None*.
+
+Session hooks (eg session_open, session_close) have no special attribute.
+
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from warnings import warn
+from logging import getLogger
+
+from logilab.common.decorators import classproperty
+from logilab.common.logging_ext import set_log_methods
+
+from cubicweb.cwvreg import CWRegistry, VRegistry
+from cubicweb.selectors import (objectify_selector, lltrace, match_search_state,
+                                entity_implements)
+from cubicweb.appobject import AppObject
+
+
+ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
+                      'before_update_entity', 'after_update_entity',
+                      'before_delete_entity', 'after_delete_entity'))
+RELATIONS_HOOKS = set(('before_add_relation',   'after_add_relation' ,
+                       'before_delete_relation','after_delete_relation'))
+SYSTEM_HOOKS = set(('server_backup', 'server_restore',
+                    'server_startup', 'server_shutdown',
+                    'session_open', 'session_close'))
+ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
+
+
+class HooksRegistry(CWRegistry):
+
+    def register(self, obj, **kwargs):
+        for event in obj.events:
+            if event not in ALL_HOOKS:
+                raise Exception('bad event %s on %s' % (event, obj))
+        super(HooksRegistry, self).register(obj, **kwargs)
+
+    def call_hooks(self, event, req=None, **kwargs):
+        kwargs['event'] = event
+        # XXX remove .enabled
+        for hook in sorted([x for x in self.possible_objects(req, **kwargs)
+                            if x.enabled], key=lambda x: x.order):
+            hook()
+
+VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry
+
+
+# some hook specific selectors #################################################
+
+@objectify_selector
+@lltrace
+def match_event(cls, req, **kwargs):
+    if kwargs.get('event') in cls.events:
+        return 1
+    return 0
+
+@objectify_selector
+@lltrace
+def enabled_category(cls, req, **kwargs):
+    if req is None:
+        # server startup / shutdown event
+        config = kwargs['repo'].config
+    else:
+        config = req.vreg.config
+    if enabled_category in config.disabled_hooks_categories:
+        return 0
+    return 1
+
+@objectify_selector
+@lltrace
+def regular_session(cls, req, **kwargs):
+    if req is None or req.is_super_session:
+        return 0
+    return 1
+
+class match_rtype(match_search_state):
+    """accept if parameters specified as initializer arguments are specified
+    in named arguments given to the selector
+
+    :param *expected: parameters (eg `basestring`) which are expected to be
+                      found in named arguments (kwargs)
+    """
+
+    @lltrace
+    def __call__(self, cls, req, *args, **kwargs):
+        return kwargs.get('rtype') in self.expected
+
+
+# base class for hook ##########################################################
+
+class Hook(AppObject):
+    __registry__ = 'hooks'
+    __select__ = match_event() & enabled_category()
+    # set this in derivated classes
+    events = None
+    category = None
+    order = 0
+    # XXX deprecates
+    enabled = True
+
+    @classproperty
+    def __id__(cls):
+        warn('[3.5] %s: please specify an id for your hook' % cls)
+        return str(id(cls))
+
+    @classmethod
+    def __registered__(cls, vreg):
+        super(Hook, cls).__registered__(vreg)
+        if getattr(cls, 'accepts', None):
+            warn('[3.5] %s: accepts is deprecated, define proper __select__' % cls)
+            rtypes = []
+            for ertype in cls.accepts:
+                if ertype.islower():
+                    rtypes.append(ertype)
+                else:
+                    cls.__select__ = cls.__select__ & entity_implements(ertype)
+            if rtypes:
+                cls.__select__ = cls.__select__ & match_rtype(*rtypes)
+        return cls
+
+    known_args = set(('entity', 'rtype', 'eidfrom', 'eidto', 'repo', 'timestamp'))
+    def __init__(self, req, event, **kwargs):
+        for arg in self.known_args:
+            if arg in kwargs:
+                setattr(self, arg, kwargs.pop(arg))
+        super(Hook, self).__init__(req, **kwargs)
+        self.event = event
+
+    def __call__(self):
+        if hasattr(self, 'call'):
+            warn('[3.5] %s: call is deprecated, implements __call__' % self.__class__)
+            if self.event.endswith('_relation'):
+                self.call(self.cw_req, self.eidfrom, self.rtype, self.eidto)
+            elif 'delete' in self.event:
+                self.call(self.cw_req, self.entity.eid)
+            elif self.event.startswith('server_'):
+                self.call(self.repo)
+            elif self.event.startswith('session_'):
+                self.call(self.cw_req)
+            else:
+                self.call(self.cw_req, self.entity)
+
+set_log_methods(Hook, getLogger('cubicweb.hook'))
+
+
+# abstract classes for operation ###############################################
+
+class Operation(object):
+    """an operation is triggered on connections pool events related to
+    commit / rollback transations. Possible events are:
+
+    precommit:
+      the pool is preparing to commit. You shouldn't do anything things which
+      has to be reverted if the commit fail at this point, but you can freely
+      do any heavy computation or raise an exception if the commit can't go.
+      You can add some new operation during this phase but their precommit
+      event won't be triggered
+
+    commit:
+      the pool is preparing to commit. You should avoid to do to expensive
+      stuff or something that may cause an exception in this event
+
+    revertcommit:
+      if an operation failed while commited, this event is triggered for
+      all operations which had their commit event already to let them
+      revert things (including the operation which made fail the commit)
+
+    rollback:
+      the transaction has been either rollbacked either
+      * intentionaly
+      * a precommit event failed, all operations are rollbacked
+      * a commit event failed, all operations which are not been triggered for
+        commit are rollbacked
+
+    order of operations may be important, and is controlled according to:
+    * operation's class
+    """
+
+    def __init__(self, session, **kwargs):
+        self.session = session
+        self.user = session.user
+        self.repo = session.repo
+        self.schema = session.repo.schema
+        self.config = session.repo.config
+        self.__dict__.update(kwargs)
+        self.register(session)
+        # execution information
+        self.processed = None # 'precommit', 'commit'
+        self.failed = False
+
+    def register(self, session):
+        session.add_operation(self, self.insert_index())
+
+    def insert_index(self):
+        """return the index of  the lastest instance which is not a
+        LateOperation instance
+        """
+        for i, op in enumerate(self.session.pending_operations):
+            if isinstance(op, (LateOperation, SingleLastOperation)):
+                return i
+        return None
+
+    def handle_event(self, event):
+        """delegate event handling to the opertaion"""
+        getattr(self, event)()
+
+    def precommit_event(self):
+        """the observed connections pool is preparing a commit"""
+
+    def revertprecommit_event(self):
+        """an error went when pre-commiting this operation or a later one
+
+        should revert pre-commit's changes but take care, they may have not
+        been all considered if it's this operation which failed
+        """
+
+    def commit_event(self):
+        """the observed connections pool is commiting"""
+
+    def revertcommit_event(self):
+        """an error went when commiting this operation or a later one
+
+        should revert commit's changes but take care, they may have not
+        been all considered if it's this operation which failed
+        """
+
+    def rollback_event(self):
+        """the observed connections pool has been rollbacked
+
+        do nothing by default, the operation will just be removed from the pool
+        operation list
+        """
+
+set_log_methods(Operation, getLogger('cubicweb.session'))
+
+
+class LateOperation(Operation):
+    """special operation which should be called after all possible (ie non late)
+    operations
+    """
+    def insert_index(self):
+        """return the index of  the lastest instance which is not a
+        SingleLastOperation instance
+        """
+        for i, op in enumerate(self.session.pending_operations):
+            if isinstance(op, SingleLastOperation):
+                return i
+        return None
+
+
+class SingleOperation(Operation):
+    """special operation which should be called once"""
+    def register(self, session):
+        """override register to handle cases where this operation has already
+        been added
+        """
+        operations = session.pending_operations
+        index = self.equivalent_index(operations)
+        if index is not None:
+            equivalent = operations.pop(index)
+        else:
+            equivalent = None
+        session.add_operation(self, self.insert_index())
+        return equivalent
+
+    def equivalent_index(self, operations):
+        """return the index of the equivalent operation if any"""
+        equivalents = [i for i, op in enumerate(operations)
+                       if op.__class__ is self.__class__]
+        if equivalents:
+            return equivalents[0]
+        return None
+
+
+class SingleLastOperation(SingleOperation):
+    """special operation which should be called once and after all other
+    operations
+    """
+    def insert_index(self):
+        return None