[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> </td>
<td colspan="3">
<br>
More information about the Checkins
mailing list