[CalendarServer-changes] [7075] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Wed Feb 23 20:35:05 PST 2011
Revision: 7075
http://trac.macosforge.org/projects/calendarserver/changeset/7075
Author: cdaboo at apple.com
Date: 2011-02-23 20:35:04 -0800 (Wed, 23 Feb 2011)
Log Message:
-----------
Fix a race condition when creating a notification collection.
Modified Paths:
--------------
CalendarServer/trunk/twext/enterprise/dal/syntax.py
CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
CalendarServer/trunk/txdav/common/datastore/sql.py
Modified: CalendarServer/trunk/twext/enterprise/dal/syntax.py
===================================================================
--- CalendarServer/trunk/twext/enterprise/dal/syntax.py 2011-02-24 00:32:43 UTC (rev 7074)
+++ CalendarServer/trunk/twext/enterprise/dal/syntax.py 2011-02-24 04:35:04 UTC (rev 7075)
@@ -780,8 +780,46 @@
self.table.subSQL(placeholder, quote, [self.table])).append(
SQLFragment(' in %s mode' % (self.mode,)))
+class Savepoint(_Statement):
+ """
+ An SQL 'savepoint' statement.
+ """
+ def __init__(self, name):
+ self.name = name
+
+ def toSQL(self, placeholder="?", quote=lambda x: x):
+ return SQLFragment('savepoint %s' % (self.name,))
+
+
+class RollbackToSavepoint(_Statement):
+ """
+ An SQL 'rollback to savepoint' statement.
+ """
+
+ def __init__(self, name):
+ self.name = name
+
+
+ def toSQL(self, placeholder="?", quote=lambda x: x):
+ return SQLFragment('rollback to savepoint %s' % (self.name,))
+
+
+class ReleaseSavepoint(_Statement):
+ """
+ An SQL 'release savepoint' statement.
+ """
+
+ def __init__(self, name):
+ self.name = name
+
+
+ def toSQL(self, placeholder="?", quote=lambda x: x):
+ return SQLFragment('release savepoint %s' % (self.name,))
+
+
+
class SQLFragment(object):
"""
Combination of SQL text and arguments; a statement which may be executed
Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py 2011-02-24 00:32:43 UTC (rev 7074)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py 2011-02-24 04:35:04 UTC (rev 7075)
@@ -393,4 +393,45 @@
obj._modified = "2011-02-08 11:22:47"
self.assertEqual(obj.created(), datetimeMktime(datetime.datetime(2011, 2, 7, 11, 22, 47)))
self.assertEqual(obj.modified(), datetimeMktime(datetime.datetime(2011, 2, 8, 11, 22, 47)))
+
+ @inlineCallbacks
+ def test_notificationsProvisioningConcurrency(self):
+ """
+ Test that two concurrent attempts to provision a notifications collection do not
+ cause a race-condition whereby the second commit results in a second
+ C{INSERT} that violates a unique constraint.
+ """
+
+ calendarStore = self._sqlCalendarStore
+
+ txn1 = calendarStore.newTransaction()
+ txn2 = calendarStore.newTransaction()
+
+ notification_uid1_1 = yield txn1.notificationsWithUID(
+ "uid1",
+ )
+
+ @inlineCallbacks
+ def _defer_notification_uid1_2():
+ notification_uid1_2 = yield txn2.notificationsWithUID(
+ "uid1",
+ )
+ yield txn2.commit()
+ returnValue(notification_uid1_2)
+ d1 = _defer_notification_uid1_2()
+
+ @inlineCallbacks
+ def _pause_notification_uid1_1():
+ yield deferLater(reactor, 1.0, lambda : None)
+ yield txn1.commit()
+ d2 = _pause_notification_uid1_1()
+
+ # Now do the concurrent provision attempt
+ yield d2
+ notification_uid1_2 = yield d1
+
+ self.assertNotEqual(notification_uid1_1, None)
+ self.assertNotEqual(notification_uid1_2, None)
+
+
\ No newline at end of file
Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py 2011-02-24 00:32:43 UTC (rev 7074)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py 2011-02-24 04:35:04 UTC (rev 7075)
@@ -58,7 +58,8 @@
from txdav.common.inotifications import INotificationCollection, \
INotificationObject
-from twext.enterprise.dal.syntax import Parameter, Max
+from twext.enterprise.dal.syntax import Parameter, Max, Savepoint,\
+ RollbackToSavepoint, ReleaseSavepoint
from twext.python.clsprop import classproperty
from twext.enterprise.dal.syntax import Select
from twext.enterprise.dal.syntax import Lock
@@ -76,6 +77,7 @@
from twistedcaldav.customxml import NotificationType
from twistedcaldav.dateops import datetimeMktime, parseSQLTimestamp
+import pg
v1_schema = getModule(__name__).filePath.sibling("sql_schema_v1.sql").getContent()
@@ -2362,6 +2364,9 @@
Return=_homeSchema.RESOURCE_ID
)
+ _savePointNotificationsWithUID = Savepoint("notificationsWithUID")
+ _rollbackNotificationsWithUID = RollbackToSavepoint("notificationsWithUID")
+ _releaseNotificationsWithUID = ReleaseSavepoint("notificationsWithUID")
@property
def _home(self):
@@ -2381,10 +2386,28 @@
resourceID = rows[0][0]
created = False
else:
- resourceID = str((
- yield cls._provisionNewNotificationsQuery.on(txn, uid=uid)
- )[0][0])
- created = True
+ # Use savepoint so we can do a partial rollback if there is a race condition
+ # where this row has already been inserted
+ yield cls._savePointNotificationsWithUID.on(txn)
+
+ try:
+ resourceID = str((
+ yield cls._provisionNewNotificationsQuery.on(txn, uid=uid)
+ )[0][0])
+ except Exception: # FIXME: Really want to trap the pg.DatabaseError but in a non-DB specific manner
+ yield cls._rollbackNotificationsWithUID.on(txn)
+
+ # Retry the query - row may exist now, if not re-raise
+ rows = yield cls._resourceIDFromUIDQuery.on(txn, uid=uid)
+ if rows:
+ resourceID = rows[0][0]
+ created = False
+ else:
+ raise
+ else:
+ created = True
+ yield cls._releaseNotificationsWithUID.on(txn)
+
collection = cls(txn, uid, resourceID)
yield collection._loadPropertyStore()
if created:
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110223/15dfe78e/attachment.html>
More information about the calendarserver-changes
mailing list