[CalendarServer-changes] [15539] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Apr 21 13:11:28 PDT 2016


Revision: 15539
          http://trac.calendarserver.org//changeset/15539
Author:   cdaboo at apple.com
Date:     2016-04-21 13:11:28 -0700 (Thu, 21 Apr 2016)
Log Message:
-----------
Make cross-pod requests more robust in the face of connectivity and data sync errors.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py
    CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
    CalendarServer/trunk/txdav/common/datastore/podding/util.py
    CalendarServer/trunk/txdav/common/datastore/sql_external.py
    CalendarServer/trunk/txdav/common/datastore/sql_sharing.py

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2016-04-21 19:48:22 UTC (rev 15538)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2016-04-21 20:11:28 UTC (rev 15539)
@@ -57,7 +57,7 @@
 
 from txdav.caldav.datastore.util import normalizationLookup
 from txdav.common.icommondatastore import InternalDataStoreError, \
-    SyncTokenValidException
+    SyncTokenValidException, ExternalShareFailed
 from txdav.xml import element
 from txdav.xml.element import dav_namespace
 
@@ -357,6 +357,12 @@
             response = yield super(CalDAVResource, self).renderHTTP(request)
         except AlreadyFinishedError:
             self._transactionError = True
+        except ExternalShareFailed:
+            # This happens when an external share is no longer valid and has been fixed
+            # by removing it from this pod. We need to treat this as a 503 "error" but let
+            # the transaction commit
+            self._transactionError = False
+            response = StatusResponse(responsecode.SERVICE_UNAVAILABLE, "Shared collection not valid - removing.")
         if transaction is None:
             transaction = self._associatedTransaction
         if transaction is not None:

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2016-04-21 19:48:22 UTC (rev 15538)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2016-04-21 20:11:28 UTC (rev 15539)
@@ -63,6 +63,7 @@
 from txdav.carddav.iaddressbookstore import (
     KindChangeNotAllowedError, GroupWithUnsharedAddressNotAllowedError
 )
+from txdav.common.datastore.podding.base import FailedCrossPodRequestError
 from txdav.common.datastore.sql_tables import (
     _BIND_MODE_READ, _BIND_MODE_WRITE,
     _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
@@ -204,8 +205,102 @@
 
 
 
-class _CommonHomeChildCollectionMixin(object):
+class _CommonStoreExceptionHandler(object):
     """
+    A mix-in class that is used to help trap store exceptions and turn them into
+    appropriate HTTP errors.
+
+    The class properties define mappings from a store exception type to a L{tuple} whose
+    first item is one of the class methods defined in this mix-in, and whose second argument
+    is the L{arg} passed to the class method. In some cases the second L{tuple} item will not
+    be present, and instead the argument will be directly provided to the class method.
+    """
+
+    # The following are used to map store exceptions into HTTP error responses
+    StoreExceptionsErrors = {}
+    StoreMoveExceptionsErrors = {}
+
+    @classmethod
+    def _storeExceptionStatus(cls, err, arg):
+        """
+        Raise a status error.
+
+        @param err: the actual exception that caused the error
+        @type err: L{Exception}
+        @param arg: description of error or C{None}
+        @type arg: C{str} or C{None}
+        """
+        raise HTTPError(StatusResponse(responsecode.FORBIDDEN, arg if arg is not None else str(err)))
+
+
+    @classmethod
+    def _storeExceptionError(cls, err, arg):
+        """
+        Raise a DAV:error error with the supplied error element.
+
+        @param err: the actual exception that caused the error
+        @type err: L{Exception}
+        @param arg: the error element
+        @type arg: C{tuple}
+        """
+        raise HTTPError(ErrorResponse(
+            responsecode.FORBIDDEN,
+            arg,
+            str(err),
+        ))
+
+
+    @classmethod
+    def _storeExceptionUnavailable(cls, err, arg):
+        """
+        Raise a service unavailable error.
+
+        @param err: the actual exception that caused the error
+        @type err: L{Exception}
+        @param arg: description of error or C{None}
+        @type arg: C{str} or C{None}
+        """
+        raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE, arg if arg is not None else str(err)))
+
+
+    @classmethod
+    def _handleStoreException(cls, ex, exceptionMap):
+        """
+        Process a store exception and see if it is in the supplied mapping. If so, execute the
+        method in the mapping (which will raise an HTTPError).
+
+        @param ex: the store exception that was raised
+        @type ex: L{Exception}
+        @param exceptionMap: the store exception mapping to use
+        @type exceptionMap: L{dict}
+        """
+        if type(ex) in exceptionMap:
+            error, arg = exceptionMap[type(ex)]
+            error(ex, arg)
+
+
+    @classmethod
+    def _handleStoreExceptionArg(cls, ex, exceptionMap, arg):
+        """
+        Process a store exception and see if it is in the supplied mapping. If so, execute the
+        method in the mapping (which will raise an HTTPError). This method is used when the argument
+        to the class method needs to be provided at runtime, rather than statically.
+
+        @param ex: the store exception that was raised
+        @type ex: L{Exception}
+        @param exceptionSet: the store exception set to use
+        @type exceptionSet: L{set}
+        @param arg: the argument to use
+        @type arg: L{object}
+        """
+        if type(ex) in exceptionMap:
+            error = exceptionMap[type(ex)]
+            error(ex, arg)
+
+
+
+class _CommonHomeChildCollectionMixin(_CommonStoreExceptionHandler):
+    """
     Methods for things which are like calendars.
     """
 
@@ -446,7 +541,11 @@
             )
             log.error(msg)
             raise HTTPError(StatusResponse(BAD_REQUEST, msg))
-        response = (yield self.storeRemove(request))
+        try:
+            response = (yield self.storeRemove(request))
+        except Exception as err:
+            self._handleStoreException(err, self.StoreExceptionsErrors)
+            raise
         returnValue(response)
 
 
@@ -1052,6 +1151,12 @@
     Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
     """
 
+    StoreExceptionsErrors = {
+        LockTimeout: (_CommonStoreExceptionHandler._storeExceptionUnavailable, "Lock timed out.",),
+        AlreadyInTrashError: (_CommonStoreExceptionHandler._storeExceptionError, (calendarserver_namespace, "not-in-trash",),),
+        FailedCrossPodRequestError: (_CommonStoreExceptionHandler._storeExceptionUnavailable, "Cross-pod request failed.",),
+    }
+
     def __init__(self, calendar, home, name=None, *args, **kw):
         """
         Create a CalendarCollectionResource from a L{txdav.caldav.icalendar.ICalendar}
@@ -2204,7 +2309,7 @@
 
 
 
-class _CommonObjectResource(_NewStoreFileMetaDataHelper, CalDAVResource, FancyEqMixin):
+class _CommonObjectResource(_NewStoreFileMetaDataHelper, _CommonStoreExceptionHandler, CalDAVResource, FancyEqMixin):
 
     _componentFromStream = None
 
@@ -2323,54 +2428,7 @@
 
         returnValue(response)
 
-    # The following are used to map store exceptions into HTTP error responses
-    StoreExceptionsErrors = {}
-    StoreMoveExceptionsErrors = {}
-    StoreRemoveExceptionsErrors = {}
 
-    @classmethod
-    def _storeExceptionStatus(cls, err, arg):
-        """
-        Raise a status error.
-
-        @param err: the actual exception that caused the error
-        @type err: L{Exception}
-        @param arg: description of error or C{None}
-        @type arg: C{str} or C{None}
-        """
-        raise HTTPError(StatusResponse(responsecode.FORBIDDEN, arg if arg is not None else str(err)))
-
-
-    @classmethod
-    def _storeExceptionError(cls, err, arg):
-        """
-        Raise a DAV:error error with the supplied error element.
-
-        @param err: the actual exception that caused the error
-        @type err: L{Exception}
-        @param arg: the error element
-        @type arg: C{tuple}
-        """
-        raise HTTPError(ErrorResponse(
-            responsecode.FORBIDDEN,
-            arg,
-            str(err),
-        ))
-
-
-    @classmethod
-    def _storeExceptionUnavailable(cls, err, arg):
-        """
-        Raise a service unavailable error.
-
-        @param err: the actual exception that caused the error
-        @type err: L{Exception}
-        @param arg: description of error or C{None}
-        @type arg: C{str} or C{None}
-        """
-        raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE, arg if arg is not None else str(err)))
-
-
     @requiresPermissions(fromParent=[davxml.Unbind()])
     def http_DELETE(self, request):
         """
@@ -2459,18 +2517,10 @@
 
         # Handle the various store errors
         except Exception as err:
+            self._handleStoreException(err, self.StoreMoveExceptionsErrors)
+            raise
 
-            # Grab the current exception state here so we can use it in a re-raise - we need this because
-            # an inlineCallback might be called and that raises an exception when it returns, wiping out the
-            # original exception "context".
-            if type(err) in self.StoreMoveExceptionsErrors:
-                error, arg = self.StoreMoveExceptionsErrors[type(err)]
-                error(err, arg)
-            else:
-                # Return the original failure (exception) state
-                raise
 
-
     def http_PROPPATCH(self, request):
         """
         No dead properties allowed on object resources.
@@ -2508,11 +2558,8 @@
 
         # Map store exception to HTTP errors
         except Exception as err:
-            if type(err) in self.StoreExceptionsErrors:
-                error, arg = self.StoreExceptionsErrors[type(err)]
-                error(err, arg)
-            else:
-                raise
+            self._handleStoreException(err, self.StoreExceptionsErrors)
+            raise
 
 
     @inlineCallbacks
@@ -2557,11 +2604,8 @@
 
         # Map store exception to HTTP errors
         except Exception as err:
-            if type(err) in self.StoreExceptionsErrors:
-                error, arg = self.StoreExceptionsErrors[type(err)]
-                error(err, arg)
-            else:
-                raise
+            self._handleStoreException(err, self.StoreExceptionsErrors)
+            raise
 
         # Re-initialize to get stuff setup again now we have no object
         self._initializeWithObject(None, self._newStoreParent)
@@ -2698,52 +2742,49 @@
             return False
 
     StoreExceptionsErrors = {
-        ObjectResourceNameNotAllowedError: (_CommonObjectResource._storeExceptionStatus, None,),
-        ObjectResourceNameAlreadyExistsError: (_CommonObjectResource._storeExceptionStatus, None,),
-        TooManyObjectResourcesError: (_CommonObjectResource._storeExceptionError, customxml.MaxResources(),),
-        ObjectResourceTooBigError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "max-resource-size"),),
-        InvalidObjectResourceError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-calendar-data"),),
-        InvalidComponentForStoreError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-calendar-object-resource"),),
-        InvalidComponentTypeError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "supported-calendar-component"),),
-        TooManyAttendeesError: (_CommonObjectResource._storeExceptionError, MaxAttendeesPerInstance.fromString(str(config.MaxAttendeesPerInstance)),),
-        InvalidCalendarAccessError: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "valid-access-restriction"),),
-        ValidOrganizerError: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "valid-organizer"),),
-        UIDExistsError: (_CommonObjectResource._storeExceptionError, NoUIDConflict(),),
-        UIDExistsElsewhereError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "unique-scheduling-object-resource"),),
-        InvalidUIDError: (_CommonObjectResource._storeExceptionError, NoUIDConflict(),),
-        InvalidPerUserDataMerge: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-calendar-data"),),
-        AttendeeAllowedError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "attendee-allowed"),),
-        InvalidOverriddenInstanceError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-calendar-data"),),
-        TooManyInstancesError: (_CommonObjectResource._storeExceptionError, MaxInstances.fromString(str(config.MaxAllowedInstances)),),
-        AttachmentStoreValidManagedID: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-managed-id"),),
-        ShareeAllowedError: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "sharee-privilege-needed",),),
-        DuplicatePrivateCommentsError: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "no-duplicate-private-comments",),),
-        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
-        UnknownTimezone: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-timezone"),),
-        AlreadyInTrashError: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "not-in-trash",),),
+        ObjectResourceNameNotAllowedError: (_CommonStoreExceptionHandler._storeExceptionStatus, None,),
+        ObjectResourceNameAlreadyExistsError: (_CommonStoreExceptionHandler._storeExceptionStatus, None,),
+        TooManyObjectResourcesError: (_CommonStoreExceptionHandler._storeExceptionError, customxml.MaxResources(),),
+        ObjectResourceTooBigError: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "max-resource-size"),),
+        InvalidObjectResourceError: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "valid-calendar-data"),),
+        InvalidComponentForStoreError: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "valid-calendar-object-resource"),),
+        InvalidComponentTypeError: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "supported-calendar-component"),),
+        TooManyAttendeesError: (_CommonStoreExceptionHandler._storeExceptionError, MaxAttendeesPerInstance.fromString(str(config.MaxAttendeesPerInstance)),),
+        InvalidCalendarAccessError: (_CommonStoreExceptionHandler._storeExceptionError, (calendarserver_namespace, "valid-access-restriction"),),
+        ValidOrganizerError: (_CommonStoreExceptionHandler._storeExceptionError, (calendarserver_namespace, "valid-organizer"),),
+        UIDExistsError: (_CommonStoreExceptionHandler._storeExceptionError, NoUIDConflict(),),
+        UIDExistsElsewhereError: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "unique-scheduling-object-resource"),),
+        InvalidUIDError: (_CommonStoreExceptionHandler._storeExceptionError, NoUIDConflict(),),
+        InvalidPerUserDataMerge: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "valid-calendar-data"),),
+        AttendeeAllowedError: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "attendee-allowed"),),
+        InvalidOverriddenInstanceError: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "valid-calendar-data"),),
+        TooManyInstancesError: (_CommonStoreExceptionHandler._storeExceptionError, MaxInstances.fromString(str(config.MaxAllowedInstances)),),
+        AttachmentStoreValidManagedID: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "valid-managed-id"),),
+        ShareeAllowedError: (_CommonStoreExceptionHandler._storeExceptionError, (calendarserver_namespace, "sharee-privilege-needed",),),
+        DuplicatePrivateCommentsError: (_CommonStoreExceptionHandler._storeExceptionError, (calendarserver_namespace, "no-duplicate-private-comments",),),
+        LockTimeout: (_CommonStoreExceptionHandler._storeExceptionUnavailable, "Lock timed out.",),
+        UnknownTimezone: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "valid-timezone"),),
+        AlreadyInTrashError: (_CommonStoreExceptionHandler._storeExceptionError, (calendarserver_namespace, "not-in-trash",),),
+        FailedCrossPodRequestError: (_CommonStoreExceptionHandler._storeExceptionUnavailable, "Cross-pod request failed.",),
     }
 
     StoreMoveExceptionsErrors = {
-        ObjectResourceNameNotAllowedError: (_CommonObjectResource._storeExceptionStatus, None,),
-        ObjectResourceNameAlreadyExistsError: (_CommonObjectResource._storeExceptionStatus, None,),
-        TooManyObjectResourcesError: (_CommonObjectResource._storeExceptionError, customxml.MaxResources(),),
-        InvalidResourceMove: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "valid-move"),),
-        InvalidComponentTypeError: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "supported-calendar-component"),),
-        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
+        ObjectResourceNameNotAllowedError: (_CommonStoreExceptionHandler._storeExceptionStatus, None,),
+        ObjectResourceNameAlreadyExistsError: (_CommonStoreExceptionHandler._storeExceptionStatus, None,),
+        TooManyObjectResourcesError: (_CommonStoreExceptionHandler._storeExceptionError, customxml.MaxResources(),),
+        InvalidResourceMove: (_CommonStoreExceptionHandler._storeExceptionError, (calendarserver_namespace, "valid-move"),),
+        InvalidComponentTypeError: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "supported-calendar-component"),),
+        LockTimeout: (_CommonStoreExceptionHandler._storeExceptionUnavailable, "Lock timed out.",),
     }
 
-    StoreRemoveExceptionsErrors = {
-        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
+    StoreAttachmentValidErrors = {
+        AttachmentStoreFailed: _CommonStoreExceptionHandler._storeExceptionError,
+        InvalidAttachmentOperation: _CommonStoreExceptionHandler._storeExceptionError,
     }
 
-    StoreAttachmentValidErrors = set((
-        AttachmentStoreFailed,
-        InvalidAttachmentOperation,
-    ))
-
     StoreAttachmentExceptionsErrors = {
-        AttachmentStoreValidManagedID: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-managed-id-parameter",),),
-        AttachmentRemoveFailed: (_CommonObjectResource._storeExceptionError, (caldav_namespace, "valid-attachment-remove",),),
+        AttachmentStoreValidManagedID: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "valid-managed-id-parameter",),),
+        AttachmentRemoveFailed: (_CommonStoreExceptionHandler._storeExceptionError, (caldav_namespace, "valid-attachment-remove",),),
     }
 
 
@@ -3218,20 +3259,11 @@
         # Map store exception to HTTP errors
         except Exception as err:
 
-            if type(err) in self.StoreAttachmentValidErrors:
-                self._storeExceptionError(err, (caldav_namespace, valid_preconditions[action],))
+            self._handleStoreExceptionArg(err, self.StoreAttachmentValidErrors, (caldav_namespace, valid_preconditions[action],))
+            self._handleStoreException(err, self.StoreAttachmentExceptionsErrors)
+            self._handleStoreException(err, self.StoreExceptionsErrors)
+            raise
 
-            elif type(err) in self.StoreAttachmentExceptionsErrors:
-                error, arg = self.StoreAttachmentExceptionsErrors[type(err)]
-                error(err, arg)
-
-            elif type(err) in self.StoreExceptionsErrors:
-                error, arg = self.StoreExceptionsErrors[type(err)]
-                error(err, arg)
-
-            else:
-                raise
-
         # Look for Prefer header
         result = yield self._processPrefer(request, post_result)
 
@@ -3530,31 +3562,28 @@
     vCard = _CommonObjectResource.component
 
     StoreExceptionsErrors = {
-        ObjectResourceNameNotAllowedError: (_CommonObjectResource._storeExceptionStatus, None,),
-        ObjectResourceNameAlreadyExistsError: (_CommonObjectResource._storeExceptionStatus, None,),
-        TooManyObjectResourcesError: (_CommonObjectResource._storeExceptionError, customxml.MaxResources(),),
-        ObjectResourceTooBigError: (_CommonObjectResource._storeExceptionError, (carddav_namespace, "max-resource-size"),),
-        InvalidObjectResourceError: (_CommonObjectResource._storeExceptionError, (carddav_namespace, "valid-address-data"),),
-        InvalidComponentForStoreError: (_CommonObjectResource._storeExceptionError, (carddav_namespace, "valid-addressbook-object-resource"),),
-        UIDExistsError: (_CommonObjectResource._storeExceptionError, NovCardUIDConflict(),),
-        InvalidUIDError: (_CommonObjectResource._storeExceptionError, NovCardUIDConflict(),),
-        InvalidPerUserDataMerge: (_CommonObjectResource._storeExceptionError, (carddav_namespace, "valid-address-data"),),
-        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
+        ObjectResourceNameNotAllowedError: (_CommonStoreExceptionHandler._storeExceptionStatus, None,),
+        ObjectResourceNameAlreadyExistsError: (_CommonStoreExceptionHandler._storeExceptionStatus, None,),
+        TooManyObjectResourcesError: (_CommonStoreExceptionHandler._storeExceptionError, customxml.MaxResources(),),
+        ObjectResourceTooBigError: (_CommonStoreExceptionHandler._storeExceptionError, (carddav_namespace, "max-resource-size"),),
+        InvalidObjectResourceError: (_CommonStoreExceptionHandler._storeExceptionError, (carddav_namespace, "valid-address-data"),),
+        InvalidComponentForStoreError: (_CommonStoreExceptionHandler._storeExceptionError, (carddav_namespace, "valid-addressbook-object-resource"),),
+        UIDExistsError: (_CommonStoreExceptionHandler._storeExceptionError, NovCardUIDConflict(),),
+        InvalidUIDError: (_CommonStoreExceptionHandler._storeExceptionError, NovCardUIDConflict(),),
+        InvalidPerUserDataMerge: (_CommonStoreExceptionHandler._storeExceptionError, (carddav_namespace, "valid-address-data"),),
+        LockTimeout: (_CommonStoreExceptionHandler._storeExceptionUnavailable, "Lock timed out.",),
+        FailedCrossPodRequestError: (_CommonStoreExceptionHandler._storeExceptionUnavailable, "Cross-pod request failed.",),
     }
 
     StoreMoveExceptionsErrors = {
-        ObjectResourceNameNotAllowedError: (_CommonObjectResource._storeExceptionStatus, None,),
-        ObjectResourceNameAlreadyExistsError: (_CommonObjectResource._storeExceptionStatus, None,),
-        TooManyObjectResourcesError: (_CommonObjectResource._storeExceptionError, customxml.MaxResources(),),
-        InvalidResourceMove: (_CommonObjectResource._storeExceptionError, (calendarserver_namespace, "valid-move"),),
-        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
+        ObjectResourceNameNotAllowedError: (_CommonStoreExceptionHandler._storeExceptionStatus, None,),
+        ObjectResourceNameAlreadyExistsError: (_CommonStoreExceptionHandler._storeExceptionStatus, None,),
+        TooManyObjectResourcesError: (_CommonStoreExceptionHandler._storeExceptionError, customxml.MaxResources(),),
+        InvalidResourceMove: (_CommonStoreExceptionHandler._storeExceptionError, (calendarserver_namespace, "valid-move"),),
+        LockTimeout: (_CommonStoreExceptionHandler._storeExceptionUnavailable, "Lock timed out.",),
     }
 
-    StoreRemoveExceptionsErrors = {
-        LockTimeout: (_CommonObjectResource._storeExceptionUnavailable, "Lock timed out.",),
-    }
 
-
     def resourceType(self):
         if self.isShared():
             return customxml.ResourceType.sharedownergroup

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py	2016-04-21 19:48:22 UTC (rev 15538)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py	2016-04-21 20:11:28 UTC (rev 15539)
@@ -20,8 +20,11 @@
 from twext.python.clsprop import classproperty
 from txdav.common.datastore.test.util import populateCalendarsFrom
 from txdav.common.datastore.sql_tables import _BIND_MODE_READ, \
-    _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
+    _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED, \
+    _HOME_STATUS_EXTERNAL, _BIND_MODE_WRITE
 from txdav.common.datastore.podding.test.util import MultiStoreConduitTest
+from txdav.common.datastore.podding.base import FailedCrossPodRequestError
+from txdav.common.icommondatastore import ExternalShareFailed
 
 
 class BaseSharingTests(MultiStoreConduitTest):
@@ -85,6 +88,14 @@
 class CalendarSharing(BaseSharingTests):
 
     @inlineCallbacks
+    def setUp(self):
+        yield super(CalendarSharing, self).setUp()
+        for store in self.theStores:
+            store._poddingFailure = None
+            store._poddingError = None
+
+
+    @inlineCallbacks
     def test_no_shares(self):
         """
         Test that initially there are no shares.
@@ -567,7 +578,167 @@
         yield self.commitTransaction(1)
 
 
+    @inlineCallbacks
+    def test_invite_sharee_failure(self):
+        """
+        Test invite fails gracefully when the other pod is down.
+        """
 
+        # Force store to generate 500 error
+        self.patch(self.theStores[1], "_poddingFailure", ValueError)
+
+        # Invite
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        yield self.assertFailure(calendar.inviteUIDToShare("puser02", _BIND_MODE_READ, "summary"), FailedCrossPodRequestError)
+
+
+    @inlineCallbacks
+    def test_uninvite_sharee_failure(self):
+        """
+        Test uninvite fails gracefully when the other pod is down.
+        Also test that the sharee bind entry is removed when an invalid share is detected.
+        """
+
+        # Invite
+        sharedName = yield self.createShare("user01", "puser02", "calendar")
+
+        # Has external sharee bind entry
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(0), name="puser02", status=_HOME_STATUS_EXTERNAL
+        )
+        calendar = yield home.anyObjectWithShareUID(sharedName)
+        self.assertTrue(calendar is not None)
+        yield self.commitTransaction(0)
+
+        # Force store to generate 500 error
+        self.patch(self.theStores[1], "_poddingFailure", ValueError)
+
+        yield self.removeShare("user01", "puser02", "calendar")
+
+        # Store working again
+        self.patch(self.theStores[1], "_poddingFailure", None)
+
+        # No external sharee bind entry
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(0), name="puser02", status=_HOME_STATUS_EXTERNAL
+        )
+        calendar = yield home.anyObjectWithShareUID(sharedName)
+        self.assertTrue(calendar is None)
+        yield self.commitTransaction(0)
+
+        # Has external sharer bind entry
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_EXTERNAL
+        )
+        calendar = yield home.anyObjectWithShareUID("calendar")
+        self.assertTrue(calendar is not None)
+        yield self.commitTransaction(1)
+
+        # Has sharee bind entry
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="puser02"
+        )
+        calendar = yield home.anyObjectWithShareUID(sharedName)
+        self.assertTrue(calendar is not None)
+        yield self.commitTransaction(1)
+
+        # Force clean-up of sharee calendar
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="puser02"
+        )
+        calendar = yield home.anyObjectWithShareUID(sharedName)
+        yield self.assertFailure(calendar.syncTokenRevision(), ExternalShareFailed)
+        yield self.commitTransaction(1)
+
+        # External sharer bind entry gone
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_EXTERNAL
+        )
+        calendar = yield home.anyObjectWithShareUID("calendar")
+        self.assertTrue(calendar is None)
+        yield self.commitTransaction(1)
+
+        # Sharee bind entry gone
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="puser02"
+        )
+        calendar = yield home.anyObjectWithShareUID(sharedName)
+        self.assertTrue(calendar is None)
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_reply_sharee_failure(self):
+        """
+        Test sharee reply fails and cleans up when the share is invalid.
+        """
+
+        # Invite
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(0), name="user01", create=True
+        )
+        calendar = yield home.calendarWithName("calendar")
+        yield calendar.inviteUIDToShare(
+            "puser02", _BIND_MODE_WRITE, "shared", shareName="shared-calendar"
+        )
+        yield self.commitTransaction(0)
+
+        # Has external sharee bind entry
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(0), name="puser02", status=_HOME_STATUS_EXTERNAL
+        )
+        calendar = yield home.anyObjectWithShareUID("shared-calendar")
+        self.assertTrue(calendar is not None)
+        yield self.commitTransaction(0)
+
+        # Has external sharer bind entry
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_EXTERNAL
+        )
+        calendar = yield home.anyObjectWithShareUID("calendar")
+        self.assertTrue(calendar is not None)
+        yield self.commitTransaction(1)
+
+        # Has sharee bind entry
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="puser02"
+        )
+        calendar = yield home.anyObjectWithShareUID("shared-calendar")
+        self.assertTrue(calendar is not None)
+        yield self.commitTransaction(1)
+
+        # Force store to generate an error
+        self.patch(self.theStores[0], "_poddingError", ExternalShareFailed)
+
+        # ACK: home2 is None
+        home2 = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="puser02"
+        )
+        yield self.assertFailure(home2.acceptShare("shared-calendar"), ExternalShareFailed)
+        yield self.commitTransaction(1)
+
+        # External sharer bind entry gone
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_EXTERNAL
+        )
+        calendar = yield home.anyObjectWithShareUID("calendar")
+        self.assertTrue(calendar is None)
+        yield self.commitTransaction(1)
+
+        # Sharee bind entry gone
+        home = yield self.homeUnderTest(
+            txn=self.theTransactionUnderTest(1), name="puser02"
+        )
+        calendar = yield home.anyObjectWithShareUID("shared-calendar")
+        self.assertTrue(calendar is None)
+        yield self.commitTransaction(1)
+
+
+
 class SharingRevisions(BaseSharingTests):
     """
     Test store-based sharing and interaction with revision table.

Modified: CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py	2016-04-21 19:48:22 UTC (rev 15538)
+++ CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py	2016-04-21 20:11:28 UTC (rev 15539)
@@ -166,7 +166,7 @@
         # Sharee home on this pod must already exist
         shareeHome = yield txn.homeWithUID(request["type"], request["sharee"])
         if shareeHome is None or shareeHome.external():
-            FailedCrossPodRequestError("Invalid sharee UID specified")
+            raise FailedCrossPodRequestError("Invalid sharee UID specified")
 
         # Remove a share
         yield shareeHome.processExternalUninvite(
@@ -229,7 +229,7 @@
         # Sharer home on this pod must already exist
         ownerHome = yield txn.homeWithUID(request["type"], request["owner"])
         if ownerHome is None or ownerHome.external():
-            FailedCrossPodRequestError("Invalid owner UID specified")
+            raise FailedCrossPodRequestError("Invalid owner UID specified")
 
         # Process a reply
         yield ownerHome.processExternalReply(

Modified: CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/util.py	2016-04-21 19:48:22 UTC (rev 15538)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/util.py	2016-04-21 20:11:28 UTC (rev 15539)
@@ -83,11 +83,20 @@
         """
 
         store = self.storeMap[self.server.details()]
+
+        # Force a failure of the entire request with the supplied exception type
+        if getattr(store, "_poddingFailure", None) is not None:
+            raise store._poddingFailure("Failed cross-pod request")
+
         j = json.loads(self.data)
         if self.stream is not None:
             j["stream"] = self.stream
             j["streamType"] = self.streamType
         try:
+            # Force a BAD cross-pod request with the supplied exception type
+            if getattr(store, "_poddingError", None) is not None:
+                raise store._poddingError("Failed cross-pod request")
+
             if store.conduit.isStreamAction(j):
                 stream = ProducerStream()
 

Modified: CalendarServer/trunk/txdav/common/datastore/podding/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/util.py	2016-04-21 19:48:22 UTC (rev 15538)
+++ CalendarServer/trunk/txdav/common/datastore/podding/util.py	2016-04-21 20:11:28 UTC (rev 15539)
@@ -19,6 +19,7 @@
 from txdav.common.datastore.podding.base import FailedCrossPodRequestError
 from txdav.common.datastore.sql_notification import NotificationCollection, \
     NotificationObject
+from txdav.common.icommondatastore import NonExistentExternalShare
 
 
 class UtilityConduitMixin(object):
@@ -137,7 +138,9 @@
         elif "homeChildSharedID" in request:
             homeChild = yield home.childWithName(request["homeChildSharedID"])
             if homeChild is None:
-                raise FailedCrossPodRequestError("Invalid home child specified")
+                # Raise NonExistentExternalShare here so we can indicate to the other pod
+                # that it has a bogus share and it can fix itself
+                raise NonExistentExternalShare("Invalid home child specified")
             returnObject = homeChild
             if request.get("classMethod", False):
                 classObject = homeChild._objectResourceClass

Modified: CalendarServer/trunk/txdav/common/datastore/sql_external.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_external.py	2016-04-21 19:48:22 UTC (rev 15538)
+++ CalendarServer/trunk/txdav/common/datastore/sql_external.py	2016-04-21 20:11:28 UTC (rev 15539)
@@ -283,6 +283,7 @@
         return True
 
 
+    @inlineCallbacks
     def fixNonExistentExternalShare(self):
         """
         An external request has returned and indicates the external share no longer exists. That
@@ -291,6 +292,7 @@
         log.error("Non-existent share detected and removed for {share}", share=self)
         ownerView = yield self.ownerView()
         yield ownerView.removeShare(self)
+        yield ownerView.cleanExternalShare()
 
 
     @inlineCallbacks

Modified: CalendarServer/trunk/txdav/common/datastore/sql_sharing.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_sharing.py	2016-04-21 19:48:22 UTC (rev 15538)
+++ CalendarServer/trunk/txdav/common/datastore/sql_sharing.py	2016-04-21 20:11:28 UTC (rev 15539)
@@ -132,9 +132,7 @@
 
         # See if there are any references to the external share. If not,
         # remove it
-        invites = yield ownerView.sharingInvites()
-        if len(invites) == 0:
-            yield ownerHome.removeExternalChild(ownerView)
+        yield ownerView.cleanExternalShare()
 
 
     @inlineCallbacks
@@ -476,7 +474,14 @@
         shareeView = yield self.shareeView(shareeUID)
         if shareeView is not None:
             if shareeView.viewerHome().external():
-                yield self._sendExternalUninvite(shareeView)
+                try:
+                    yield self._sendExternalUninvite(shareeView)
+                except Exception as e:
+                    # If the cross-pod request fails for some reason, ignore the exception and go ahead
+                    # and remove the share on this pod. It is up to the other pod to "heal" itself
+                    # by  detecting an invalid share when it is running properly again.
+                    log.error("Could not send sharing uninvite '{userid}': {ex}", userid=shareeUID, ex=e)
+
             else:
                 # If current user state is accepted then we send an invite with the new state, otherwise
                 # we cancel any existing invites for the user. Also, if the ownerHome is disabled, we assume
@@ -551,6 +556,20 @@
             yield self.uninviteUIDFromShare(invitation.shareeUID)
 
 
+    @inlineCallbacks
+    def cleanExternalShare(self):
+        """
+        Called when an external share is removed. This method will remove the reference to
+        the external shared calendar when there are no more sharees on this pod.
+        """
+
+        # See if there are any references to the external share. If not,
+        # remove it
+        invites = yield self.sharingInvites()
+        if len(invites) == 0:
+            yield self._home.removeExternalChild(self)
+
+
     def newShare(self, displayname=None):
         """
         Override in derived classes to do any specific operations needed when a share
@@ -694,15 +713,21 @@
     @inlineCallbacks
     def _replyExternalInvite(self, status, summary=None):
 
-        yield self._txn.store().conduit.send_sharereply(
-            self._txn,
-            self.viewerHome()._homeType,
-            self.ownerHome().uid(),
-            self.viewerHome().uid(),
-            self.shareUID(),
-            status,
-            summary,
-        )
+        # If a reply to an external share fails, then assume the external share
+        # is broken and remove it from the local pod
+        try:
+            yield self._txn.store().conduit.send_sharereply(
+                self._txn,
+                self.viewerHome()._homeType,
+                self.ownerHome().uid(),
+                self.viewerHome().uid(),
+                self.shareUID(),
+                status,
+                summary,
+            )
+        except ExternalShareFailed:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
 
 
     #
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20160421/a1e7f2f4/attachment-0001.html>


More information about the calendarserver-changes mailing list