[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