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:
......
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