[Checkins] SVN: Products.QueueCatalog/trunk/ - Added a second catalog queue conflict resolution strategy and made

Jens Vagelpohl jens at dataflake.org
Tue May 9 11:31:38 EDT 2006


Log message for revision 68067:
  - Added a second catalog queue conflict resolution strategy and made
    the strategy configurable via the 'Configure' ZMI tab. The new policy
    can be tried when ZODB conflict errors emanating from the catalog
    queue buckets become a problem. It attempts to avoid conflicts by
    doing even more guessing about what the behavior should be, rather
    than deeming a conflicting situation as too insane to handle and
    raising a ConflictError.
  
  - Removed all uses of zLOG in favor of the Python logging module
  
  - Updated some help screens
  

Changed:
  U   Products.QueueCatalog/trunk/CHANGES.txt
  U   Products.QueueCatalog/trunk/CatalogEventQueue.py
  U   Products.QueueCatalog/trunk/QueueCatalog.py
  U   Products.QueueCatalog/trunk/help/QueueCatalog-Configure.stx
  U   Products.QueueCatalog/trunk/tests/test_CatalogEventQueue.py
  U   Products.QueueCatalog/trunk/tests/test_QueueCatalog.py
  U   Products.QueueCatalog/trunk/www/edit.zpt

-=-
Modified: Products.QueueCatalog/trunk/CHANGES.txt
===================================================================
--- Products.QueueCatalog/trunk/CHANGES.txt	2006-05-09 11:30:28 UTC (rev 68066)
+++ Products.QueueCatalog/trunk/CHANGES.txt	2006-05-09 15:31:37 UTC (rev 68067)
@@ -2,6 +2,18 @@
 
   After QueueCatalog 1.3
 
+    - Added a second catalog queue conflict resolution strategy and made
+      the strategy configurable via the 'Configure' ZMI tab. The new policy
+      can be tried when ZODB conflict errors emanating from the catalog
+      queue buckets become a problem. It attempts to avoid conflicts by
+      doing even more guessing about what the behavior should be, rather
+      than deeming a conflicting situation as too insane to handle and 
+      raising a ConflictError.
+
+    - Removed all uses of zLOG in favor of the Python logging module
+
+    - Updated some help screens
+
     - Added a dependencies document
 
     - Updated the tests to run on Zope 2.8+

Modified: Products.QueueCatalog/trunk/CatalogEventQueue.py
===================================================================
--- Products.QueueCatalog/trunk/CatalogEventQueue.py	2006-05-09 11:30:28 UTC (rev 68066)
+++ Products.QueueCatalog/trunk/CatalogEventQueue.py	2006-05-09 15:31:37 UTC (rev 68067)
@@ -15,10 +15,16 @@
 $Id$
 """
 
+import logging
+
 from Persistence import Persistent
 from ZODB.POSException import ConflictError
-from time import time as current_wall_time
 
+logger = logging.getLogger('event.QueueCatalog')
+
+SAFE_POLICY = 0
+ALTERNATIVE_POLICY = 1
+
 REMOVED       = 0
 ADDED         = 1
 CHANGED       = 2
@@ -108,10 +114,13 @@
     
     """
 
-    def __init__(self):
+    _conflict_policy = SAFE_POLICY
 
+    def __init__(self, conflict_policy=SAFE_POLICY):
+
         # Mapping from uid -> (generation, event type)
         self._data = {}
+        self._conflict_policy = conflict_policy
 
     def __nonzero__(self):
         return not not self._data
@@ -175,6 +184,10 @@
         # the transaction being undone and newdata is the data for the
         # transaction previous to the undone transaction.
 
+        # Find the conflict policy on the new state to make sure changes
+        # to it will be applied
+        policy = newstate['_conflict_policy']
+
         # Committed is always the currently committed data.
         oldstate_data  =  oldstate['_data']
         committed_data = committed['_data']
@@ -185,6 +198,7 @@
 
             # Decide if this is a change
             old = oldstate_data.get(uid)
+            current = committed_data.get(uid)
             
             if new != old:
                 # something changed
@@ -197,7 +211,15 @@
                         # (undone) event. 
                         new = (0, antiEvent(old[1]))
                     elif new[1] is ADDED:
-                        raise ConflictError
+                        if policy == SAFE_POLICY:
+                            logger.error('Queue conflict on %s: ADDED on existing item' % uid)
+                            raise ConflictError
+                        else:
+                            if current and current[1] == REMOVED:
+                                new = current
+                            else:
+                                new = (current[0]+1, CHANGED_ADDED)
+                            
 
                     # remove this event from old, so that we don't
                     # mess with it later.
@@ -206,11 +228,24 @@
                 # Check aqainst current value. Either we want a
                 # different event, in which case we give up, or we
                 # do nothing.
-                current = committed_data.get(uid)
                 if current is not None:
                     if current[1] != new[1]:
-                        # This is too complicated, bail
-                        raise ConflictError
+                        if policy == SAFE_POLICY:
+                            # This is too complicated, bail
+                            logger.error('Queue conflict on %s' % uid)
+                            raise ConflictError
+                        elif REMOVED not in (new[1], current[1]):
+                            new = (current[0]+1, CHANGED_ADDED)
+                            committed_data[uid] = new
+                        elif ( current[0] < new[0] and
+                               new[1] == REMOVED ):
+                            committed_data[uid] = new
+
+                        # remove this event from old, so that we don't
+                        # mess with it later.
+                        if oldstate_data.get(uid) is not None:
+                            del oldstate_data[uid]
+
                     # nothing to do
                     continue
 
@@ -226,13 +261,16 @@
             if current is not None:
                 if current[1] != new[1]:
                     # This is too complicated, bail
+                    logger.error('Queue conflict on %s processing undos' % uid)
                     raise ConflictError
                 # nothing to do
                 continue
 
             committed_data[uid] = new
 
-        return {'_data': committed_data}
+        return { '_data': committed_data
+               , '_conflict_policy' : policy
+               }
 
 __doc__ = CatalogEventQueue.__doc__ + __doc__
 

Modified: Products.QueueCatalog/trunk/QueueCatalog.py
===================================================================
--- Products.QueueCatalog/trunk/QueueCatalog.py	2006-05-09 11:30:28 UTC (rev 68066)
+++ Products.QueueCatalog/trunk/QueueCatalog.py	2006-05-09 15:31:37 UTC (rev 68067)
@@ -15,14 +15,15 @@
 $Id$
 """
 # Python std. lib
-import sets, sys
+import logging
+import sets
+import sys
 from time import time
 from types import StringType
 
 # Other packages
 from ZODB.POSException import ConflictError
 from ZEO.Exceptions import ClientDisconnected
-from zLOG import LOG, INFO, BLATHER, ERROR
 from zExceptions import Unauthorized
 from ExtensionClass import Base
 from OFS.SimpleItem import SimpleItem
@@ -39,7 +40,9 @@
 # Local
 from CatalogEventQueue import CatalogEventQueue, EVENT_TYPES, ADDED_EVENTS
 from CatalogEventQueue import ADDED, CHANGED, CHANGED_ADDED, REMOVED
+from CatalogEventQueue import SAFE_POLICY, ALTERNATIVE_POLICY
 
+logger = logging.getLogger('event.QueueCatalog')
 
 _zcatalog_methods = {
     'catalog_object': 1,
@@ -107,16 +110,24 @@
                              #       indexes
     title = ''
 
+
     # When set, _v_catalog_cache is a tuple containing the wrapped ZCatalog
     # and the REQUEST it is bound to.
     _v_catalog_cache = None
 
-    def __init__(self, buckets=1009):
+    # As an alternative to the original queue conflict handling there is now
+    # a policy which will reduce conflicts, but at the cost of possibly having
+    # situations where items get cataloged unnecessarily. YMMV.
+    _conflict_policy = SAFE_POLICY
+
+    def __init__(self, buckets=1009, conflict_policy=SAFE_POLICY):
         self._buckets = buckets
+        self._conflict_policy = conflict_policy
         self._clearQueues()
 
     def _clearQueues(self):
-        self._queues = [CatalogEventQueue() for i in range(self._buckets)]
+        self._queues = [ CatalogEventQueue(self.getConflictPolicy()) 
+                              for i in range(self._buckets) ]
 
     def getTitle(self):
         return self.title
@@ -190,7 +201,26 @@
         self._buckets = int(count)
         self._clearQueues()
 
+    security.declareProtected(view_management_screens, 'getConflictPolicy')
+    def getConflictPolicy(self):
+        """ Return the currently-used conflict policy
+        """
+        return self._conflict_policy
 
+    security.declareProtected(view_management_screens, 'setConflictPolicy')
+    def setConflictPolicy(self, policy=SAFE_POLICY):
+        """ Set the conflic policy to be used
+        """
+        try:
+            policy = int(policy)
+        except ValueError:
+            return
+
+        if ( policy in (SAFE_POLICY, ALTERNATIVE_POLICY) and
+             policy != self.getConflictPolicy() ):
+            self._conflict_policy = policy
+            self._clearQueues()
+
     security.declareProtected(manage_zcatalog_entries, 'getZCatalog')
     def getZCatalog(self, method=''):
         ZC = None
@@ -258,10 +288,11 @@
         # update_metadata=0 is ignored if the queued catalog is set to
         # update metadata during queue processing, rather than immediately
 
-        # similarly, limiting the idxs only limits the immediate indexes.  If
-        # any work needs to be done in the queue processing, it will all be         # done: we have not implemented partial indexing during queue
-        # processing.  The only way to avoid any of it is to avoid all of it
-        # (i.e., update metadata immediately and don't have any indexes to
+        # similarly, limiting the idxs only limits the immediate indexes.  If 
+        # any work needs to be done in the queue processing, it will all be
+        # done: we have not implemented partial indexing during queue 
+        # processing.  The only way to avoid any of it is to avoid all of it 
+        # (i.e., update metadata immediately and don't have any indexes to 
         # update on the queued side).
 
         # Make sure the current context is allowed to do this:
@@ -378,7 +409,7 @@
                 except (ConflictError, ClientDisconnected):
                     raise
                 except:
-                    LOG('QueueCatalog', ERROR, 'error uncataloging object',                         error=sys.exc_info())
+                    logger.error('error uncataloging object', exc_info=True)
             else:
                 # add or change
                 if event is CHANGED and not cataloged(catalog, uid):
@@ -394,8 +425,7 @@
                     except (ConflictError, ClientDisconnected):
                         raise
                     except:
-                        LOG('QueueCatalog', ERROR, 'error cataloging object',
-                            error=sys.exc_info())
+                        logger.error('error cataloging object', exc_info=True)
 
             count = count + 1
 
@@ -451,7 +481,7 @@
     security.declareProtected(view_management_screens, 'manage_edit')
     def manage_edit(self, title='', location='', immediate_indexes=(),
                     immediate_removal=0, bucket_count=0, immediate_metadata=0,
-                    all_indexes=0, RESPONSE=None):
+                    all_indexes=0, conflict_policy=SAFE_POLICY, RESPONSE=None):
         """ Edit the instance """
         self.title = title
         self.setLocation(location or None)
@@ -459,6 +489,7 @@
         self.setImmediateRemoval(immediate_removal)
         self.setImmediateMetadataUpdate(immediate_metadata)
         self.setProcessAllIndexes(all_indexes)
+        self.setConflictPolicy(conflict_policy)
         if bucket_count:
             bucket_count = int(bucket_count)
             if bucket_count != self.getBucketCount():

Modified: Products.QueueCatalog/trunk/help/QueueCatalog-Configure.stx
===================================================================
--- Products.QueueCatalog/trunk/help/QueueCatalog-Configure.stx	2006-05-09 11:30:28 UTC (rev 68066)
+++ Products.QueueCatalog/trunk/help/QueueCatalog-Configure.stx	2006-05-09 15:31:37 UTC (rev 68067)
@@ -10,7 +10,33 @@
 
     'Title' -- An optional Title
 
-    'Catalog Instance' -- This is where the *real* catalog for
-      sending cataloging requests to can be selected.
+    'Catalog location' -- Enter the path to the real ZCatalog where
+      cataloging requests will be sent
 
-    'Change' -- Commit the changes
+    'Process removal events immediately' -- If this item is selected,
+      requests to uncatalog an item will be processed immediately as
+      opposed to being processed along with the normal event queue.
+
+    'Update metadata immediately' -- If this item is selected, metadata
+      columns will be updated immediately when a cataloging/uncataloging
+      request comes in as opposed to being updated when the queue is
+      processed.
+
+    'Process all indexes during queue' -- Normally only those indices
+      that are not flagged to be updated immediately are updated when the
+      queue is being processed. Enabling this flag will update all 
+      indices during queue processing.
+
+    'Bucket count' -- The number of "buckets" in which to collect the
+      various queued events. If you are unsure what to put here, leave it
+      at the default value (1009).
+
+    'Conflict handling policy' -- Select a conflict resolution policy for
+      when a bucket is updated by more than one thread at the time. The
+      "Safe Policy" represents the original default behavior and will be 
+      fine for most situations. If ZODB conflict errors become a problem,
+      the "Conflict-averse" policy uses an algorithm that avoids conflicts
+      at the expense of possibly updating the catalog more often than 
+      needed during queue processing.
+
+    'Save Changes' -- Commit the changes

Modified: Products.QueueCatalog/trunk/tests/test_CatalogEventQueue.py
===================================================================
--- Products.QueueCatalog/trunk/tests/test_CatalogEventQueue.py	2006-05-09 11:30:28 UTC (rev 68066)
+++ Products.QueueCatalog/trunk/tests/test_CatalogEventQueue.py	2006-05-09 15:31:37 UTC (rev 68067)
@@ -32,6 +32,8 @@
 from Products.QueueCatalog.CatalogEventQueue import CHANGED
 from Products.QueueCatalog.CatalogEventQueue import CHANGED_ADDED
 from Products.QueueCatalog.CatalogEventQueue import REMOVED
+from Products.QueueCatalog.CatalogEventQueue import SAFE_POLICY
+from Products.QueueCatalog.CatalogEventQueue import ALTERNATIVE_POLICY
 from Products.QueueCatalog.QueueCatalog import QueueCatalog
 from OFS.Application import Application
 from OFS.Folder import Folder
@@ -41,6 +43,16 @@
 
 class QueueConflictTests(unittest.TestCase):
 
+    def _setAlternativePolicy(self):
+        # Apply the alternative conflict resolution policy
+        self.queue._conflict_policy = ALTERNATIVE_POLICY
+        self.queue._p_jar.transaction_manager.commit()
+        self.queue2._p_jar.sync()
+
+        self.assertEquals(self.queue._conflict_policy, ALTERNATIVE_POLICY)
+        self.assertEquals(self.queue2._conflict_policy, ALTERNATIVE_POLICY)
+ 
+
     def _insane_update(self, queue, uid, etype):
         # Queue update method that allows insane state changes, needed
         # to provoke pathological queue states
@@ -71,16 +83,16 @@
         queue = CatalogEventQueue()
 
         tm1 = transaction.TransactionManager()
-        conn1 = self.db.open(transaction_manager=tm1)
-        r1 = conn1.root()
+        self.conn1 = self.db.open(transaction_manager=tm1)
+        r1 = self.conn1.root()
         r1["queue"] = queue
         del queue
         self.queue = r1["queue"]
         tm1.commit()
 
         tm2 = transaction.TransactionManager()
-        conn2 = self.db.open(transaction_manager=tm2)
-        r2 = conn2.root()
+        self.conn2 = self.db.open(transaction_manager=tm2)
+        r2 = self.conn2.root()
         self.queue2 = r2["queue"]
         ignored = dir(self.queue2)    # unghostify
 
@@ -129,27 +141,106 @@
     def test_unresolved_add_after_something(self):
         # If an  event is encountered for an object and we are trying to
         # commit an ADDED event, a conflict is encountered
-        self._insane_update(self.queue, '/f0', CHANGED)
+
+        # Mutilate the logger so we don't see complaints about the 
+        # conflict we are about to provoke
+        from Products.QueueCatalog.QueueCatalog import logger
+        logger.disabled = 1
+
+        self.queue.update('/f0', ADDED)
+        self.queue.update('/f0', CHANGED)
         self.queue._p_jar.transaction_manager.commit()
 
-        self._insane_update(self.queue2, '/f0', CHANGED)
+        self.queue2.update('/f0', ADDED)
+        self.queue2.update('/f0', CHANGED)
         self.queue2._p_jar.transaction_manager.commit()
 
         self._insane_update(self.queue, '/f0', CHANGED)
         self.queue._p_jar.transaction_manager.commit()
 
-        # This commit should now raise a conflict
         self._insane_update(self.queue2, '/f0', ADDED)
         self.assertRaises( ConflictError
                          , self.queue2._p_jar.transaction_manager.commit
                          )
 
+        # cleanup the logger
+        logger.disabled = 0
+
+    def test_resolved_add_after_nonremoval(self):
+        # If an  event is encountered for an object and we are trying to
+        # commit an ADDED event while the conflict resolution policy is
+        # NOT the SAFE_POLICY, we won't get a conflict.
+        self._setAlternativePolicy()
+        
+        self.queue.update('/f0', ADDED)
+        self.queue.update('/f0', CHANGED)
+        self.queue._p_jar.transaction_manager.commit()
+
+        self.queue2.update('/f0', ADDED)
+        self.queue2.update('/f0', CHANGED)
+        self.queue2._p_jar.transaction_manager.commit()
+
+        self._insane_update(self.queue, '/f0', CHANGED)
+        self.queue._p_jar.transaction_manager.commit()
+
+        # If we had a conflict, this would blow up
+        self._insane_update(self.queue2, '/f0', ADDED)
+        self.queue2._p_jar.transaction_manager.commit()
+
+        # After the conflict has been resolved, we expect the queues to
+        # containa a CHANGED_ADDED event.
+        self.queue._p_jar.sync()
+        self.queue2._p_jar.sync()
+        self.assertEquals(len(self.queue), 1)
+        self.assertEquals(len(self.queue2), 1)
+        event1 = self.queue.getEvent('/f0')
+        event2 = self.queue2.getEvent('/f0')
+        self.failUnless(event1 == event2 == CHANGED_ADDED)
+
+    def test_resolved_add_after_removal(self):
+        # If a REMOVED event is encountered for an object and we are trying to
+        # commit an ADDED event while the conflict resolution policy is
+        # NOT the SAFE_POLICY, we won't get a conflict.
+        self._setAlternativePolicy()
+        
+        self.queue.update('/f0', ADDED)
+        self.queue.update('/f0', CHANGED)
+        self.queue._p_jar.transaction_manager.commit()
+
+        self.queue2.update('/f0', ADDED)
+        self.queue2.update('/f0', CHANGED)
+        self.queue2._p_jar.transaction_manager.commit()
+
+        self.queue.update('/f0', REMOVED)
+        self.queue._p_jar.transaction_manager.commit()
+
+        # If we had a conflict, this would blow up
+        self._insane_update(self.queue2, '/f0', ADDED)
+        self.queue2._p_jar.transaction_manager.commit()
+
+        # After the conflict has been resolved, we expect the queue to
+        # contain a REMOVED event.
+        self.queue._p_jar.sync()
+        self.queue2._p_jar.sync()
+        self.assertEquals(len(self.queue), 1)
+        self.assertEquals(len(self.queue2), 1)
+        event1 = self.queue.getEvent('/f0')
+        event2 = self.queue2.getEvent('/f0')
+        self.failUnless(event1 == event2 == REMOVED)
+
     def test_unresolved_new_old_current_all_different(self):
         # If the events we get from the current, new and old states are
         # all different, we throw in the towel in the form of a conflict.
         # This test relies on the fact that no OLD state is de-facto treated
         # as a state.
-        self._insane_update(self.queue, '/f0', CHANGED)
+
+        # Mutilate the logger so we don't see complaints about the 
+        # conflict we are about to provoke
+        from Products.QueueCatalog.QueueCatalog import logger
+        logger.disabled = 1
+
+        self.queue.update('/f0', ADDED)
+        self.queue.update('/f0', CHANGED)
         self.queue._p_jar.transaction_manager.commit()
 
         # This commit should now raise a conflict
@@ -158,6 +249,101 @@
                          , self.queue2._p_jar.transaction_manager.commit
                          )
 
+        # cleanup the logger
+        logger.disabled = 0
+
+    def test_resolved_new_old_current_all_different(self):
+        # If the events we get from the current, new and old states are
+        # all different and the SAFE_POLICY conflict resolution policy is 
+        # not enforced, the conflict resolves without bloodshed.
+        # This test relies on the fact that no OLD state is de-facto treated
+        # as a state.
+        self._setAlternativePolicy()
+ 
+        self.queue.update('/f0', ADDED)
+        self.queue.update('/f0', CHANGED)
+        self.queue._p_jar.transaction_manager.commit()
+
+        # This commit should not raise a conflict
+        self._insane_update(self.queue2, '/f0', REMOVED)
+        self.queue2._p_jar.transaction_manager.commit()
+
+        # In this scenario (the incoming new state has a REMOVED event), 
+        # the new state is disregarded and the old state is used. We are 
+        # left with a CHANGED_ADDED event. (see queue.update method; ADDED
+        # plus CHANGED results in CHANGED_ADDED)
+        self.queue._p_jar.sync()
+        self.queue2._p_jar.sync()
+        self.assertEquals(len(self.queue), 1)
+        self.assertEquals(len(self.queue2), 1)
+        event1 = self.queue.getEvent('/f0')
+        event2 = self.queue2.getEvent('/f0')
+        self.failUnless(event1 == event2 == CHANGED_ADDED)
+
+    def test_unresolved_new_old_current_all_different_2(self):
+        # If the events we get from the current, new and old states are
+        # all different, we throw in the towel in the form of a conflict.
+        # This test relies on the fact that no OLD state is de-facto treated
+        # as a state.
+
+        # Mutilate the logger so we don't see complaints about the 
+        # conflict we are about to provoke
+        from Products.QueueCatalog.QueueCatalog import logger
+        logger.disabled = 1
+
+        self.queue.update('/f0', ADDED)
+        self.queue.update('/f0', CHANGED)
+        self.queue._p_jar.transaction_manager.commit()
+
+        self.queue2.update('/f0', ADDED)
+        self.queue2.update('/f0', CHANGED)
+        self.queue2._p_jar.transaction_manager.commit()
+
+        self.queue.update('/f0', CHANGED)
+        self.queue._p_jar.transaction_manager.commit()
+
+        # This commit should now raise a conflict
+        self._insane_update(self.queue2, '/f0', REMOVED)
+        self.assertRaises( ConflictError
+                         , self.queue2._p_jar.transaction_manager.commit
+                         )
+
+        # cleanup the logger
+        logger.disabled = 0
+
+    def test_resolved_new_old_current_all_different_2(self):
+        # If the events we get from the current, new and old states are
+        # all different and the SAFE_POLICY conflict resolution policy is 
+        # not enforced, the conflict resolves without bloodshed.
+        self._setAlternativePolicy()
+ 
+        self.queue.update('/f0', ADDED)
+        self.queue.update('/f0', CHANGED)
+        self.queue._p_jar.transaction_manager.commit()
+
+        self.queue2.update('/f0', ADDED)
+        self.queue2.update('/f0', CHANGED)
+        self.queue2._p_jar.transaction_manager.commit()
+
+        self.queue.update('/f0', CHANGED)
+        self.queue._p_jar.transaction_manager.commit()
+
+        # This commit should not raise a conflict
+        self._insane_update(self.queue2, '/f0', REMOVED)
+        self.queue2._p_jar.transaction_manager.commit()
+
+        # In this scenario (the incoming new state has a REMOVED event), 
+        # we will take the new state to resolve the conflict, because its
+        # generation number is higher then the oldstate and current state.
+        self.queue._p_jar.sync()
+        self.queue2._p_jar.sync()
+        self.assertEquals(len(self.queue), 1)
+        self.assertEquals(len(self.queue2), 1)
+        event1 = self.queue.getEvent('/f0')
+        event2 = self.queue2.getEvent('/f0')
+        self.failUnless(event1 == event2 == REMOVED)
+
+
 def test_suite():
     return unittest.TestSuite((
             unittest.makeSuite(QueueConflictTests),

Modified: Products.QueueCatalog/trunk/tests/test_QueueCatalog.py
===================================================================
--- Products.QueueCatalog/trunk/tests/test_QueueCatalog.py	2006-05-09 11:30:28 UTC (rev 68066)
+++ Products.QueueCatalog/trunk/tests/test_QueueCatalog.py	2006-05-09 15:31:37 UTC (rev 68067)
@@ -16,7 +16,9 @@
 $Id$
 """
 
+import logging
 import unittest
+import cStringIO
 
 import Testing
 import Zope2
@@ -159,33 +161,44 @@
         res = app.queue_cat.searchResults(id='f1')[0]
         self.assertEqual(res.title, 'Betty')
 
-    #def testLogCatalogErrors(self):
-    #    app = self.app
-    #    app.f1 = Folder()
-    #    app.f1.id = 'f1'
-    #    app.queue_cat.catalog_object(app.f1)
-    #    app.real_cat.catalog_object = lambda : None # raises TypeError
-    #    app.queue_cat.process()
-    #    del app.real_cat.catalog_object
-    #    app.queue_cat.setImmediateRemoval(False)
-    #    app.queue_cat.uncatalog_object(app.queue_cat.uidForObject(app.f1))
-    #    app.real_cat.uncatalog_object = lambda : None # raises TypeError
-    #    app.queue_cat.process()
-    #    del app.real_cat.uncatalog_object
-    #    f = self.getLogFile()
-    #    self.verifyEntry(f, subsys="QueueCatalog",
-    #                     summary="error cataloging object")
-    #    # the verify method in the log tests is broken :-(
-    #    l = f.readline()
-    #    marker = "------\n"
-    #    while l != marker:
-    #        l = f.readline()
-    #        if not l:
-    #            self.fail('could not find next log entry')
-    #    f.seek(f.tell() - len(marker))
-    #    self.verifyEntry(f, subsys="QueueCatalog",
-    #                     summary="error uncataloging object")
+    def testLogCatalogErrors(self):
+        # Mutilate the logger so we can capture output silently
+        from Products.QueueCatalog.QueueCatalog import logger
+        logger.propagate = 0
+        fake_file = cStringIO.StringIO()
+        fake_log_handler = logging.StreamHandler(fake_file)
+        logger.addHandler(fake_log_handler)
 
+        # Now do our bidding
+        app = self.app
+        app.f1 = Folder()
+        app.f1.id = 'f1'
+        app.queue_cat.catalog_object(app.f1)
+        app.real_cat.catalog_object = lambda : None # raises TypeError
+        app.queue_cat.process()
+        del app.real_cat.catalog_object
+
+        # See what the fake file contains, and then rewind for reuse
+        output = fake_file.getvalue()
+        self.failUnless(output.startswith('error cataloging object'))
+        fake_file.seek(0)
+        
+        app.queue_cat.setImmediateRemoval(False)
+        app.queue_cat.uncatalog_object(app.queue_cat.uidForObject(app.f1))
+        app.real_cat.uncatalog_object = lambda : None # raises TypeError
+        app.queue_cat.process()
+        del app.real_cat.uncatalog_object
+
+        # See what the fake file contains, and then rewind for reuse
+        output = fake_file.getvalue()
+        self.failUnless(output.startswith('error uncataloging object'))
+        fake_file.close()
+
+        # cleanup the logger
+        fake_log_handler.close()
+        logger.removeHandler(fake_log_handler)
+        logger.propagate = 1
+
     def testQueueProcessingLimit(self):
         # Don't try to process too many items at once.
         app = self.app

Modified: Products.QueueCatalog/trunk/www/edit.zpt
===================================================================
--- Products.QueueCatalog/trunk/www/edit.zpt	2006-05-09 11:30:28 UTC (rev 68066)
+++ Products.QueueCatalog/trunk/www/edit.zpt	2006-05-09 15:31:37 UTC (rev 68067)
@@ -88,6 +88,25 @@
     </tr>
 
     <tr>
+      <td align="left" valign="top" class="form-label">
+        Conflict handling policy
+      </td>
+      <td align="left" valign="top" colspan="3">
+        <select name="conflict_policy:int"
+                tal:define="policy here/getConflictPolicy">
+          <option value="0"
+                  tal:attributes="selected python: policy == 0">
+            Safe policy (old default policy)
+          </option>
+          <option value="1"
+                  tal:attributes="selected python: policy == 1">
+            Conflict-averse policy
+          </option>
+        </select>
+      </td>
+    </tr>
+
+    <tr>
       <td>&nbsp;</td>
       <td colspan="3">
         <br>



More information about the Checkins mailing list