[hook/operation] nicer api to achieve same result as set_operation, as described in #1253630
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Sat, 09 Oct 2010 00:05:50 +0200
changeset 6426 541659c39f6a
parent 6425 8d7c2fd2ac66
child 6427 c8a5ac2d1eaa
[hook/operation] nicer api to achieve same result as set_operation, as described in #1253630
hooks/integrity.py
hooks/metadata.py
hooks/security.py
hooks/syncschema.py
server/hook.py
server/migractions.py
server/repository.py
server/sources/native.py
server/sources/storages.py
server/ssplanner.py
--- a/hooks/integrity.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/hooks/integrity.py	Sat Oct 09 00:05:50 2010 +0200
@@ -31,7 +31,6 @@
 from cubicweb.selectors import is_instance
 from cubicweb.uilib import soup2xhtml
 from cubicweb.server import hook
-from cubicweb.server.hook import set_operation
 
 # special relations that don't have to be checked for integrity, usually
 # because they are handled internally by hooks (so we trust ourselves)
@@ -68,19 +67,21 @@
         _release_unique_cstr_lock(self.session)
 
 
-class _CheckRequiredRelationOperation(hook.LateOperation):
-    """checking relation cardinality has to be done after commit in
-    case the relation is being replaced
+class _CheckRequiredRelationOperation(hook.DataOperationMixIn,
+                                      hook.LateOperation):
+    """checking relation cardinality has to be done after commit in case the
+    relation is being replaced
     """
+    containercls = list
     role = key = base_rql = None
 
     def precommit_event(self):
-        session =self.session
+        session = self.session
         pendingeids = session.transaction_data.get('pendingeids', ())
         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
         # poping key is not optional: if further operation trigger new deletion
         # of relation, we'll need a new operation
-        for eid, rtype in session.transaction_data.pop(self.key):
+        for eid, rtype in self.get_data():
             # recheck pending eids / relation types
             if eid in pendingeids:
                 continue
@@ -98,13 +99,11 @@
 class _CheckSRelationOp(_CheckRequiredRelationOperation):
     """check required subject relation"""
     role = 'subject'
-    key = '_cwisrel'
     base_rql = 'Any O WHERE S eid %%(x)s, S %s O'
 
 class _CheckORelationOp(_CheckRequiredRelationOperation):
     """check required object relation"""
     role = 'object'
-    key = '_cwiorel'
     base_rql = 'Any S WHERE O eid %%(x)s, S %s O'
 
 
@@ -131,11 +130,10 @@
             rdef = rschema.role_rdef(eschema, targetschemas[0], role)
             if rdef.role_cardinality(role) in '1+':
                 if role == 'subject':
-                    set_operation(self._cw, '_cwisrel', (eid, rschema.type),
-                                  _CheckSRelationOp, list)
+                    op = _CheckSRelationOp.get_instance(self._cw)
                 else:
-                    set_operation(self._cw, '_cwiorel', (eid, rschema.type),
-                                  _CheckORelationOp, list)
+                    op = _CheckORelationOp.get_instance(self._cw)
+                op.add_data((eid, rschema.type))
 
     def before_delete_relation(self):
         rtype = self.rtype
@@ -148,19 +146,17 @@
             return
         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
-            set_operation(self._cw, '_cwisrel', (eidfrom, rtype),
-                          _CheckSRelationOp, list)
+            _CheckSRelationOp.get_instance(self._cw).add_data((eidfrom, rtype))
         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
-            set_operation(self._cw, '_cwiorel', (eidto, rtype),
-                          _CheckORelationOp, list)
+            _CheckORelationOp.get_instance(self._cw).add_data((eidto, rtype))
 
 
-class _CheckConstraintsOp(hook.LateOperation):
+class _CheckConstraintsOp(hook.DataOperationMixIn, hook.LateOperation):
     """ check a new relation satisfy its constraints """
-
+    containercls = list
     def precommit_event(self):
         session = self.session
-        for values in session.transaction_data.pop('check_constraints_op'):
+        for values in self.get_data():
             eidfrom, rtype, eidto, constraints = values
             # first check related entities have not been deleted in the same
             # transaction
@@ -196,9 +192,8 @@
         constraints = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
                                                 'constraints')
         if constraints:
-            hook.set_operation(self._cw, 'check_constraints_op',
-                               (self.eidfrom, self.rtype, self.eidto, tuple(constraints)),
-                               _CheckConstraintsOp, list)
+            _CheckConstraintsOp.get_instance(self._cw).add_data(
+                (self.eidfrom, self.rtype, self.eidto, constraints))
 
 
 class CheckAttributeConstraintHook(IntegrityHook):
@@ -217,9 +212,8 @@
                 constraints = [c for c in eschema.rdef(attr).constraints
                                if isinstance(c, (RQLUniqueConstraint, RQLConstraint))]
                 if constraints:
-                    hook.set_operation(self._cw, 'check_constraints_op',
-                                       (self.entity.eid, attr, None, tuple(constraints)),
-                                       _CheckConstraintsOp, list)
+                    _CheckConstraintsOp.get_instance(self._cw).add_data(
+                        (self.entity.eid, attr, None, constraints))
 
 
 class CheckUniqueHook(IntegrityHook):
@@ -297,11 +291,11 @@
 # 'active' integrity hooks: you usually don't want to deactivate them, they are
 # not really integrity check, they maintain consistency on changes
 
-class _DelayedDeleteOp(hook.Operation):
+class _DelayedDeleteOp(hook.DataOperationMixIn, hook.Operation):
     """delete the object of composite relation except if the relation has
     actually been redirected to another composite
     """
-    key = base_rql = None
+    base_rql = None
 
     def precommit_event(self):
         session = self.session
@@ -309,7 +303,7 @@
         neweids = session.transaction_data.get('neweids', ())
         # poping key is not optional: if further operation trigger new deletion
         # of composite relation, we'll need a new operation
-        for eid, rtype in session.transaction_data.pop(self.key):
+        for eid, rtype in self.get_data():
             # don't do anything if the entity is being created or deleted
             if not (eid in pendingeids or eid in neweids):
                 etype = session.describe(eid)[0]
@@ -317,12 +311,10 @@
 
 class _DelayedDeleteSEntityOp(_DelayedDeleteOp):
     """delete orphan subject entity of a composite relation"""
-    key = '_cwiscomp'
     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y'
 
 class _DelayedDeleteOEntityOp(_DelayedDeleteOp):
     """check required object relation"""
-    key = '_cwiocomp'
     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X'
 
 
@@ -343,8 +335,8 @@
         composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
                                               'composite')
         if composite == 'subject':
-            set_operation(self._cw, '_cwiocomp', (self.eidto, self.rtype),
-                          _DelayedDeleteOEntityOp)
+            _DelayedDeleteOEntityOp.get_instance(self._cw).add_data(
+                (self.eidto, self.rtype))
         elif composite == 'object':
-            set_operation(self._cw, '_cwiscomp', (self.eidfrom, self.rtype),
-                          _DelayedDeleteSEntityOp)
+            _DelayedDeleteSEntityOp.get_instance(self._cw).add_data(
+                (self.eidfrom, self.rtype))
--- a/hooks/metadata.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/hooks/metadata.py	Sat Oct 09 00:05:50 2010 +0200
@@ -64,11 +64,11 @@
             self.entity.cw_edited.setdefault('modification_date', datetime.now())
 
 
-class _SetCreatorOp(hook.Operation):
+class SetCreatorOp(hook.DataOperationMixIn, hook.Operation):
 
     def precommit_event(self):
         session = self.session
-        for eid in session.transaction_data.pop('set_creator_op'):
+        for eid in self.get_data():
             if session.deleted_in_transaction(eid):
                 # entity have been created and deleted in the same transaction
                 continue
@@ -109,11 +109,12 @@
     def __call__(self):
         if not self._cw.is_internal_session:
             self._cw.add_relation(self.entity.eid, 'owned_by', self._cw.user.eid)
-            hook.set_operation(self._cw, 'set_creator_op', self.entity.eid, _SetCreatorOp)
+            SetCreatorOp.get_instance(self._cw).add_data(self.entity.eid)
+
 
-class _SyncOwnersOp(hook.Operation):
+class SyncOwnersOp(hook.DataOperationMixIn, hook.Operation):
     def precommit_event(self):
-        for compositeeid, composedeid in self.session.transaction_data.pop('sync_owners_op'):
+        for compositeeid, composedeid in self.get_data():
             self.session.execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,'
                                  'NOT EXISTS(X owned_by U, X eid %(x)s)',
                                  {'c': compositeeid, 'x': composedeid})
@@ -133,9 +134,9 @@
         eidfrom, eidto = self.eidfrom, self.eidto
         composite = self._cw.schema_rproperty(self.rtype, eidfrom, eidto, 'composite')
         if composite == 'subject':
-            hook.set_operation(self._cw, 'sync_owners_op', (eidfrom, eidto), _SyncOwnersOp)
+            SyncOwnersOp.get_instance(self._cw).add_data( (eidfrom, eidto) )
         elif composite == 'object':
-            hook.set_operation(self._cw, 'sync_owners_op', (eidto, eidfrom), _SyncOwnersOp)
+            SyncOwnersOp.get_instance(self._cw).add_data( (eidto, eidfrom) )
 
 
 class FixUserOwnershipHook(MetaDataHook):
--- a/hooks/security.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/hooks/security.py	Sat Oct 09 00:05:50 2010 +0200
@@ -45,23 +45,20 @@
             rdef.check_perm(session, 'update', eid=eid)
 
 
-class _CheckEntityPermissionOp(hook.LateOperation):
+class CheckEntityPermissionOp(hook.DataOperationMixIn, hook.LateOperation):
     def precommit_event(self):
-        #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action
         session = self.session
-        for values in session.transaction_data.pop('check_entity_perm_op'):
-            eid, action, edited = values
+        for eid, action, edited in self.get_data():
             entity = session.entity_from_eid(eid)
             entity.cw_check_perm(action)
             check_entity_attributes(session, entity, edited,
-                                    creation=self.creation)
+                                    creation=(action == 'add'))
 
 
-class _CheckRelationPermissionOp(hook.LateOperation):
+class CheckRelationPermissionOp(hook.DataOperationMixIn, hook.LateOperation):
     def precommit_event(self):
         session = self.session
-        for args in session.transaction_data.pop('check_relation_perm_op'):
-            action, rschema, eidfrom, eidto = args
+        for action, rschema, eidfrom, eidto in self.get_data():
             rdef = rschema.rdef(session.describe(eidfrom)[0],
                                 session.describe(eidto)[0])
             rdef.check_perm(session, action, fromeid=eidfrom, toeid=eidto)
@@ -85,9 +82,8 @@
     events = ('after_add_entity',)
 
     def __call__(self):
-        hook.set_operation(self._cw, 'check_entity_perm_op',
-                           (self.entity.eid, 'add', self.entity.cw_edited),
-                           _CheckEntityPermissionOp, creation=True)
+        CheckEntityPermissionOp.get_instance(self._cw).add_data(
+            (self.entity.eid, 'add', self.entity.cw_edited) )
 
 
 class AfterUpdateEntitySecurityHook(SecurityHook):
@@ -104,9 +100,8 @@
             # save back editedattrs in case the entity is reedited later in the
             # same transaction, which will lead to cw_edited being
             # overwritten
-            hook.set_operation(self._cw, 'check_entity_perm_op',
-                               (self.entity.eid, 'update', self.entity.cw_edited),
-                               _CheckEntityPermissionOp, creation=False)
+            CheckEntityPermissionOp.get_instance(self._cw).add_data(
+                (self.entity.eid, 'update', self.entity.cw_edited) )
 
 
 class BeforeDelEntitySecurityHook(SecurityHook):
@@ -143,9 +138,8 @@
                 return
             rschema = self._cw.repo.schema[self.rtype]
             if self.rtype in ON_COMMIT_ADD_RELATIONS:
-                hook.set_operation(self._cw, 'check_relation_perm_op',
-                                   ('add', rschema, self.eidfrom, self.eidto),
-                                   _CheckRelationPermissionOp)
+                CheckRelationPermissionOp.get_instance(self._cw).add_data(
+                    ('add', rschema, self.eidfrom, self.eidto) )
             else:
                 rdef = rschema.rdef(self._cw.describe(self.eidfrom)[0],
                                     self._cw.describe(self.eidto)[0])
--- a/hooks/syncschema.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/hooks/syncschema.py	Sat Oct 09 00:05:50 2010 +0200
@@ -307,11 +307,10 @@
             return # watched changes to final relation type are unexpected
         session = self.session
         if 'fulltext_container' in self.values:
+            op = UpdateFTIndexOp.get_instance(session)
             for subjtype, objtype in rschema.rdefs:
-                hook.set_operation(session, 'fti_update_etypes', subjtype,
-                                   UpdateFTIndexOp)
-                hook.set_operation(session, 'fti_update_etypes', objtype,
-                                   UpdateFTIndexOp)
+                op.add_data(subjtype)
+                op.add_data(objtype)
         # update the in-memory schema first
         self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
         self.rschema.__dict__.update(self.values)
@@ -603,8 +602,7 @@
             syssource.update_rdef_null_allowed(self.session, rdef)
             self.null_allowed_changed = True
         if 'fulltextindexed' in self.values:
-            hook.set_operation(session, 'fti_update_etypes', rdef.subject,
-                               UpdateFTIndexOp)
+            UpdateFTIndexOp.get_instance(session).add_data(rdef.subject)
 
     def revertprecommit_event(self):
         if self.rdef is None:
@@ -1181,19 +1179,20 @@
 
 
 
-class UpdateFTIndexOp(hook.SingleLastOperation):
+class UpdateFTIndexOp(hook.DataOperationMixIn, hook.SingleLastOperation):
     """operation to update full text indexation of entity whose schema change
 
-    We wait after the commit to as the schema in memory is only updated after the commit.
+    We wait after the commit to as the schema in memory is only updated after
+    the commit.
     """
 
     def postcommit_event(self):
         session = self.session
         source = session.repo.system_source
-        to_reindex = session.transaction_data.pop('fti_update_etypes', ())
+        schema = session.repo.vreg.schema
+        to_reindex = self.get_data()
         self.info('%i etypes need full text indexed reindexation',
                   len(to_reindex))
-        schema = self.session.repo.vreg.schema
         for etype in to_reindex:
             rset = session.execute('Any X WHERE X is %s' % etype)
             self.info('Reindexing full text index for %i entity of type %s',
@@ -1205,8 +1204,8 @@
                     if still_fti or container is not entity:
                         source.fti_unindex_entity(session, container.eid)
                         source.fti_index_entity(session, container)
-        if len(to_reindex):
-            # Transaction have already been committed
+        if to_reindex:
+            # Transaction has already been committed
             session.pool.commit()
 
 
--- a/server/hook.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/server/hook.py	Sat Oct 09 00:05:50 2010 +0200
@@ -239,9 +239,8 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. autoclass:: cubicweb.server.hook.Hook
 .. autoclass:: cubicweb.server.hook.Operation
+.. autoclass:: cubicweb.server.hook.DataOperation
 .. autoclass:: cubicweb.server.hook.LateOperation
-.. autofunction:: cubicweb.server.hook.set_operation
-
 """
 
 from __future__ import with_statement
@@ -726,6 +725,99 @@
 def _container_add(container, value):
     {set: set.add, list: list.append}[container.__class__](container, value)
 
+
+class DataOperationMixIn(object):
+    """Mix-in class to ease applying a single operation on a set of data,
+    avoiding to create as many as operation as they are individual modification.
+    The body of the operation must then iterate over the values that have been
+    stored in a single operation instance.
+
+    You should try to use this instead of creating on operation for each
+    `value`, since handling operations becomes costly on massive data import.
+
+    Usage looks like:
+    .. sourcecode:: python
+
+        class MyEntityHook(Hook):
+            __regid__ = 'my.entity.hook'
+            __select__ = Hook.__select__ & is_instance('MyEntity')
+            events = ('after_add_entity',)
+
+            def __call__(self):
+                MyOperation.get_instance(self._cw).add_data(self.entity)
+
+
+        class MyOperation(DataOperation, DataOperationMixIn):
+            def precommit_event(self):
+                for bucket in self.get_data():
+                    process(bucket)
+
+    You can modify the `containercls` class attribute, which defines the
+    container class that should be instantiated to hold payloads. An instance is
+    created on instantiation, and then the :meth:`add_data` method will add the
+    given data to the existing container. Default to a `set`. Give `list` if you
+    want to keep arrival ordering. You can also use another kind of container
+    by redefining :meth:`_build_container` and :meth:`add_data`
+
+    More optional parameters can be given to the `get_instance` operation, that
+    will be given to the operation constructer (though those parameters should
+    not vary accross different calls to this method for a same operation for
+    obvious reason).
+
+    .. Note::
+        For sanity reason `get_data` will reset the operation, so that once
+        the operation has started its treatment, if some hook want to push
+        additional data to this same operation, a new instance will be created
+        (else that data has a great chance to be never treated). This implies:
+
+        * you should **always** call `get_data` when starting treatment
+
+        * you should **never** call `get_data` for another reason.
+    """
+    containercls = set
+
+    @classproperty
+    def data_key(cls):
+        return ('cw.dataops', cls.__name__)
+
+    @classmethod
+    def get_instance(cls, session, **kwargs):
+        # no need to lock: transaction_data already comes from thread's local storage
+        try:
+            return session.transaction_data[cls.data_key]
+        except KeyError:
+            op = session.transaction_data[cls.data_key] = cls(session, **kwargs)
+            return op
+
+    def __init__(self, *args, **kwargs):
+        super(DataOperationMixIn, self).__init__(*args, **kwargs)
+        self._container = self._build_container()
+        self._processed = False
+
+    def __contains__(self, value):
+        return value in self._container
+
+    def _build_container(self):
+        return self.containercls()
+
+    def add_data(self, data):
+        assert not self._processed, """Trying to add data to a closed operation.
+Iterating over operation data closed it and should be reserved to precommit /
+postcommit method of the operation."""
+        _container_add(self._container, data)
+
+    def get_data(self):
+        assert not self._processed, """Trying to get data from a closed operation.
+Iterating over operation data closed it and should be reserved to precommit /
+postcommit method of the operation."""
+        self._processed = True
+        op = self.session.transaction_data.pop(self.data_key)
+        assert op is self, "Bad handling of operation data, found %s instead of %s for key %s" % (
+            op, self, self.data_key)
+        return self._container
+
+
+@deprecated('[3.10] use opcls.get_instance(session, **opkwargs).add_data(value)')
 def set_operation(session, datakey, value, opcls, containercls=set, **opkwargs):
     """Function to ease applying a single operation on a set of data, avoiding
     to create as many as operation as they are individual modification. You
@@ -766,9 +858,6 @@
        **poping** the key from `transaction_data` is not an option, else you may
        get unexpected data loss in some case of nested hooks.
     """
-
-
-
     try:
         # Search for session.transaction_data[`datakey`] (expected to be a set):
         # if found, simply append `value`
@@ -861,7 +950,7 @@
             execute(*rql)
 
 
-class CleanupNewEidsCacheOp(SingleLastOperation):
+class CleanupNewEidsCacheOp(DataOperationMixIn, SingleLastOperation):
     """on rollback of a insert query we have to remove from repository's
     type/source cache eids of entities added in that transaction.
 
@@ -871,28 +960,27 @@
     too expensive. Notice that there is no pb when using args to specify eids
     instead of giving them into the rql string.
     """
+    data_key = 'neweids'
 
     def rollback_event(self):
         """the observed connections pool has been rollbacked,
         remove inserted eid from repository type/source cache
         """
         try:
-            self.session.repo.clear_caches(
-                self.session.transaction_data['neweids'])
+            self.session.repo.clear_caches(self.get_data())
         except KeyError:
             pass
 
-class CleanupDeletedEidsCacheOp(SingleLastOperation):
+class CleanupDeletedEidsCacheOp(DataOperationMixIn, SingleLastOperation):
     """on commit of delete query, we have to remove from repository's
     type/source cache eids of entities deleted in that transaction.
     """
-
+    data_key = 'pendingeids'
     def postcommit_event(self):
         """the observed connections pool has been rollbacked,
         remove inserted eid from repository type/source cache
         """
         try:
-            self.session.repo.clear_caches(
-                self.session.transaction_data['pendingeids'])
+            self.session.repo.clear_caches(self.get_data())
         except KeyError:
             pass
--- a/server/migractions.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/server/migractions.py	Sat Oct 09 00:05:50 2010 +0200
@@ -960,8 +960,7 @@
                     # get some validation error on commit since integrity hooks
                     # may think some required relation is missing... This also ensure
                     # repository caches are properly cleanup
-                    hook.set_operation(session, 'pendingeids', eid,
-                                       hook.CleanupDeletedEidsCacheOp)
+                    CleanupDeletedEidsCacheOp.get_instance(session).add_data(eid)
                     # and don't forget to remove record from system tables
                     self.repo.system_source.delete_info(
                         session, session.entity_from_eid(eid, rdeftype),
--- a/server/repository.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/server/repository.py	Sat Oct 09 00:05:50 2010 +0200
@@ -1031,8 +1031,7 @@
         and index the entity with the full text index
         """
         # begin by inserting eid/type/source/extid into the entities table
-        hook.set_operation(session, 'neweids', entity.eid,
-                           hook.CleanupNewEidsCacheOp)
+        hook.CleanupNewEidsCacheOp.get_instance(session).add_data(entity.eid)
         self.system_source.add_info(session, entity, source, extid, complete)
 
     def delete_info(self, session, entity, sourceuri, extid):
@@ -1041,8 +1040,7 @@
         """
         # mark eid as being deleted in session info and setup cache update
         # operation
-        hook.set_operation(session, 'pendingeids', entity.eid,
-                           hook.CleanupDeletedEidsCacheOp)
+        hook.CleanupDeletedEidsCacheOp.get_instance(session).add_data(entity.eid)
         self._delete_info(session, entity, sourceuri, extid)
 
     def _delete_info(self, session, entity, sourceuri, extid):
--- a/server/sources/native.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/server/sources/native.py	Sat Oct 09 00:05:50 2010 +0200
@@ -1220,8 +1220,7 @@
                 "no more supported" % {'eid': eid, 'etype': etype})]
         entity.eid = eid
         # for proper eid/type cache update
-        hook.set_operation(session, 'pendingeids', eid,
-                           CleanupDeletedEidsCacheOp)
+        CleanupDeletedEidsCacheOp.get_instance(session).add_data(eid)
         self.repo.hm.call_hooks('before_delete_entity', session, entity=entity)
         # remove is / is_instance_of which are added using sql by hooks, hence
         # unvisible as transaction action
@@ -1288,7 +1287,7 @@
         """create an operation to [re]index textual content of the given entity
         on commit
         """
-        hook.set_operation(session, 'ftindex', entity.eid, FTIndexEntityOp)
+        FTIndexEntityOp.get_instance(session).add_data(entity.eid)
 
     def fti_unindex_entity(self, session, eid):
         """remove text content for entity with the given eid from the full text
@@ -1313,7 +1312,7 @@
             self.exception('error while reindexing %s', entity)
 
 
-class FTIndexEntityOp(hook.LateOperation):
+class FTIndexEntityOp(hook.DataOperationMixIn, hook.LateOperation):
     """operation to delay entity full text indexation to commit
 
     since fti indexing may trigger discovery of other entities, it should be
@@ -1326,7 +1325,7 @@
         source = session.repo.system_source
         pendingeids = session.transaction_data.get('pendingeids', ())
         done = session.transaction_data.setdefault('indexedeids', set())
-        for eid in session.transaction_data.pop('ftindex', ()):
+        for eid in self.get_data():
             if eid in pendingeids or eid in done:
                 # entity added and deleted in the same transaction or already
                 # processed
--- a/server/sources/storages.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/server/sources/storages.py	Sat Oct 09 00:05:50 2010 +0200
@@ -136,7 +136,7 @@
             # bytes storage used to store file's path
             entity.cw_edited.edited_attribute(attr, Binary(fpath))
             file(fpath, 'wb').write(binary.getvalue())
-            hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp)
+            AddFileOp.get_instance(entity._cw).add_data(fpath)
         return binary
 
     def entity_updated(self, entity, attr):
@@ -167,20 +167,19 @@
             file(fpath, 'wb').write(binary.getvalue())
             # Mark the new file as added during the transaction.
             # The file will be removed on rollback
-            hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp)
+            AddFileOp.get_instance(entity._cw).add_data(fpath)
         if oldpath != fpath:
             # register the new location for the file.
             entity.cw_edited.edited_attribute(attr, Binary(fpath))
             # Mark the old file as useless so the file will be removed at
             # commit.
-            hook.set_operation(entity._cw, 'bfss_deleted', oldpath,
-                               DeleteFileOp)
+            DeleteFileOp.get_instance(entity._cw).add_data(oldpath)
         return binary
 
     def entity_deleted(self, entity, attr):
         """an entity using this storage for attr has been deleted"""
         fpath = self.current_fs_path(entity, attr)
-        hook.set_operation(entity._cw, 'bfss_deleted', fpath, DeleteFileOp)
+        DeleteFileOp.get_instance(entity._cw).add_data(fpath)
 
     def new_fs_path(self, entity, attr):
         # We try to get some hint about how to name the file using attribute's
@@ -223,17 +222,17 @@
         entity.cw_edited = None
 
 
-class AddFileOp(hook.Operation):
+class AddFileOp(hook.DataOperationMixIn, hook.Operation):
     def rollback_event(self):
-        for filepath in self.session.transaction_data.pop('bfss_added'):
+        for filepath in self.get_data():
             try:
                 unlink(filepath)
             except Exception, ex:
                 self.error('cant remove %s: %s' % (filepath, ex))
 
-class DeleteFileOp(hook.Operation):
+class DeleteFileOp(hook.DataOperationMixIn, hook.Operation):
     def postcommit_event(self):
-        for filepath in self.session.transaction_data.pop('bfss_deleted'):
+        for filepath in self.get_data():
             try:
                 unlink(filepath)
             except Exception, ex:
--- a/server/ssplanner.py	Sat Oct 09 00:05:49 2010 +0200
+++ b/server/ssplanner.py	Sat Oct 09 00:05:50 2010 +0200
@@ -651,13 +651,9 @@
         # mark eids as being deleted in session info and setup cache update
         # operation (register pending eids before actual deletion to avoid
         # multiple call to glob_delete_entity)
-        try:
-            pending = session.transaction_data['pendingeids']
-        except KeyError:
-            pending = session.transaction_data['pendingeids'] = set()
-            CleanupDeletedEidsCacheOp(session)
-        actual = todelete - pending
-        pending |= actual
+        op = CleanupDeletedEidsCacheOp.get_instance(session)
+        actual = todelete - op._container
+        op._container |= actual
         for eid in actual:
             delete(session, eid)
         return results