"bt5/erp5_workflow/bt/license" did not exist on "c779f19684b0b1a0c90eed8c97b7797adf429c54"
Commit e47f2923 authored by Julien Muchembled's avatar Julien Muchembled

CMFActivity: remove non-executable message state (-3)

When an object is deleted, higher level code used to flush its messages (without
invoking them). However, a concurrent and very long transaction may be about to
activate such an object, without conflict. We already experienced false -3
errors that could prevent other messages to be validated.

Because there is no efficient and reliable way to flush absolutely all messages,
messages on deleted objects are now ignored and deleted without any email
notification. There's only a WARNING in logs. But for performance reasons,
there's still a flush on object deletion.

To simplify code, messages that went to -3 for other reasons, like a
non-existing method, now go to -2. In fact, this was already the case for
grouped messages.

In case that a path is recycled, it may still be possible for a message to be
executed on a wrong object (the new one), instead of being ignored (because the
activated object was deleted). So in such scenario, developer should make sure
not to delete an object that may be activated in a concurrent transaction.
If the original object has an OID at the moment it is activated, an assertion
will make sure the message is not executed on another object.
parent fcce7b97
...@@ -41,10 +41,6 @@ DEFAULT_ACTIVITY = 'SQLDict' ...@@ -41,10 +41,6 @@ DEFAULT_ACTIVITY = 'SQLDict'
# Processing node are used to store processing state or processing node # Processing node are used to store processing state or processing node
DISTRIBUTABLE_STATE = -1 DISTRIBUTABLE_STATE = -1
INVOKE_ERROR_STATE = -2 INVOKE_ERROR_STATE = -2
VALIDATE_ERROR_STATE = -3
STOP_STATE = -4
# Special state which allows to select positive nodes
POSITIVE_NODE_STATE = 'Positive Node State'
_DEFAULT_ACTIVATE_PARAMETER_KEY = 'default_activate_parameter' _DEFAULT_ACTIVATE_PARAMETER_KEY = 'default_activate_parameter'
...@@ -145,12 +141,6 @@ class ActiveObject(ExtensionClass.Base): ...@@ -145,12 +141,6 @@ class ActiveObject(ExtensionClass.Base):
""" """
return self.hasActivity(processing_node = INVOKE_ERROR_STATE) return self.hasActivity(processing_node = INVOKE_ERROR_STATE)
security.declareProtected( permissions.View, 'hasInvalidActivity' )
def hasInvalidActivity(self, **kw):
"""Tells if there is invalied activities for this object.
"""
return self.hasActivity(processing_node = VALIDATE_ERROR_STATE)
def getActiveProcess(self): def getActiveProcess(self):
path = getActivityRuntimeEnvironment()._message.active_process path = getActivityRuntimeEnvironment()._message.active_process
if path: if path:
......
...@@ -33,8 +33,7 @@ from Products.ERP5Type.Base import Base ...@@ -33,8 +33,7 @@ from Products.ERP5Type.Base import Base
from Products.ERP5Type import PropertySheet from Products.ERP5Type import PropertySheet
from Products.ERP5Type.ConflictFree import ConflictFreeLog from Products.ERP5Type.ConflictFree import ConflictFreeLog
from BTrees.Length import Length from BTrees.Length import Length
from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE, \ from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE
VALIDATE_ERROR_STATE
from random import randrange from random import randrange
from .ActiveResult import ActiveResult from .ActiveResult import ActiveResult
...@@ -150,13 +149,6 @@ class ActiveProcess(Base): ...@@ -150,13 +149,6 @@ class ActiveProcess(Base):
""" """
return self.hasActivity(processing_node = INVOKE_ERROR_STATE) return self.hasActivity(processing_node = INVOKE_ERROR_STATE)
security.declareProtected( CMFCorePermissions.View, 'hasInvalidActivity' )
def hasInvalidActivity(self, **kw):
"""
Tells if an object if active
"""
return self.hasActivity(processing_node = VALIDATE_ERROR_STATE)
def getCreationDate(self): def getCreationDate(self):
""" """
Define a Creation Date for an active process Define a Creation Date for an active process
......
...@@ -34,8 +34,7 @@ from zLOG import LOG, TRACE, INFO, WARNING, ERROR, PANIC ...@@ -34,8 +34,7 @@ from zLOG import LOG, TRACE, INFO, WARNING, ERROR, PANIC
from ZODB.POSException import ConflictError from ZODB.POSException import ConflictError
from Products.CMFActivity.ActivityTool import ( from Products.CMFActivity.ActivityTool import (
MESSAGE_NOT_EXECUTED, MESSAGE_EXECUTED) MESSAGE_NOT_EXECUTED, MESSAGE_EXECUTED)
from Products.CMFActivity.ActiveObject import ( from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE
INVOKE_ERROR_STATE, VALIDATE_ERROR_STATE)
from Products.CMFActivity.ActivityRuntimeEnvironment import ( from Products.CMFActivity.ActivityRuntimeEnvironment import (
ActivityRuntimeEnvironment, getTransactionalVariable) ActivityRuntimeEnvironment, getTransactionalVariable)
from Queue import Queue, VALIDATION_ERROR_DELAY, VALID, INVALID_PATH from Queue import Queue, VALIDATION_ERROR_DELAY, VALID, INVALID_PATH
...@@ -278,7 +277,7 @@ class SQLBase(Queue): ...@@ -278,7 +277,7 @@ class SQLBase(Queue):
# Count the number of objects to prevent too many objects. # Count the number of objects to prevent too many objects.
cost = m.activity_kw.get('group_method_cost', .01) cost = m.activity_kw.get('group_method_cost', .01)
assert 0 < cost <= 1, (self.sql_table, uid) assert 0 < cost <= 1, (self.sql_table, uid)
count = len(m.getObjectList(activity_tool)) count = m.getObjectCount(activity_tool)
# this is heuristic (messages with same group_method_id # this is heuristic (messages with same group_method_id
# are likely to have the same group_method_cost) # are likely to have the same group_method_cost)
limit = int(1. / cost + 1 - count) limit = int(1. / cost + 1 - count)
...@@ -294,7 +293,7 @@ class SQLBase(Queue): ...@@ -294,7 +293,7 @@ class SQLBase(Queue):
uid_to_duplicate_uid_list_dict[uid] += uid_list uid_to_duplicate_uid_list_dict[uid] += uid_list
continue continue
uid_to_duplicate_uid_list_dict[uid] = uid_list uid_to_duplicate_uid_list_dict[uid] = uid_list
cost += len(m.getObjectList(activity_tool)) * \ cost += m.getObjectCount(activity_tool) * \
m.activity_kw.get('group_method_cost', .01) m.activity_kw.get('group_method_cost', .01)
message_list.append(m) message_list.append(m)
if cost >= 1: if cost >= 1:
...@@ -415,7 +414,6 @@ class SQLBase(Queue): ...@@ -415,7 +414,6 @@ class SQLBase(Queue):
final_error_uid_list = [] final_error_uid_list = []
make_available_uid_list = [] make_available_uid_list = []
notify_user_list = [] notify_user_list = []
non_executable_message_list = []
executed_uid_list = deletable_uid_list executed_uid_list = deletable_uid_list
if uid_to_duplicate_uid_list_dict is not None: if uid_to_duplicate_uid_list_dict is not None:
for m in message_list: for m in message_list:
...@@ -461,12 +459,14 @@ class SQLBase(Queue): ...@@ -461,12 +459,14 @@ class SQLBase(Queue):
except: except:
self._log(WARNING, 'Failed to reactivate %r' % uid) self._log(WARNING, 'Failed to reactivate %r' % uid)
make_available_uid_list.append(uid) make_available_uid_list.append(uid)
else: else: # MESSAGE_NOT_EXECUTABLE
# Internal CMFActivity error: the message can not be executed because # 'path' does not point to any object. Activities are normally flushed
# something is missing (context object cannot be found, method cannot # (without invoking them) when an object is deleted, but this is only
# be accessed on object). # an optimisation. There is no efficient and reliable way to do such
non_executable_message_list.append(uid) # this, because a concurrent and very long transaction may be about to
notify_user_list.append((m, False)) # activate this object, without conflict.
# So we have to clean up any remaining activity.
deletable_uid_list.append(uid)
if deletable_uid_list: if deletable_uid_list:
try: try:
self._retryOnLockError(activity_tool.SQLBase_delMessage, self._retryOnLockError(activity_tool.SQLBase_delMessage,
...@@ -490,13 +490,6 @@ class SQLBase(Queue): ...@@ -490,13 +490,6 @@ class SQLBase(Queue):
except: except:
self._log(ERROR, 'Failed to set message to error state for %r' self._log(ERROR, 'Failed to set message to error state for %r'
% final_error_uid_list) % final_error_uid_list)
if non_executable_message_list:
try:
activity_tool.SQLBase_assignMessage(table=self.sql_table,
uid=non_executable_message_list, processing_node=VALIDATE_ERROR_STATE)
except:
self._log(ERROR, 'Failed to set message to invalid path state for %r'
% non_executable_message_list)
if make_available_uid_list: if make_available_uid_list:
try: try:
self.makeMessageListAvailable(activity_tool=activity_tool, self.makeMessageListAvailable(activity_tool=activity_tool,
......
...@@ -169,6 +169,7 @@ class Message(BaseMessage): ...@@ -169,6 +169,7 @@ class Message(BaseMessage):
is_executed = MESSAGE_NOT_EXECUTED is_executed = MESSAGE_NOT_EXECUTED
processing = None processing = None
traceback = None traceback = None
oid = None
def __init__(self, obj, active_process, activity_kw, method_id, args, kw): def __init__(self, obj, active_process, activity_kw, method_id, args, kw):
if isinstance(obj, str): if isinstance(obj, str):
...@@ -177,6 +178,12 @@ class Message(BaseMessage): ...@@ -177,6 +178,12 @@ class Message(BaseMessage):
else: else:
self.object_path = obj.getPhysicalPath() self.object_path = obj.getPhysicalPath()
activity_creation_trace = obj.getPortalObject().portal_activities.activity_creation_trace activity_creation_trace = obj.getPortalObject().portal_activities.activity_creation_trace
try:
self.oid = aq_base(obj)._p_oid
# Note that it's too early to get the OID of a newly created object,
# so at this point, self.oid may still be None.
except AttributeError:
pass
if active_process is not None: if active_process is not None:
self.active_process = active_process.getPhysicalPath() self.active_process = active_process.getPhysicalPath()
self.active_process_uid = active_process.getUid() self.active_process_uid = active_process.getUid()
...@@ -216,29 +223,38 @@ class Message(BaseMessage): ...@@ -216,29 +223,38 @@ class Message(BaseMessage):
def getObject(self, activity_tool): def getObject(self, activity_tool):
"""return the object referenced in this message.""" """return the object referenced in this message."""
return activity_tool.unrestrictedTraverse(self.object_path)
def getObjectList(self, activity_tool):
"""return the list of object that can be expanded from this message."""
object_list = []
try: try:
object_list.append(self.getObject(activity_tool)) obj = activity_tool.unrestrictedTraverse(self.object_path)
except KeyError: except KeyError:
pass LOG('CMFActivity', WARNING, "Message dropped (no object found at path %r)"
% (self.object_path,), error=sys.exc_info())
self.setExecutionState(MESSAGE_NOT_EXECUTABLE)
else: else:
if self.hasExpandMethod(): if self.oid and self.oid != getattr(aq_base(obj), '_p_oid', None):
expand_method_id = self.activity_kw['expand_method_id'] raise ValueError("OID mismatch for %r" % obj)
# FIXME: how to pass parameters? return obj
object_list = getattr(object_list[0], expand_method_id)()
return object_list def getObjectList(self, activity_tool):
"""return the list of object that can be expanded from this message
def hasExpandMethod(self):
"""return true if the message has an expand method.
An expand method is used to expand the list of objects and to turn a An expand method is used to expand the list of objects and to turn a
big recursive transaction affecting many objects into multiple big recursive transaction affecting many objects into multiple
transactions affecting only one object at a time (this can prevent transactions affecting only one object at a time (this can prevent
duplicated method calls).""" duplicated method calls)."""
return self.activity_kw.has_key('expand_method_id') obj = self.getObject(activity_tool)
if obj is None:
return ()
if 'expand_method_id' in self.activity_kw:
return getattr(obj, self.activity_kw['expand_method_id'])()
return obj,
def getObjectCount(self, activity_tool):
if 'expand_method_id' in self.activity_kw:
try:
obj = activity_tool.unrestrictedTraverse(self.object_path)
return len(getattr(obj, self.activity_kw['expand_method_id'])())
except StandardError:
pass
return 1
def changeUser(self, user_name, activity_tool): def changeUser(self, user_name, activity_tool):
"""restore the security context for the calling user.""" """restore the security context for the calling user."""
...@@ -280,39 +296,21 @@ class Message(BaseMessage): ...@@ -280,39 +296,21 @@ class Message(BaseMessage):
def __call__(self, activity_tool): def __call__(self, activity_tool):
try: try:
obj = self.getObject(activity_tool) obj = self.getObject(activity_tool)
except KeyError: if obj is not None:
exc_info = sys.exc_info()
LOG('CMFActivity', ERROR,
'Message failed in getting an object from the path %r'
% (self.object_path,), error=exc_info)
self.setExecutionState(MESSAGE_NOT_EXECUTABLE, exc_info,
context=activity_tool)
else:
try:
old_security_manager = getSecurityManager() old_security_manager = getSecurityManager()
try: try:
# Change user if required (TO BE DONE) # Change user if required (TO BE DONE)
# We will change the user only in order to execute this method # We will change the user only in order to execute this method
self.changeUser(self.user_name, activity_tool) self.changeUser(self.user_name, activity_tool)
try: # XXX: There is no check to see if user is allowed to access
# XXX: There is no check to see if user is allowed to access # that method !
# that method ! method = getattr(obj, self.method_id)
method = getattr(obj, self.method_id) # Store site info
except Exception: setSite(activity_tool.getParentValue())
exc_info = sys.exc_info() if activity_tool.activity_timing_log:
LOG('CMFActivity', ERROR, result = activity_timing_method(method, self.args, self.kw)
'Message failed in getting a method %r from an object %r'
% (self.method_id, obj), error=exc_info)
method = None
self.setExecutionState(MESSAGE_NOT_EXECUTABLE, exc_info,
context=activity_tool)
else: else:
# Store site info result = method(*self.args, **self.kw)
setSite(activity_tool.getParentValue())
if activity_tool.activity_timing_log:
result = activity_timing_method(method, self.args, self.kw)
else:
result = method(*self.args, **self.kw)
finally: finally:
setSecurityManager(old_security_manager) setSecurityManager(old_security_manager)
...@@ -322,8 +320,8 @@ class Message(BaseMessage): ...@@ -322,8 +320,8 @@ class Message(BaseMessage):
activity_tool.unrestrictedTraverse(self.active_process), activity_tool.unrestrictedTraverse(self.active_process),
result, obj) result, obj)
self.setExecutionState(MESSAGE_EXECUTED) self.setExecutionState(MESSAGE_EXECUTED)
except: except:
self.setExecutionState(MESSAGE_NOT_EXECUTED, context=activity_tool) self.setExecutionState(MESSAGE_NOT_EXECUTED, context=activity_tool)
def validate(self, activity, activity_tool, check_order_validation=1): def validate(self, activity, activity_tool, check_order_validation=1):
return activity.validate(activity_tool, self, return activity.validate(activity_tool, self,
...@@ -338,9 +336,7 @@ class Message(BaseMessage): ...@@ -338,9 +336,7 @@ class Message(BaseMessage):
email_from_name = portal.getProperty('email_from_name', email_from_name = portal.getProperty('email_from_name',
portal.getProperty('email_from_address')) portal.getProperty('email_from_address'))
fail_count = self.line.retry + 1 fail_count = self.line.retry + 1
if self.getExecutionState() == MESSAGE_NOT_EXECUTABLE: if retry:
message = "Not executable activity"
elif retry:
message = "Pending activity already failed %s times" % fail_count message = "Pending activity already failed %s times" % fail_count
else: else:
message = "Activity failed" message = "Activity failed"
...@@ -371,7 +367,7 @@ Named Parameters: %r ...@@ -371,7 +367,7 @@ Named Parameters: %r
def reactivate(self, activity_tool, activity=DEFAULT_ACTIVITY): def reactivate(self, activity_tool, activity=DEFAULT_ACTIVITY):
# Reactivate the original object. # Reactivate the original object.
obj= self.getObject(activity_tool) obj = activity_tool.unrestrictedTraverse(self.object_path)
old_security_manager = getSecurityManager() old_security_manager = getSecurityManager()
try: try:
# Change user if required (TO BE DONE) # Change user if required (TO BE DONE)
...@@ -410,7 +406,7 @@ Named Parameters: %r ...@@ -410,7 +406,7 @@ Named Parameters: %r
""" """
assert is_executed in (MESSAGE_NOT_EXECUTED, MESSAGE_EXECUTED, MESSAGE_NOT_EXECUTABLE) assert is_executed in (MESSAGE_NOT_EXECUTED, MESSAGE_EXECUTED, MESSAGE_NOT_EXECUTABLE)
self.is_executed = is_executed self.is_executed = is_executed
if is_executed != MESSAGE_EXECUTED: if is_executed == MESSAGE_NOT_EXECUTED:
if not exc_info: if not exc_info:
exc_info = sys.exc_info() exc_info = sys.exc_info()
if self.on_error_callback is not None: if self.on_error_callback is not None:
...@@ -1189,21 +1185,11 @@ class ActivityTool (Folder, UniqueObject): ...@@ -1189,21 +1185,11 @@ class ActivityTool (Folder, UniqueObject):
# alternate method is used to segregate objects which cannot be grouped. # alternate method is used to segregate objects which cannot be grouped.
alternate_method_id = m.activity_kw.get('alternate_method_id') alternate_method_id = m.activity_kw.get('alternate_method_id')
try: try:
obj = m.getObject(self) object_list = m.getObjectList(self)
except KeyError: if object_list is None:
exc_info = sys.exc_info() continue
LOG('CMFActivity', ERROR,
'Message failed in getting an object from the path %r'
% (m.object_path,), error=exc_info)
m.setExecutionState(MESSAGE_NOT_EXECUTABLE, exc_info, context=self)
continue
try:
if m.hasExpandMethod():
subobject_list = m.getObjectList(self)
else:
subobject_list = (obj,)
message_dict[m] = expanded_object_list = [] message_dict[m] = expanded_object_list = []
for subobj in subobject_list: for subobj in object_list:
if merge_duplicate: if merge_duplicate:
path = subobj.getPath() path = subobj.getPath()
if path in path_set: if path in path_set:
......
...@@ -26,18 +26,16 @@ ...@@ -26,18 +26,16 @@
# #
############################################################################## ##############################################################################
import inspect
import unittest import unittest
from Products.ERP5Type.tests.utils import LogInterceptor from Products.ERP5Type.tests.utils import LogInterceptor
from Products.ERP5Type.tests.backportUnittest import skip from Products.ERP5Type.tests.backportUnittest import skip
from Testing import ZopeTestCase from Testing import ZopeTestCase
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import DummyMailHost
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
from Products.ERP5Type.Base import Base from Products.ERP5Type.Base import Base
from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE,\ from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE
VALIDATE_ERROR_STATE
from Products.CMFActivity.Activity.Queue import VALIDATION_ERROR_DELAY from Products.CMFActivity.Activity.Queue import VALIDATION_ERROR_DELAY
from Products.CMFActivity.Activity.SQLDict import SQLDict from Products.CMFActivity.Activity.SQLDict import SQLDict
import Products.CMFActivity.ActivityTool import Products.CMFActivity.ActivityTool
...@@ -433,60 +431,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor): ...@@ -433,60 +431,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
self.tic() self.tic()
self.assertEquals(o.getTitle(), 'acb') self.assertEquals(o.getTitle(), 'acb')
def ExpandedMethodWithDeletedSubObject(self, activity):
"""
Do recursiveReindexObject, then delete a
subobject an see if there is only one activity
in the queue
"""
portal = self.getPortal()
organisation_module = self.getOrganisationModule()
if not(organisation_module.hasContent(self.company_id2)):
o2 = organisation_module.newContent(id=self.company_id2)
o1 = portal.organisation._getOb(self.company_id)
o2 = portal.organisation._getOb(self.company_id2)
for o in (o1,o2):
if not(o.hasContent('1')):
o.newContent(portal_type='Email',id='1')
if not(o.hasContent('2')):
o.newContent(portal_type='Email',id='2')
o1.recursiveReindexObject()
o2.recursiveReindexObject()
o1._delOb('2')
self.commit()
portal.portal_activities.distribute()
portal.portal_activities.tic()
self.commit()
message_list = portal.portal_activities.getMessageList()
self.assertEquals(len(message_list),1)
def ExpandedMethodWithDeletedObject(self, activity):
"""
Do recursiveReindexObject, then delete a
subobject an see if there is only one activity
in the queue
"""
portal = self.getPortal()
organisation_module = self.getOrganisationModule()
if not(organisation_module.hasContent(self.company_id2)):
o2 = organisation_module.newContent(id=self.company_id2)
o1 = portal.organisation._getOb(self.company_id)
o2 = portal.organisation._getOb(self.company_id2)
for o in (o1,o2):
if not(o.hasContent('1')):
o.newContent(portal_type='Email',id='1')
if not(o.hasContent('2')):
o.newContent(portal_type='Email',id='2')
o1.recursiveReindexObject()
o2.recursiveReindexObject()
organisation_module._delOb(self.company_id2)
self.commit()
portal.portal_activities.distribute()
portal.portal_activities.tic()
self.commit()
message_list = portal.portal_activities.getMessageList()
self.assertEquals(len(message_list),1)
def TryAfterTag(self, activity): def TryAfterTag(self, activity):
""" """
Ensure the order of an execution by a tag Ensure the order of an execution by a tag
...@@ -1061,24 +1005,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor): ...@@ -1061,24 +1005,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
# Check if what we did was executed as toto # Check if what we did was executed as toto
self.assertEquals(email.getOwnerInfo()['id'],'toto') self.assertEquals(email.getOwnerInfo()['id'],'toto')
def test_57_ExpandedMethodWithDeletedSubObject(self, quiet=0, run=run_all_test):
# Test if after_method_id can be used
if not run: return
if not quiet:
message = '\nTry Expanded Method With Deleted Sub Object'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
self.ExpandedMethodWithDeletedSubObject('SQLDict')
def test_58_ExpandedMethodWithDeletedObject(self, quiet=0, run=run_all_test):
# Test if after_method_id can be used
if not run: return
if not quiet:
message = '\nTry Expanded Method With Deleted Object'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
self.ExpandedMethodWithDeletedObject('SQLDict')
def test_59_TryAfterTagWithSQLDict(self, quiet=0, run=run_all_test): def test_59_TryAfterTagWithSQLDict(self, quiet=0, run=run_all_test):
# Test if after_tag can be used # Test if after_tag can be used
if not run: return if not run: return
...@@ -1162,8 +1088,7 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor): ...@@ -1162,8 +1088,7 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
finished = 1 finished = 1
for message in activity_tool.getMessageList(): for message in activity_tool.getMessageList():
if message.processing_node not in (INVOKE_ERROR_STATE, if message.processing_node != INVOKE_ERROR_STATE:
VALIDATE_ERROR_STATE):
finished = 0 finished = 0
activity_tool.timeShift(3 * VALIDATION_ERROR_DELAY) activity_tool.timeShift(3 * VALIDATION_ERROR_DELAY)
...@@ -1969,38 +1894,33 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor): ...@@ -1969,38 +1894,33 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
delattr(Organisation, 'checkMarkerValue') delattr(Organisation, 'checkMarkerValue')
def TryUserNotificationOnActivityFailure(self, activity): def TryUserNotificationOnActivityFailure(self, activity):
message_list = self.portal.MailHost._message_list
del message_list[:]
obj = self.portal.organisation_module.newContent(portal_type='Organisation')
self.tic() self.tic()
obj = self.getPortal().organisation_module.newContent(portal_type='Organisation') def failingMethod(self): raise ValueError('This method always fails')
self.tic()
# Use a mutable variable to be able to modify the same instance from
# monkeypatch method.
notification_done = []
from Products.CMFActivity.ActivityTool import Message
def fake_notifyUser(self, *args, **kw):
notification_done.append(True)
original_notifyUser = Message.notifyUser
def failingMethod(self):
raise ValueError, 'This method always fail'
Message.notifyUser = fake_notifyUser
Organisation.failingMethod = failingMethod Organisation.failingMethod = failingMethod
try: try:
# MESSAGE_NOT_EXECUTED # MESSAGE_NOT_EXECUTED
obj.activate(activity=activity).failingMethod() obj.activate(activity=activity).failingMethod()
self.commit() self.commit()
self.assertEqual(len(notification_done), 0) self.assertFalse(message_list)
self.flushAllActivities(silent=1, loop_size=100) self.flushAllActivities(silent=1, loop_size=100)
self.assertEqual(len(notification_done), 1) # Check there is a traceback in the email notification
sender, recipients, mail = message_list.pop()
self.assertTrue("Module %s, line %s, in failingMethod" % (
__name__, inspect.getsourcelines(failingMethod)[1]) in mail, mail)
self.assertTrue("ValueError:" in mail, mail)
# MESSAGE_NOT_EXECUTABLE # MESSAGE_NOT_EXECUTABLE
obj.getParentValue()._delObject(obj.getId()) obj.getParentValue()._delObject(obj.getId())
obj.activate(activity=activity).getId() obj.activate(activity=activity).failingMethod()
self.commit() self.commit()
self.assertEqual(len(notification_done), 1) self.assertTrue(obj.hasActivity())
self.flushAllActivities(silent=1, loop_size=100) self.tic()
self.assertEqual(len(notification_done), 2) self.assertFalse(obj.hasActivity())
self.assertFalse(message_list)
finally: finally:
Message.notifyUser = original_notifyUser del Organisation.failingMethod
delattr(Organisation, 'failingMethod')
def test_90_userNotificationOnActivityFailureWithSQLDict(self, quiet=0, run=run_all_test): def test_90_userNotificationOnActivityFailureWithSQLDict(self, quiet=0, run=run_all_test):
""" """
...@@ -2579,89 +2499,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor): ...@@ -2579,89 +2499,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
finally: finally:
delattr(Organisation, 'checkAbsoluteUrl') delattr(Organisation, 'checkAbsoluteUrl')
def CheckMissingActivityContextObject(self, activity):
"""
Check that a message whose context has ben deleted goes to -3
processing_node.
This must happen on first message execution, without any delay.
"""
activity_tool = self.getActivityTool()
container = self.getPortal().organisation_module
organisation = container.newContent(portal_type='Organisation')
self.tic()
organisation.activate(activity=activity).getTitle()
self.commit()
self.assertEqual(len(activity_tool.getMessageList()), 1)
# Here, we delete the subobject using most low-level method, to avoid
# pending activity to be removed.
organisation_id = organisation.id
container._delOb(organisation_id)
del organisation # Avoid keeping a reference to a deleted object.
self.commit()
self.assertEqual(getattr(container, organisation_id, None), None)
self.assertEqual(len(activity_tool.getMessageList()), 1)
activity_tool.distribute()
self.assertEqual([], activity_tool.getMessageList(activity=activity,
processing_node=-3))
activity_tool.tic()
self.assertEqual(1, len(activity_tool.getMessageList(activity=activity,
processing_node=-3)))
def test_109_checkMissingActivityContextObjectSQLDict(self, quiet=0,
run=run_all_test):
if not run: return
if not quiet:
message = '\nCheck missing activity context object (SQLDict)'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
self.CheckMissingActivityContextObject('SQLDict')
def test_110_checkMissingActivityContextObjectSQLQueue(self, quiet=0,
run=run_all_test):
if not run: return
if not quiet:
message = '\nCheck missing activity context object (SQLQueue)'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
self.CheckMissingActivityContextObject('SQLQueue')
def test_111_checkMissingActivityContextObjectSQLDict(self, quiet=0,
run=run_all_test):
"""
This is similar to tst 108, but here the object will be missing for an
activity with a group_method_id.
"""
if not run: return
if not quiet:
message = '\nCheck missing activity context object with ' \
'group_method_id (SQLDict)'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
activity_tool = self.getActivityTool()
container = self.getPortalObject().organisation_module
organisation = container.newContent(portal_type='Organisation')
organisation_2 = container.newContent(portal_type='Organisation')
self.tic()
organisation.reindexObject()
organisation_2.reindexObject()
self.commit()
self.assertEqual(len(activity_tool.getMessageList()), 2)
# Here, we delete the subobject using most low-level method, to avoid
# pending activity to be removed.
organisation_id = organisation.id
container._delOb(organisation_id)
del organisation # Avoid keeping a reference to a deleted object.
self.commit()
self.assertEqual(getattr(container, organisation_id, None), None)
self.assertEqual(len(activity_tool.getMessageList()), 2)
activity_tool.distribute()
self.assertEqual([], activity_tool.getMessageList(activity="SQLDict",
processing_node=-3))
activity_tool.tic()
message, = activity_tool.getMessageList()
# The message excuted on "organisation_2" must have succeeded.
self.assertEqual(message.processing_node, -3)
def CheckLocalizerWorks(self, activity): def CheckLocalizerWorks(self, activity):
FROM_STRING = 'Foo' FROM_STRING = 'Foo'
TO_STRING = 'Bar' TO_STRING = 'Bar'
...@@ -2712,69 +2549,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor): ...@@ -2712,69 +2549,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
LOG('Testing... ',0,message) LOG('Testing... ',0,message)
self.CheckLocalizerWorks('SQLDict') self.CheckLocalizerWorks('SQLDict')
def testMessageContainsFailureTraceback(self, quiet=0, run=run_all_test):
if not run: return
if not quiet:
message = '\nCheck message contains failure traceback'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
portal = self.getPortalObject()
activity_tool = self.getActivityTool()
def checkMessage(message, exception_type):
self.assertNotEqual(message.getExecutionState(), 1) # 1 == MESSAGE_EXECUTED
self.assertEqual(message.exc_type, exception_type)
self.assertNotEqual(message.traceback, None)
# With Message.__call__
# 1: activity context does not exist when activity is executed
organisation = portal.organisation_module.newContent(portal_type='Organisation')
self.tic()
organisation.activate().getTitle() # This generates the mssage we want to test.
self.commit()
message_list = activity_tool.getMessageList()
self.assertEqual(len(message_list), 1)
message = message_list[0]
portal.organisation_module._delOb(organisation.id)
message(activity_tool)
checkMessage(message, KeyError)
activity_tool.manageCancel(message.object_path, message.method_id)
# 2: activity method does not exist when activity is executed
portal.organisation_module.activate().this_method_does_not_exist()
self.commit()
message_list = activity_tool.getMessageList()
self.assertEqual(len(message_list), 1)
message = message_list[0]
message(activity_tool)
checkMessage(message, AttributeError)
activity_tool.manageCancel(message.object_path, message.method_id)
# With ActivityTool.invokeGroup
# 1: activity context does not exist when activity is executed
organisation = portal.organisation_module.newContent(portal_type='Organisation')
self.tic()
organisation.activate().getTitle() # This generates the mssage we want to test.
self.commit()
message_list = activity_tool.getMessageList()
self.assertEqual(len(message_list), 1)
message = message_list[0]
portal.organisation_module._delOb(organisation.id)
activity_tool.invokeGroup('getTitle', [message], 'SQLDict', True)
checkMessage(message, KeyError)
activity_tool.manageCancel(message.object_path, message.method_id)
# 2: activity method does not exist when activity is executed
portal.organisation_module.activate().this_method_does_not_exist()
self.commit()
message_list = activity_tool.getMessageList()
self.assertEqual(len(message_list), 1)
message = message_list[0]
activity_tool.invokeGroup('this_method_does_not_exist',
[message], 'SQLDict', True)
checkMessage(message, KeyError)
activity_tool.manageCancel(message.object_path, message.method_id)
# Unadressed error paths (in both cases):
# 3: activity commit raises
# 4: activity raises
def test_114_checkSQLQueueActivitySucceedsAfterActivityChangingSkin(self, def test_114_checkSQLQueueActivitySucceedsAfterActivityChangingSkin(self,
quiet=0, run=run_all_test): quiet=0, run=run_all_test):
if not run: return if not run: return
...@@ -3620,6 +3394,41 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor): ...@@ -3620,6 +3394,41 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
self.tic() self.tic()
test() test()
def test_MessageNonExecutable(self):
message_list = self.portal.MailHost._message_list
del message_list[:]
activity_tool = self.portal.portal_activities
kw = {}
self._catch_log_errors(subsystem='CMFActivity')
try:
for kw['activity'] in 'SQLDict', 'SQLQueue':
for kw['group_method_id'] in '', None:
obj = activity_tool.newActiveProcess()
self.tic()
obj.activate(**kw).getId()
activity_tool._delOb(obj.getId())
obj = activity_tool.newActiveProcess(id=obj.getId(),
is_indexable=False)
self.commit()
self.assertEqual(1, activity_tool.countMessage())
self.flushAllActivities()
sender, recipients, mail = message_list.pop()
self.assertTrue('OID mismatch' in mail, mail)
m, = activity_tool.getMessageList()
self.assertEqual(m.processing_node, INVOKE_ERROR_STATE)
obj.flushActivity()
obj.activate(**kw).getId()
activity_tool._delOb(obj.getId())
self.commit()
self.assertEqual(1, activity_tool.countMessage())
activity_tool.tic()
self.assertTrue('no object found' in self.logged.pop().getMessage())
finally:
self._ignore_log_errors()
self.assertFalse(self.logged)
self.assertFalse(message_list, message_list)
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestCMFActivity)) suite.addTest(unittest.makeSuite(TestCMFActivity))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment