Commit f4fec173 authored by Sebastien Robin's avatar Sebastien Robin

Due to the sorting of database connectors during a transaction, the

ZMySQLDA connector for the activities database would finish its
commit procedure before ZODB, making the description of an activity
message in MySQL available before its respective data in the ZODB.

The fix consisted in replacing the ZMySQLDA connector with another
one based on ZMySQLDA but with a “sortKey()” method that forced it
to be sorted after both the ZODB connection and the ZMySQLDA
connection for ZSQLCatalog.

Analysis of issue was done by Sebastien and Julien.

This patch itself was done by Leonardo.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@41598 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 7987b2b9
##############################################################################
#
# Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved.
# Leonardo Rochael Almeida <leonardo@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from Products.ZMySQLDA.DA import Connection
from Products.ERP5Type.Globals import InitializeClass
from App.special_dtml import HTMLFile
# If the sort order below doesn't work, we cannot guarantee the setSortKey()
# call below will actually result in the activity connection being committed
# after the ZODB and Catalog data.
assert None < 0 < '' < (), "Cannot guarantee commit of activities comes after the appropriate data"
manage_addActivityConnectionForm = HTMLFile('dtml/connectionAdd', globals())
def manage_addActivityConnection(self, id, title,
connection_string,
check=None, REQUEST=None):
"""Add a DB connection to a folder"""
self._setObject(id, Connection(id, title, connection_string, check))
if REQUEST is not None: return self.manage_main(self,REQUEST)
class ActivityConnection(Connection):
"""Products ZMySQLDA.DA.Connection subclass that tweaks the sortKey() of
the actual connection to commit after all other connections
"""
meta_type = title = 'CMFActivity Database Connection'
# Declarative constructors
constructors = (manage_addActivityConnectionForm,
manage_addActivityConnection)
# reuse the permission from ZMySQLDA
permission_type = 'Add Z MySQL Database Connections'
def connect(self, s):
result = Connection.connect(self, s)
# the call above will set self._v_database_connection, and it won't
# have disappeared by now.
self._v_database_connection.setSortKey( (0,) )
return result
InitializeClass(ActivityConnection)
...@@ -37,6 +37,7 @@ from Products.CMFCore import permissions as CMFCorePermissions ...@@ -37,6 +37,7 @@ from Products.CMFCore import permissions as CMFCorePermissions
from Products.ERP5Type.Core.Folder import Folder from Products.ERP5Type.Core.Folder import Folder
from Products.CMFActivity.ActiveResult import ActiveResult from Products.CMFActivity.ActiveResult import ActiveResult
from Products.CMFActivity.ActiveObject import DEFAULT_ACTIVITY from Products.CMFActivity.ActiveObject import DEFAULT_ACTIVITY
from Products.CMFActivity.ActivityConnection import ActivityConnection
from Products.PythonScripts.Utility import allow_class from Products.PythonScripts.Utility import allow_class
from AccessControl import ClassSecurityInfo, Permissions from AccessControl import ClassSecurityInfo, Permissions
from AccessControl.SecurityManagement import newSecurityManager from AccessControl.SecurityManagement import newSecurityManager
...@@ -45,8 +46,7 @@ from AccessControl.SecurityManagement import setSecurityManager ...@@ -45,8 +46,7 @@ from AccessControl.SecurityManagement import setSecurityManager
from AccessControl.SecurityManagement import getSecurityManager from AccessControl.SecurityManagement import getSecurityManager
from Products.CMFCore.utils import UniqueObject, _getAuthenticatedUser, getToolByName from Products.CMFCore.utils import UniqueObject, _getAuthenticatedUser, getToolByName
from Products.ERP5Type.Globals import InitializeClass, DTMLFile from Products.ERP5Type.Globals import InitializeClass, DTMLFile
from Acquisition import aq_base from Acquisition import aq_base, aq_inner, aq_parent
from Acquisition import aq_inner
from ActivityBuffer import ActivityBuffer from ActivityBuffer import ActivityBuffer
from ActivityRuntimeEnvironment import BaseMessage from ActivityRuntimeEnvironment import BaseMessage
from zExceptions import ExceptionFormatter from zExceptions import ExceptionFormatter
...@@ -547,14 +547,29 @@ class ActivityTool (Folder, UniqueObject): ...@@ -547,14 +547,29 @@ class ActivityTool (Folder, UniqueObject):
meta_types.append(meta_type) meta_types.append(meta_type)
return meta_types return meta_types
def maybeMigrateConnectionClass(self):
connection_id = 'cmf_activity_sql_connection'
sql_connection = getattr(self, connection_id, None)
if (sql_connection is not None and
not isinstance(sql_connection, ActivityConnection)):
# SQL Connection migration is needed
LOG('ActivityTool', WARNING, "Migrating MySQL Connection class")
parent = aq_parent(aq_inner(sql_connection))
parent._delObject(sql_connection.getId())
new_sql_connection = ActivityConnection(connection_id,
sql_connection.title,
sql_connection.connection_string)
parent._setObject(connection_id, new_sql_connection)
def initialize(self): def initialize(self):
global is_initialized global is_initialized
from Activity import RAMQueue, RAMDict, SQLQueue, SQLDict from Activity import RAMQueue, RAMDict, SQLQueue, SQLDict
# Initialize each queue # Initialize each queue
for activity in activity_dict.itervalues(): for activity in activity_dict.itervalues():
activity.initialize(self) activity.initialize(self)
self.maybeMigrateConnectionClass()
is_initialized = True is_initialized = True
security.declareProtected(Permissions.manage_properties, 'isSubscribed') security.declareProtected(Permissions.manage_properties, 'isSubscribed')
def isSubscribed(self): def isSubscribed(self):
""" """
......
...@@ -40,8 +40,10 @@ document_classes = updateGlobals(this_module, globals(), ...@@ -40,8 +40,10 @@ document_classes = updateGlobals(this_module, globals(),
# Finish installation # Finish installation
def initialize( context ): def initialize( context ):
# Define object classes and tools # Define object classes and tools
import ActivityTool, ActiveProcess import ActivityTool, ActiveProcess, ActivityConnection
object_classes = (ActiveProcess.ActiveProcess, ) object_classes = (ActiveProcess.ActiveProcess,
#ActivityConnection.ActivityConnection
)
portal_tools = (ActivityTool.ActivityTool, ) portal_tools = (ActivityTool.ActivityTool, )
content_classes = () content_classes = ()
content_constructors = () content_constructors = ()
...@@ -51,6 +53,15 @@ def initialize( context ): ...@@ -51,6 +53,15 @@ def initialize( context ):
content_constructors=content_constructors, content_constructors=content_constructors,
content_classes=content_classes) content_classes=content_classes)
# register manually instead of using object_classes above so we can reuse
# the ZMySQLDA icon without having to carry the gif around in our own product
context.registerClass(
ActivityConnection.ActivityConnection,
permission='Add Z MySQL Database Connections', # reuse the permission
constructors=(ActivityConnection.manage_addActivityConnectionForm,
ActivityConnection.manage_addActivityConnection),
)
# This is used by a script (external method) that can be run # This is used by a script (external method) that can be run
# to set up CMFActivity in an existing CMF Site instance. # to set up CMFActivity in an existing CMF Site instance.
cmfactivity_globals = globals() cmfactivity_globals = globals()
......
...@@ -39,6 +39,7 @@ from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE,\ ...@@ -39,6 +39,7 @@ from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE,\
VALIDATE_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
from Products.CMFActivity.Errors import ActivityPendingError, ActivityFlushError from Products.CMFActivity.Errors import ActivityPendingError, ActivityFlushError
from erp5.portal_type import Organisation from erp5.portal_type import Organisation
from AccessControl.SecurityManagement import newSecurityManager from AccessControl.SecurityManagement import newSecurityManager
...@@ -3827,6 +3828,51 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor): ...@@ -3827,6 +3828,51 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
finally: finally:
del activity_tool.__class__.doSomething del activity_tool.__class__.doSomething
def test_connection_migration(self):
"""
Make sure the cmf_activity_sql_connection is automatically migrated from
the ZMySQLDA Connection class to ActivityConnection
"""
# replace the activity connector with a standard ZMySQLDA one
portal = self.portal
activity_tool = portal.portal_activities
stdconn = self.portal.cmf_activity_sql_connection
portal._delObject('cmf_activity_sql_connection')
portal.manage_addProduct['ZMySQLDA'].manage_addZMySQLConnection(
stdconn.id,
stdconn.title,
stdconn.connection_string,
)
oldconn = portal.cmf_activity_sql_connection
self.assertEquals(oldconn.meta_type, 'Z MySQL Database Connection')
# de-initialize and check that migration of the connection happens
# automatically
Products.CMFActivity.ActivityTool.is_initialized = False
activity_tool.activate(activity='SQLQueue').getId()
self.tic()
newconn = portal.cmf_activity_sql_connection
self.assertEquals(newconn.meta_type, 'CMFActivity Database Connection')
def test_connection_installable(self):
"""
Test if the cmf_activity_sql_connector can be installed
"""
# delete the activity connection
portal = self.portal
activity_tool = portal.portal_activities
stdconn = self.portal.cmf_activity_sql_connection
portal._delObject('cmf_activity_sql_connection')
# check the installation form can be rendered
portal.manage_addProduct['CMFActivity'].connectionAdd(
portal.REQUEST
)
# check it can be installed
portal.manage_addProduct['CMFActivity'].manage_addActivityConnection(
stdconn.id,
stdconn.title,
stdconn.connection_string
)
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