[CalendarServer-changes] [14333] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Jan 21 11:57:08 PST 2015


Revision: 14333
          http://trac.calendarserver.org//changeset/14333
Author:   cdaboo at apple.com
Date:     2015-01-21 11:57:08 -0800 (Wed, 21 Jan 2015)
Log Message:
-----------
Refactor the conduit API to support arbitrary store object RPC calls between pods.

Modified Paths:
--------------
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py
    CalendarServer/trunk/txdav/carddav/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/podding/attachments.py
    CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
    CalendarServer/trunk/txdav/common/datastore/podding/resource.py
    CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql_external.py
    CalendarServer/trunk/txdav/common/datastore/test/util.py

Added Paths:
-----------
    CalendarServer/trunk/doc/Developer/CrossPodDesign.txt
    CalendarServer/trunk/txdav/common/datastore/podding/store_api.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_store_api.py

Removed Paths:
-------------
    CalendarServer/trunk/txdav/common/datastore/podding/sharing_base.py
    CalendarServer/trunk/txdav/common/datastore/podding/sharing_store.py

Added: CalendarServer/trunk/doc/Developer/CrossPodDesign.txt
===================================================================
--- CalendarServer/trunk/doc/Developer/CrossPodDesign.txt	                        (rev 0)
+++ CalendarServer/trunk/doc/Developer/CrossPodDesign.txt	2015-01-21 19:57:08 UTC (rev 14333)
@@ -0,0 +1,73 @@
+Cross Pod API Design (Conduit)
+==============================
+
+This document described the key design features of the cross-pod API that allows each server in a pod (multi-store, single directory cluster) to communicate with each other.
+
+The cross-pod API needs to handle the following behaviors:
+
+1) Sharing - allow users on one pod to share calendars with users on another.
+
+2) Managed attachments - allow users on one pod to add attachments to events hosted on another.
+
+3) Delegate assignments - allow delegate assignments to users on other pods to correctly allow those other users to see the assignments on their own principal resources.
+
+4) Migration - allow user data to be moved from one pod to another, together with all sharing and delegate assignments.
+
+Some other key requirements:
+
+1) Must support pods using different versions of the software.
+
+2) Must support load balancing of requests to pods with multiple hosts.
+
+Basic Design
+============
+
+The cross-pod code is located in the txdav.common.datastore.podding package.
+
+The L{PoddingConduit} class handles the cross-pod requests and responses. The base API uses JSON to serialize commands as a set of "actions" with additional arguments. L{PoddingConduit} has a L{conduitRequestClass} class variable that defines how the requests are sent. The default implementation is L{ConduitRequest} which uses HTTP to send the request to another Pod. The L{ConduitResource} is an HTTP L{Resource} that handles the cross-pod HTTP request and channels the request data to the L{PoddingConduit} in the recipient pod. Thus two L{PoddingConduits} on different pods are "hooked-up" to each other over HTTP. For unit testing it is possible to hook up two L{PoddingConduits} to directly call each other, thus bypassing the need to setup any HTTP servers.
+
+When a L{CommonDataStore} is created, it creates a L{PoddingConduit} object and assigns it to an instance variable. When a store detects that a cross-pod request is needed it will use the associated conduit.
+
+The L{PoddingConduit} JSON request object contains an "action" member that is the name of the RPC call, together with "arguments" and "keywords" members representing the position arguments and keyword arguments of the RPC call. The request object also contains sufficient information to identify the target store object (the home user uid, home child resource id, object resource resource id, etc).
+
+The L{PoddingConduit} JSON response object contains a "result" member that indicates whether the request succeeded (when set to the value "ok") or failed (when set to the value "exception"). When set to "ok", there will be a "value" member present with the result of the RPC call. When set to "exception" there will be a "class" member (whose name matches the class of the exception object raised) and a "details" member (whose value is the string representation of the raised exception). If an exception is returned as the response, the conduit on the sender's side will raise the named exception.
+
+External Store API
+==================
+
+A L{CommonDataStore} makes use of the following key classes: L{CommonHome}, L{CommonHomeChild}, L{CommonObjectResource}. Each of those make calls to the store's underlying database (SQL) to store and retrieve their associated data.
+
+For cross-pod support, there are a new set of derived classes: L{CommonHomeExternal}, L{CommonHomeChildExternal}, L{CommonObjectResourceExternal}. These classes override the methods that make calls to the database, and instead direct those calls to the store's conduit using the same API as the original method. The conduit then serializes the API call and sends it to the other pod, which deserializes the request, creates the matching "internal" store object and calls the appropriate method with the supplied arguments (note that the API has to distinguish the case of a call of a class method vs an instance method). In effect this implements a remote procedure call from a store object on one pod, to a store object on another.
+
+The L{CommonHomeExternal}, L{CommonHomeChildExternal}, L{CommonObjectResourceExternal} are "chained" such that an L{CommonHomeExternal} will use L{CommonHomeChildExternal} as its home child class, and L{CommonHomeChildExternal} will use L{CommonObjectResourceExternal} as its object resource class (though for sharing this behavior can be different).
+
+Note that the external classes typically exist without any associated data in the local store's database - i.e., they entirely represent objects in another pod - (though for sharing this behavior can be different).
+
+The external store API is handled by the L{StoreAPIConduitMixin} which defines a set of methods on the L{PoddingConduit} class that implement the necessary RPC calls. For each RPC call there is one "send_XXX" and one "recv_XXX" method for the action "XXX". The "send" method is called by the external store object, and the "recv" method is called by the receiving pod's HTTP request handling resource. The L{StoreAPIConduitMixin} has a L{_make_simple_action} class method that can be used to programmatically create a set of "send_XXX" and "recv_XXX" methods for each RPC call. 
+
+The L{StoreAPIConduitMixin} also contains specialized "send_XXX" and "recv_XXX" methods for some specific API calls that either don't use store objects directly or have more complex requirements. Other mixin classes add additional RPC calls for specific behaviors.
+
+Sharing API
+===========
+
+When sharing a collection, the collection is identified via an ownerHome and a viewerHome. The owner is the sharer and the viewer is the sharee (when both are the same then the collection is the owner's own view of it). A BIND table is used to map a collection to a specific viewer home. There will always be one such entry for the viewer == owner case. When a sharee is added, there will be a new BIND entry for them.
+
+For cross-pod sharing, we want to replicate the BIND table entries across pods so that each pod can quickly identify that shared collections from another pod exist, without the need to initiate cross-pod calls to all pods to query for possible shared collections. The owner and sharee of a collection are identifiable on each pod:
+
+	(a) Owners pod:
+		- A HOME table entry for the sharee marked as status-external
+		- A BIND table entry for the shared calendar referencing the sharee HOME entry
+	
+	(b) Sharee's pod:
+		- A HOME table entry for the owner marked as status-external
+		- A BIND table entry for the owner's calendar referencing the owner HOME
+		
+Cross-pod sharing can then be split into two functional areas:
+
+	(a) Management of sharing invites (i.e., creating, updating, removing BIND rows, and handling notifications)
+	(b) Sharee accessing owner data (object resources, sync tokens etc)
+	
+For (a) a set of "invite" specific conduit APIs exist in the L{SharingInvitesConduitMixin}. The existing store classes are modified to spot when a viewer or owner home is "external" and in such cases will send a cross-pod request to the relevant pod.
+
+For (b), the sharee's pod needs to direct store data requests to the owner pod for specific operations (anything that needs to access the owner data). Basically any request where the ownerHome() of the collection refers to an external home needs to be directed to the owner's pod. i.e., the server chosen for a cross-pod request must be based on the owner's server and not the viewer's server (since the request originates on the viewer's server).
+

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -2273,7 +2273,7 @@
         if name.startswith("."):
             raise ObjectResourceNameNotAllowedError(name)
 
-        c = cls._externalClass if parent.external() else cls
+        c = cls._externalClass if parent.externalClass() else cls
         objectResource = c(parent, name, None, None, options=options)
         yield objectResource._setComponentInternal(component, inserting=True, internal_state=internal_state, split_details=split_details)
         yield objectResource._loadPropertyStore(created=True)

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql_external.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -38,7 +38,7 @@
 
     @inlineCallbacks
     def populate(self):
-        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        yield populateCalendarsFrom(self.requirements, self.theStoreUnderTest(0))
         self.notifierFactory.reset()
 
     cal1 = """BEGIN:VCALENDAR
@@ -90,7 +90,7 @@
         Test that initially there are no shares.
         """
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        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())
@@ -103,7 +103,7 @@
         """
 
         # Invite
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        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())
@@ -123,18 +123,18 @@
 
         self.assertTrue(calendar.isShared())
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+        notifyHome = yield self.theTransactionUnderTest(1).notificationsWithUID("puser02")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID, ])
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         # Uninvite
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         invites = yield calendar.sharingInvites()
         self.assertEqual(len(invites), 1)
 
@@ -144,14 +144,14 @@
 
         self.assertTrue(calendar.isShared())
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+        notifyHome = yield self.theTransactionUnderTest(1).notificationsWithUID("puser02")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [])
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertTrue(calendar.isShared())
         yield calendar.setShared(False)
         self.assertFalse(calendar.isShared())
@@ -164,7 +164,7 @@
         """
 
         # Invite
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        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())
@@ -178,48 +178,48 @@
 
         self.assertTrue(calendar.isShared())
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+        notifyHome = yield self.theTransactionUnderTest(1).notificationsWithUID("puser02")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(len(notifications), 1)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         # Accept
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         yield shareeHome.acceptShare(inviteUID)
 
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is not None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifyHome = yield self.theTransactionUnderTest(0).notificationsWithUID("user01")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID + "-reply", ])
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertTrue(calendar.isShared())
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         # Re-accept
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         yield shareeHome.acceptShare(inviteUID)
 
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is not None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifyHome = yield self.theTransactionUnderTest(0).notificationsWithUID("user01")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID + "-reply", ])
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertTrue(calendar.isShared())
 
 
@@ -230,7 +230,7 @@
         """
 
         # Invite
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        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())
@@ -244,49 +244,49 @@
 
         self.assertTrue(calendar.isShared())
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is None)
 
         notifyHome = yield txn2.notificationsWithUID("puser02")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(len(notifications), 1)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         # Decline
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         yield shareeHome.declineShare(inviteUID)
 
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifyHome = yield self.theTransactionUnderTest(0).notificationsWithUID("user01")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID + "-reply", ])
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertTrue(calendar.isShared())
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         # Redecline
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         yield shareeHome.declineShare(inviteUID)
 
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifyHome = yield self.theTransactionUnderTest(0).notificationsWithUID("user01")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID + "-reply", ])
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertTrue(calendar.isShared())
 
 
@@ -298,7 +298,7 @@
         """
 
         # Invite
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        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())
@@ -312,49 +312,49 @@
 
         self.assertTrue(calendar.isShared())
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is None)
 
         notifyHome = yield txn2.notificationsWithUID("puser02")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(len(notifications), 1)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         # Accept
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         yield shareeHome.acceptShare(inviteUID)
 
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is not None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifyHome = yield self.theTransactionUnderTest(0).notificationsWithUID("user01")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID + "-reply", ])
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertTrue(calendar.isShared())
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         # Decline
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         yield shareeHome.declineShare(inviteUID)
 
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifyHome = yield self.theTransactionUnderTest(0).notificationsWithUID("user01")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID + "-reply", ])
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertTrue(calendar.isShared())
 
 
@@ -366,7 +366,7 @@
         """
 
         # Invite
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         invites = yield calendar.sharingInvites()
         self.assertEqual(len(invites), 0)
 
@@ -377,44 +377,44 @@
         inviteUID = shareeView.shareUID()
         sharedName = shareeView.name()
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is None)
 
         notifyHome = yield txn2.notificationsWithUID("puser02")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(len(notifications), 1)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         # Accept
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         yield shareeHome.acceptShare(inviteUID)
 
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is not None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifyHome = yield self.theTransactionUnderTest(0).notificationsWithUID("user01")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID + "-reply", ])
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         # Delete
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         yield shared.deleteShare()
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertTrue(shared is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifyHome = yield self.theTransactionUnderTest(0).notificationsWithUID("user01")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID + "-reply", ])
 
@@ -425,10 +425,10 @@
         yield self.removeShare()
         shared_name = yield self.createShare()
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
         self.assertTrue(otherCal is not None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -437,49 +437,49 @@
         Test that deleting and re-creating a share with the same sharer name works.
         """
 
-        home = yield self.homeUnderTest(name="user01", create=True)
+        home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
         yield home.createCalendarWithName("shared")
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         shared_name = yield self.createShare(name="shared")
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
         self.assertTrue(otherCal is not None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         yield self.removeShare(name="shared")
-        home = yield self.homeUnderTest(name="user01", create=True)
+        home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
         yield home.removeCalendarWithName("shared")
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
         self.assertTrue(otherCal is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        home = yield self.homeUnderTest(name="user01", create=True)
+        home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
         yield home.createCalendarWithName("shared")
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         shared_name = yield self.createShare(name="shared")
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
         self.assertTrue(otherCal is not None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
     def test_inviteProperties(self):
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar.setUsedForFreeBusy(True)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         shared_name = yield self.createShare()
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
         self.assertFalse(shared.isUsedForFreeBusy())
 
@@ -491,7 +491,7 @@
         """
 
         # Invite
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        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())
@@ -507,28 +507,28 @@
 
         sharedName = shareeView.name()
 
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shared = yield self.calendarUnderTest(txn=txn2, home="user02", name=sharedName)
         self.assertTrue(shared is not None)
 
         notifyHome = yield txn2.notificationsWithUID("user02")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(len(notifications), 0)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         # Remove
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         yield shared.deleteShare()
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         invites = yield calendar.sharingInvites()
         self.assertEqual(len(invites), 0)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifyHome = yield self.theTransactionUnderTest(0).notificationsWithUID("user01")
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(len(notifications), 0)
 
@@ -538,13 +538,13 @@
     def test_sharedNotifierID(self):
         shared_name = yield self.createShare()
 
-        home = yield self.homeUnderTest(name="user01")
+        home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01")
         self.assertEquals(home.notifierID(), ("CalDAV", "user01",))
         calendar = yield home.calendarWithName("calendar")
         self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         home = yield self.homeUnderTest(txn=txn2, name="puser02")
         self.assertEquals(home.notifierID(), ("CalDAV", "puser02",))
         calendar = yield home.calendarWithName(shared_name)
@@ -556,15 +556,15 @@
         shared_name1 = yield self.createShare(shareeGUID="puser02")
         shared_name2 = yield self.createShare(shareeGUID="puser03")
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name1)
         self.assertTrue(otherCal is not None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherCal = yield self.calendarUnderTest(txn=txn2, home="puser03", name=shared_name2)
         self.assertTrue(otherCal is not None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
 
@@ -580,10 +580,10 @@
         """
         sharedName = yield self.createShare()
 
-        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        normalCal = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertEqual(normalCal._bindRevision, 0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertNotEqual(otherCal._bindRevision, 0)
 
@@ -594,34 +594,34 @@
         Verify that bindRevision on calendars and shared calendars has the correct value.
         """
         # Invite
-        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         invites = yield calendar.sharingInvites()
         self.assertEqual(len(invites), 0)
 
         shareeView = yield calendar.inviteUIDToShare("puser02", _BIND_MODE_READ, "summary")
         newCalName = shareeView.shareUID()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        normalCal = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertEqual(normalCal._bindRevision, 0)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         otherCal = yield otherHome.anyObjectWithShareUID(newCalName)
         self.assertEqual(otherCal._bindRevision, 0)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         shareeView = yield shareeHome.acceptShare(newCalName)
         sharedName = shareeView.name()
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        normalCal = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertEqual(normalCal._bindRevision, 0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertNotEqual(otherCal._bindRevision, 0)
 
@@ -633,10 +633,10 @@
         """
         sharedName = yield self.createShare()
 
-        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        normalCal = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         self.assertEqual(normalCal._bindRevision, 0)
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
         otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
         self.assertNotEqual(otherCal._bindRevision, 0)
@@ -668,11 +668,11 @@
             self.assertEqual(len(deleted), 0)
             self.assertEqual(len(invalid), 1 if depth == "infinity" else 0)
 
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         yield self.removeShare()
 
-        txn2 = self.newOtherTransaction()
+        txn2 = self.theTransactionUnderTest(1)
         otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
 
         for depth in ("1", "infinity",):

Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/sql.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -1892,7 +1892,7 @@
         @rtype: L{CommonHomeChild}
         """
 
-        c = cls._externalClass if parent.external() else cls
+        c = cls._externalClass if parent.externalClass() else cls
         child = c(
             parent,
             objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
@@ -2040,6 +2040,15 @@
         return self.addressbook().external()
 
 
+    def externalClass(self):
+        """
+        Is this an external object.
+
+        @return: a string.
+        """
+        return self.addressbook().externalClass()
+
+
     @inlineCallbacks
     def remove(self, options=None):
 

Modified: CalendarServer/trunk/txdav/common/datastore/podding/attachments.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/attachments.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/podding/attachments.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -17,7 +17,7 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 
-class AttachmentsPoddingConduitMixin(object):
+class AttachmentsConduitMixin(object):
     """
     Defines the cross-pod API for managed attachments that will be mixed into the
     L{PoddingConduit} class.
@@ -41,12 +41,11 @@
         """
 
         actionName = "add-attachment"
-        shareeView = objectResource._parentCollection
-        request, recipient = yield self._getRequestForResource(actionName, shareeView, objectResource)
+        txn, request, server = yield self._getRequestForStoreObject(actionName, objectResource, False)
         request["rids"] = rids
         request["filename"] = filename
 
-        response = yield self.sendRequest(shareeView._txn, recipient, request, stream, content_type)
+        response = yield self.sendRequestToServer(txn, server, request, stream, content_type)
         returnValue(response)
 
 
@@ -59,7 +58,7 @@
         @type request: C{dict}
         """
 
-        _ignore_shareeView, objectResource = yield self._getResourcesForRequest(txn, request)
+        objectResource, _ignore = yield self._getStoreObjectForRequest(txn, request)
         attachment, location = yield objectResource.addAttachment(
             request["rids"],
             request["streamType"],
@@ -88,12 +87,11 @@
         """
 
         actionName = "update-attachment"
-        shareeView = objectResource._parentCollection
-        request, recipient = yield self._getRequestForResource(actionName, shareeView, objectResource)
+        txn, request, server = yield self._getRequestForStoreObject(actionName, objectResource, False)
         request["managedID"] = managed_id
         request["filename"] = filename
 
-        response = yield self.sendRequest(shareeView._txn, recipient, request, stream, content_type)
+        response = yield self.sendRequestToServer(txn, server, request, stream, content_type)
         returnValue(response)
 
 
@@ -106,7 +104,7 @@
         @type request: C{dict}
         """
 
-        _ignore_shareeView, objectResource = yield self._getResourcesForRequest(txn, request)
+        objectResource, _ignore = yield self._getStoreObjectForRequest(txn, request)
         attachment, location = yield objectResource.updateAttachment(
             request["managedID"],
             request["streamType"],
@@ -131,12 +129,11 @@
         """
 
         actionName = "remove-attachment"
-        shareeView = objectResource._parentCollection
-        request, recipient = yield self._getRequestForResource(actionName, shareeView, objectResource)
+        txn, request, server = yield self._getRequestForStoreObject(actionName, objectResource, False)
         request["rids"] = rids
         request["managedID"] = managed_id
 
-        yield self.sendRequest(shareeView._txn, recipient, request)
+        yield self.sendRequestToServer(txn, server, request)
 
 
     @inlineCallbacks
@@ -148,7 +145,7 @@
         @type request: C{dict}
         """
 
-        _ignore_shareeView, objectResource = yield self._getResourcesForRequest(txn, request)
+        objectResource, _ignore = yield self._getStoreObjectForRequest(txn, request)
         yield objectResource.removeAttachment(
             request["rids"],
             request["managedID"],

Modified: CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/conduit.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/podding/conduit.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -17,25 +17,24 @@
 from twext.python.log import Logger
 
 from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
-from txdav.common.datastore.podding.attachments import AttachmentsPoddingConduitMixin
+from txdav.common.datastore.podding.attachments import AttachmentsConduitMixin
 from txdav.common.datastore.podding.base import FailedCrossPodRequestError
 from txdav.common.datastore.podding.directory import DirectoryPoddingConduitMixin
+from txdav.common.datastore.podding.store_api import StoreAPIConduitMixin
 from txdav.common.datastore.podding.request import ConduitRequest
-from txdav.common.datastore.podding.sharing_invites import SharingInvitesPoddingConduitMixin
-from txdav.common.datastore.podding.sharing_store import SharingStorePoddingConduitMixin
+from txdav.common.datastore.podding.sharing_invites import SharingInvitesConduitMixin
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python.reflect import namedClass
-from twisted.python.failure import Failure
 
 
 log = Logger()
 
 
 class PoddingConduit(
-    AttachmentsPoddingConduitMixin,
-    SharingInvitesPoddingConduitMixin,
-    SharingStorePoddingConduitMixin,
+    StoreAPIConduitMixin,
+    AttachmentsConduitMixin,
+    SharingInvitesConduitMixin,
     DirectoryPoddingConduitMixin,
 ):
     """
@@ -117,7 +116,7 @@
         except Exception as e:
             raise FailedCrossPodRequestError("Failed cross-pod request: {}".format(e))
         if response["result"] == "exception":
-            raise namedClass(response["class"])(response["result"])
+            raise namedClass(response["class"])(response["details"])
         elif response["result"] != "ok":
             raise FailedCrossPodRequestError("Cross-pod request failed: {}".format(response))
         else:
@@ -157,12 +156,18 @@
             result = {"result": "ok"}
             if value is not None:
                 result["value"] = value
+
         except Exception as e:
-            ex = Failure()
+            # Send the exception over to the other side
             yield txn.abort()
             log.error("Failed action: {action}, {ex}", action=action, ex=e)
-            ex.raiseException()
+            result = {
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "details": str(e),
+            }
 
-        yield txn.commit()
+        else:
+            yield txn.commit()
 
         returnValue(result)

Modified: CalendarServer/trunk/txdav/common/datastore/podding/resource.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/resource.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/podding/resource.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -157,7 +157,7 @@
         # Get the conduit to process the data
         try:
             result = yield self.store.conduit.processRequest(j)
-            code = responsecode.OK
+            code = responsecode.OK if result["result"] == "ok" else responsecode.BAD_REQUEST
         except Exception as e:
             # Send the exception over to the other side
             result = {

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/sharing_base.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/sharing_base.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/podding/sharing_base.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -1,91 +0,0 @@
-##
-# Copyright (c) 2013-2015 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-from txdav.common.datastore.podding.base import FailedCrossPodRequestError
-
-
-class SharingCommonPoddingConduit(object):
-    """
-    Defines common cross-pod API for sharing that will be mixed into the L{PoddingConduit} class.
-    """
-
-    #
-    # Sharer data access related apis
-    #
-
-    @inlineCallbacks
-    def _getRequestForResource(self, action, parent, child=None):
-        """
-        Create a request for an operation on a L{CommonHomeChild}. This is used when building the JSON
-        request object prior to sending it.
-
-        @param shareeView: sharee resource being operated on.
-        @type shareeView: L{CommonHomeChildExternal}
-        """
-
-        homeType = parent.ownerHome()._homeType
-        ownerUID = parent.ownerHome().uid()
-        ownerID = parent.external_id()
-        shareeUID = parent.viewerHome().uid()
-
-        _ignore_sender, recipient = yield self.validRequest(shareeUID, ownerUID)
-
-        result = {
-            "action": action,
-            "type": homeType,
-            "owner": ownerUID,
-            "owner_id": ownerID,
-            "sharee": shareeUID,
-        }
-        if child is not None:
-            result["resource_id"] = child.id()
-        returnValue((result, recipient))
-
-
-    @inlineCallbacks
-    def _getResourcesForRequest(self, txn, request):
-        """
-        Find the resources associated with the request. This is used when a JSON request has been received
-        and the underlying store objects the request refers to need to be found.
-
-        @param request: request arguments
-        @type request: C{dict}
-        """
-
-        # Get a share
-        ownerHome = yield txn.homeWithUID(request["type"], request["owner"])
-        if ownerHome is None or ownerHome.external():
-            FailedCrossPodRequestError("Invalid owner UID specified")
-
-        shareeHome = yield txn.homeWithUID(request["type"], request["sharee"])
-        if shareeHome is None or not shareeHome.external():
-            FailedCrossPodRequestError("Invalid sharee UID specified")
-
-        shareeView = yield shareeHome.childWithID(request["owner_id"])
-        if shareeView is None:
-            FailedCrossPodRequestError("Invalid shared resource specified")
-
-        resourceID = request.get("resource_id", None)
-        if resourceID is not None:
-            objectResource = yield shareeView.objectResourceWithID(resourceID)
-            if objectResource is None:
-                FailedCrossPodRequestError("Invalid owner shared object resource specified")
-        else:
-            objectResource = None
-
-        returnValue((shareeView, objectResource,))

Modified: CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -17,10 +17,9 @@
 from twisted.internet.defer import inlineCallbacks
 
 from txdav.common.datastore.podding.base import FailedCrossPodRequestError
-from txdav.common.datastore.podding.sharing_base import SharingCommonPoddingConduit
 
 
-class SharingInvitesPoddingConduitMixin(SharingCommonPoddingConduit):
+class SharingInvitesConduitMixin(object):
     """
     Defines the cross-pod API for sharing invites that will be mixed into the
     L{PoddingConduit} class.

Deleted: CalendarServer/trunk/txdav/common/datastore/podding/sharing_store.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/sharing_store.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/podding/sharing_store.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -1,243 +0,0 @@
-##
-# Copyright (c) 2013-2015 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-from txdav.common.datastore.podding.sharing_base import SharingCommonPoddingConduit
-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
-
-from twistedcaldav.caldavxml import TimeRange
-
-
-class SharingStorePoddingConduitMixin(SharingCommonPoddingConduit):
-    """
-    Defines the cross-pod API for access to shared resource data that will be mixed into the
-    L{PoddingConduit} class.
-    """
-
-    #
-    # Simple calls are ones where there is no argument and a single return value. We can simplify
-    # code generation for these by dynamically generating the appropriate class methods.
-    #
-
-    @inlineCallbacks
-    def _simple_send(self, actionName, shareeView, objectResource=None, transform=None, args=None, kwargs=None):
-        """
-        A simple send operation that returns a value.
-
-        @param actionName: name of the action.
-        @type actionName: C{str}
-        @param shareeView: sharee resource being operated on.
-        @type shareeView: L{CommonHomeChildExternal}
-        @param objectResource: the resource being operated on, or C{None} for classmethod.
-        @type objectResource: L{CommonObjectResourceExternal}
-        @param transform: a function used to convert the JSON response into return values.
-        @type transform: C{callable}
-        @param args: list of optional arguments.
-        @type args: C{list}
-        @param kwargs: optional keyword arguments.
-        @type kwargs: C{dict}
-        """
-
-        request, recipient = yield self._getRequestForResource(actionName, shareeView, objectResource)
-        if args is not None:
-            request["arguments"] = args
-        if kwargs is not None:
-            request["keywords"] = kwargs
-        response = yield self.sendRequest(shareeView._txn, recipient, request)
-        returnValue(response if transform is None else transform(response, shareeView, objectResource))
-
-
-    @inlineCallbacks
-    def _simple_recv(self, txn, actionName, request, method, onHomeChild=True, transform=None):
-        """
-        A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
-        and include those only if present.
-
-        @param actionName: name of the action.
-        @type actionName: C{str}
-        @param request: request arguments
-        @type request: C{dict}
-        @param method: name of the method to execute on the shared resource to get the result.
-        @type method: C{str}
-        @param transform: method to call on returned JSON value to convert it to something useful.
-        @type transform: C{callable}
-        """
-
-        shareeView, objectResource = yield self._getResourcesForRequest(txn, request)
-        if onHomeChild:
-            # Operate on the L{CommonHomeChild}
-            value = yield getattr(shareeView, method)(*request.get("arguments", ()), **request.get("keywords", {}))
-        else:
-            # Operate on the L{CommonObjectResource}
-            if objectResource is not None:
-                value = yield getattr(objectResource, method)(*request.get("arguments", ()), **request.get("keywords", {}))
-            else:
-                # classmethod call
-                value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *request.get("arguments", ()), **request.get("keywords", {}))
-
-        returnValue(transform(value, shareeView, objectResource) if transform is not None else value)
-
-
-    @inlineCallbacks
-    def send_freebusy(
-        self,
-        calresource,
-        timerange,
-        matchtotal,
-        excludeuid,
-        organizer,
-        organizerPrincipal,
-        same_calendar_user,
-        servertoserver,
-        event_details,
-    ):
-        """
-        Request free busy information for a shared calendar collection hosted on a different pod. See
-        L{txdav.caldav.datastore.scheduling.freebusy} for the base free busy lookup behavior.
-        """
-        action, recipient = yield self._getRequestForResource("freebusy", calresource)
-        action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
-        action["matchtotal"] = matchtotal
-        action["excludeuid"] = excludeuid
-        action["organizer"] = organizer
-        action["organizerPrincipal"] = organizerPrincipal
-        action["same_calendar_user"] = same_calendar_user
-        action["servertoserver"] = servertoserver
-        action["event_details"] = event_details
-
-        response = yield self.sendRequest(calresource._txn, recipient, action)
-        returnValue((response["fbresults"], response["matchtotal"],))
-
-
-    @inlineCallbacks
-    def recv_freebusy(self, txn, request):
-        """
-        Process a freebusy cross-pod request. Message arguments as per L{send_freebusy}.
-
-        @param request: request arguments
-        @type request: C{dict}
-        """
-
-        # Operate on the L{CommonHomeChild}
-        shareeView, _ignore_objectResource = yield self._getResourcesForRequest(txn, request)
-
-        fbinfo = [[], [], []]
-        matchtotal = yield generateFreeBusyInfo(
-            shareeView,
-            fbinfo,
-            TimeRange(start=request["timerange"][0], end=request["timerange"][1]),
-            request["matchtotal"],
-            request["excludeuid"],
-            request["organizer"],
-            request["organizerPrincipal"],
-            request["same_calendar_user"],
-            request["servertoserver"],
-            request["event_details"],
-            logItems=None
-        )
-
-        # Convert L{DateTime} objects to text for JSON response
-        for i in range(3):
-            for j in range(len(fbinfo[i])):
-                fbinfo[i][j] = fbinfo[i][j].getText()
-
-        returnValue({
-            "fbresults": fbinfo,
-            "matchtotal": matchtotal,
-        })
-
-
-    #
-    # Methods used to transform arguments to or results from a JSON request or response.
-    #
-    @staticmethod
-    def _to_tuple(value, shareeView, objectResource):
-        return tuple(value)
-
-
-    @staticmethod
-    def _to_string(value, shareeView, objectResource):
-        return str(value)
-
-
-    @staticmethod
-    def _to_externalize(value, shareeView, objectResource):
-        """
-        Convert the value to the external (JSON-based) representation.
-        """
-        if isinstance(value, shareeView._objectResourceClass):
-            value = value.externalize()
-        elif value is not None:
-            value = [v.externalize() for v in value]
-        return value
-
-
-    #
-    # Factory methods for binding actions to the conduit class
-    #
-    @classmethod
-    def _make_simple_homechild_action(cls, action, method, transform_recv=None, transform_send=None):
-        setattr(
-            cls,
-            "send_{}".format(action),
-            lambda self, shareeView, *args, **kwargs:
-                self._simple_send(action, shareeView, transform=transform_send, args=args, kwargs=kwargs)
-        )
-        setattr(
-            cls,
-            "recv_{}".format(action),
-            lambda self, txn, message:
-                self._simple_recv(txn, action, message, method, transform=transform_recv)
-        )
-
-
-    @classmethod
-    def _make_simple_object_action(cls, action, method, transform_recv=None, transform_send=None):
-        setattr(
-            cls,
-            "send_{}".format(action),
-            lambda self, shareeView, objectResource, *args, **kwargs:
-                self._simple_send(action, shareeView, objectResource, transform=transform_send, args=args, kwargs=kwargs)
-        )
-        setattr(
-            cls,
-            "recv_{}".format(action),
-            lambda self, txn, message:
-                self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_recv)
-        )
-
-# These are the actions on store objects we need to expose via the conduit api
-
-# Calls on L{CommonHomeChild} objects
-SharingStorePoddingConduitMixin._make_simple_homechild_action("countobjects", "countObjectResources")
-SharingStorePoddingConduitMixin._make_simple_homechild_action("listobjects", "listObjectResources")
-SharingStorePoddingConduitMixin._make_simple_homechild_action("resourceuidforname", "resourceUIDForName")
-SharingStorePoddingConduitMixin._make_simple_homechild_action("resourcenameforuid", "resourceNameForUID")
-SharingStorePoddingConduitMixin._make_simple_homechild_action("movehere", "moveObjectResourceHere")
-SharingStorePoddingConduitMixin._make_simple_homechild_action("moveaway", "moveObjectResourceAway")
-SharingStorePoddingConduitMixin._make_simple_homechild_action("synctoken", "syncToken")
-SharingStorePoddingConduitMixin._make_simple_homechild_action("resourcenamessincerevision", "resourceNamesSinceRevision", transform_send=SharingStorePoddingConduitMixin._to_tuple)
-SharingStorePoddingConduitMixin._make_simple_homechild_action("search", "search")
-
-# Calls on L{CommonObjectResource} objects
-SharingStorePoddingConduitMixin._make_simple_object_action("loadallobjects", "loadAllObjects", transform_recv=SharingStorePoddingConduitMixin._to_externalize)
-SharingStorePoddingConduitMixin._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_recv=SharingStorePoddingConduitMixin._to_externalize)
-SharingStorePoddingConduitMixin._make_simple_object_action("objectwith", "objectWith", transform_recv=SharingStorePoddingConduitMixin._to_externalize)
-SharingStorePoddingConduitMixin._make_simple_object_action("create", "create", transform_recv=SharingStorePoddingConduitMixin._to_externalize)
-SharingStorePoddingConduitMixin._make_simple_object_action("setcomponent", "setComponent")
-SharingStorePoddingConduitMixin._make_simple_object_action("component", "component", transform_recv=SharingStorePoddingConduitMixin._to_string)
-SharingStorePoddingConduitMixin._make_simple_object_action("remove", "remove")

Added: CalendarServer/trunk/txdav/common/datastore/podding/store_api.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/store_api.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/store_api.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -0,0 +1,368 @@
+##
+# Copyright (c) 2013-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from txdav.common.datastore.podding.base import FailedCrossPodRequestError
+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+
+from twistedcaldav.caldavxml import TimeRange
+
+
+class StoreAPIConduitMixin(object):
+    """
+    Defines common cross-pod API for generic access to remote resources.
+    """
+
+    #
+    # Utility methods to map from store objects to/from JSON
+    #
+
+    @inlineCallbacks
+    def _getRequestForStoreObject(self, action, storeObject, classMethod):
+        """
+        Create the JSON data needed to identify the remote resource by type and ids, along with any parent resources.
+
+        @param action: the conduit action name
+        @type action: L{str}
+        @param storeObject: the store object that is being operated on
+        @type storeObject: L{object}
+        @param classMethod: indicates whether the method being called is a classmethod
+        @type classMethod: L{bool}
+
+        @return: the transaction in use, the JSON dict to send in the request,
+            the server where the request should be sent
+        @rtype: L{tuple} of (L{CommonStoreTransaction}, L{dict}, L{str})
+        """
+
+        from txdav.common.datastore.sql import CommonObjectResource, CommonHomeChild, CommonHome
+        result = {
+            "action": action,
+        }
+
+        # Extract the relevant store objects
+        txn = storeObject._txn
+        owner_home = None
+        viewer_home = None
+        home_child = None
+        object_resource = None
+        if isinstance(storeObject, CommonObjectResource):
+            owner_home = storeObject.ownerHome()
+            viewer_home = storeObject.viewerHome()
+            home_child = storeObject.parentCollection()
+            object_resource = storeObject
+        elif isinstance(storeObject, CommonHomeChild):
+            owner_home = storeObject.ownerHome()
+            viewer_home = storeObject.viewerHome()
+            home_child = storeObject
+            result["classMethod"] = classMethod
+        elif isinstance(storeObject, CommonHome):
+            owner_home = storeObject
+            viewer_home = storeObject
+            txn = storeObject._txn
+            result["classMethod"] = classMethod
+
+        # Add store object identities to JSON request
+        result["homeType"] = viewer_home._homeType
+        result["homeUID"] = viewer_home.uid()
+        if home_child:
+            if home_child.owned():
+                result["homeChildID"] = home_child.id()
+            else:
+                result["homeChildSharedID"] = home_child.name()
+        if object_resource:
+            result["objectResourceID"] = object_resource.id()
+
+        # Note that the owner_home is always the ownerHome() because in the sharing case
+        # a viewer is accessing the owner's data on another pod.
+        recipient = yield self.store.directoryService().recordWithUID(owner_home.uid())
+
+        returnValue((txn, result, recipient.server(),))
+
+
+    @inlineCallbacks
+    def _getStoreObjectForRequest(self, txn, request):
+        """
+        Resolve the supplied JSON data to get a store object to operate on.
+        """
+
+        returnObject = txn
+        classObject = None
+
+        if "homeUID" in request:
+            home = yield txn.homeWithUID(request["homeType"], request["homeUID"])
+            if home is None:
+                raise FailedCrossPodRequestError("Invalid owner UID specified")
+            home._internalRequest = False
+            returnObject = home
+            if request.get("classMethod", False):
+                classObject = home._childClass
+
+        if "homeChildID" in request:
+            homeChild = yield home.childWithID(request["homeChildID"])
+            if homeChild is None:
+                raise FailedCrossPodRequestError("Invalid home child specified")
+            returnObject = homeChild
+            if request.get("classMethod", False):
+                classObject = homeChild._objectResourceClass
+        elif "homeChildSharedID" in request:
+            homeChild = yield home.childWithName(request["homeChildSharedID"])
+            if homeChild is None:
+                raise FailedCrossPodRequestError("Invalid home child specified")
+            returnObject = homeChild
+            if request.get("classMethod", False):
+                classObject = homeChild._objectResourceClass
+
+        if "objectResourceID" in request:
+            objectResource = yield homeChild.objectResourceWithID(request["objectResourceID"])
+            if objectResource is None:
+                raise FailedCrossPodRequestError("Invalid object resource specified")
+            returnObject = objectResource
+
+        returnValue((returnObject, classObject,))
+
+
+    @inlineCallbacks
+    def send_home_resource_id(self, txn, recipient):
+        """
+        Lookup the remote resourceID matching the specified directory uid.
+
+        @param ownerUID: directory record for user whose home is needed
+        @type ownerUID: L{DirectroryRecord}
+        """
+
+        request = {
+            "action": "home-resource_id",
+            "ownerUID": recipient.uid,
+        }
+
+        response = yield self.sendRequest(txn, recipient, request)
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def recv_home_resource_id(self, txn, request):
+        """
+        Process an addAttachment cross-pod request. Request arguments as per L{send_add_attachment}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        home = yield txn.calendarHomeWithUID(request["ownerUID"])
+        returnValue(home.id() if home is not None else None)
+
+
+    @inlineCallbacks
+    def send_freebusy(
+        self,
+        calresource,
+        timerange,
+        matchtotal,
+        excludeuid,
+        organizer,
+        organizerPrincipal,
+        same_calendar_user,
+        servertoserver,
+        event_details,
+    ):
+        """
+        Request free busy information for a shared calendar collection hosted on a different pod. See
+        L{txdav.caldav.datastore.scheduling.freebusy} for the base free busy lookup behavior.
+        """
+        txn, request, server = yield self._getRequestForStoreObject("freebusy", calresource, False)
+
+        request["timerange"] = [timerange.start.getText(), timerange.end.getText()]
+        request["matchtotal"] = matchtotal
+        request["excludeuid"] = excludeuid
+        request["organizer"] = organizer
+        request["organizerPrincipal"] = organizerPrincipal
+        request["same_calendar_user"] = same_calendar_user
+        request["servertoserver"] = servertoserver
+        request["event_details"] = event_details
+
+        response = yield self.sendRequestToServer(txn, server, request)
+        returnValue((response["fbresults"], response["matchtotal"],))
+
+
+    @inlineCallbacks
+    def recv_freebusy(self, txn, request):
+        """
+        Process a freebusy cross-pod request. Message arguments as per L{send_freebusy}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        # Operate on the L{CommonHomeChild}
+        calresource, _ignore = yield self._getStoreObjectForRequest(txn, request)
+
+        fbinfo = [[], [], []]
+        matchtotal = yield generateFreeBusyInfo(
+            calresource,
+            fbinfo,
+            TimeRange(start=request["timerange"][0], end=request["timerange"][1]),
+            request["matchtotal"],
+            request["excludeuid"],
+            request["organizer"],
+            request["organizerPrincipal"],
+            request["same_calendar_user"],
+            request["servertoserver"],
+            request["event_details"],
+            logItems=None
+        )
+
+        # Convert L{DateTime} objects to text for JSON response
+        for i in range(3):
+            for j in range(len(fbinfo[i])):
+                fbinfo[i][j] = fbinfo[i][j].getText()
+
+        returnValue({
+            "fbresults": fbinfo,
+            "matchtotal": matchtotal,
+        })
+
+
+    #
+    # We can simplify code generation for simple calls by dynamically generating the appropriate class methods.
+    #
+
+    @inlineCallbacks
+    def _simple_object_send(self, actionName, storeObject, classMethod=False, transform=None, args=None, kwargs=None):
+        """
+        A simple send operation that returns a value.
+
+        @param actionName: name of the action.
+        @type actionName: C{str}
+        @param shareeView: sharee resource being operated on.
+        @type shareeView: L{CommonHomeChildExternal}
+        @param objectResource: the resource being operated on, or C{None} for classmethod.
+        @type objectResource: L{CommonObjectResourceExternal}
+        @param transform: a function used to convert the JSON response into return values.
+        @type transform: C{callable}
+        @param args: list of optional arguments.
+        @type args: C{list}
+        @param kwargs: optional keyword arguments.
+        @type kwargs: C{dict}
+        """
+
+        txn, request, server = yield self._getRequestForStoreObject(actionName, storeObject, classMethod)
+        if args is not None:
+            request["arguments"] = args
+        if kwargs is not None:
+            request["keywords"] = kwargs
+        response = yield self.sendRequestToServer(txn, server, request)
+        returnValue(transform(response) if transform is not None else response)
+
+
+    @inlineCallbacks
+    def _simple_object_recv(self, txn, actionName, request, method, transform=None):
+        """
+        A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
+        and include those only if present.
+
+        @param actionName: name of the action.
+        @type actionName: C{str}
+        @param request: request arguments
+        @type request: C{dict}
+        @param method: name of the method to execute on the shared resource to get the result.
+        @type method: C{str}
+        @param transform: method to call on returned JSON value to convert it to something useful.
+        @type transform: C{callable}
+        """
+
+        storeObject, classObject = yield self._getStoreObjectForRequest(txn, request)
+        if classObject is not None:
+            value = yield getattr(classObject, method)(storeObject, *request.get("arguments", ()), **request.get("keywords", {}))
+        else:
+            value = yield getattr(storeObject, method)(*request.get("arguments", ()), **request.get("keywords", {}))
+
+        returnValue(transform(value) if transform is not None else value)
+
+
+    #
+    # Factory methods for binding actions to the conduit class
+    #
+    @classmethod
+    def _make_simple_action(cls, action, method, classMethod=False, transform_recv_result=None, transform_send_result=None):
+        setattr(
+            cls,
+            "send_{}".format(action),
+            lambda self, storeObject, *args, **kwargs:
+                self._simple_object_send(action, storeObject, classMethod=classMethod, transform=transform_send_result, args=args, kwargs=kwargs)
+        )
+        setattr(
+            cls,
+            "recv_{}".format(action),
+            lambda self, txn, message:
+                self._simple_object_recv(txn, action, message, method, transform=transform_recv_result)
+        )
+
+
+    #
+    # Transforms for returned data
+    #
+    @staticmethod
+    def _to_externalize(value):
+        """
+        Convert the value to the external (JSON-based) representation.
+        """
+        return value.externalize() if value is not None else None
+
+
+    @staticmethod
+    def _to_externalize_list(value):
+        """
+        Convert the value to the external (JSON-based) representation.
+        """
+        return [v.externalize() for v in value]
+
+
+    @staticmethod
+    def _to_string(value):
+        return str(value)
+
+
+    @staticmethod
+    def _to_tuple(value):
+        return tuple(value)
+
+# These are the actions on store objects we need to expose via the conduit api
+
+# Calls on L{CommonHome} objects
+
+# Calls on L{CommonHomeChild} objects
+StoreAPIConduitMixin._make_simple_action("homechild_listobjects", "listObjects", classMethod=True)
+StoreAPIConduitMixin._make_simple_action("homechild_loadallobjects", "loadAllObjects", classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize_list)
+StoreAPIConduitMixin._make_simple_action("homechild_objectwith", "objectWith", classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize)
+StoreAPIConduitMixin._make_simple_action("homechild_movehere", "moveObjectResourceHere")
+StoreAPIConduitMixin._make_simple_action("homechild_moveaway", "moveObjectResourceAway")
+StoreAPIConduitMixin._make_simple_action("homechild_synctoken", "syncToken")
+StoreAPIConduitMixin._make_simple_action("homechild_resourcenamessincerevision", "resourceNamesSinceRevision", transform_send_result=StoreAPIConduitMixin._to_tuple)
+StoreAPIConduitMixin._make_simple_action("homechild_search", "search")
+
+# Calls on L{CommonObjectResource} objects
+StoreAPIConduitMixin._make_simple_action("objectresource_loadallobjects", "loadAllObjects", classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize_list)
+StoreAPIConduitMixin._make_simple_action("objectresource_loadallobjectswithnames", "loadAllObjectsWithNames", classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize_list)
+StoreAPIConduitMixin._make_simple_action("objectresource_listobjects", "listObjects", classMethod=True)
+StoreAPIConduitMixin._make_simple_action("objectresource_countobjects", "countObjects", classMethod=True)
+StoreAPIConduitMixin._make_simple_action("objectresource_objectwith", "objectWith", classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize)
+StoreAPIConduitMixin._make_simple_action("objectresource_resourcenameforuid", "resourceNameForUID", classMethod=True)
+StoreAPIConduitMixin._make_simple_action("objectresource_resourceuidforname", "resourceUIDForName", classMethod=True)
+StoreAPIConduitMixin._make_simple_action("objectresource_create", "create", classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize)
+StoreAPIConduitMixin._make_simple_action("objectresource_setcomponent", "setComponent")
+StoreAPIConduitMixin._make_simple_action("objectresource_component", "component", transform_recv_result=StoreAPIConduitMixin._to_string)
+StoreAPIConduitMixin._make_simple_action("objectresource_remove", "remove")

Modified: CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -168,17 +168,15 @@
         Cross-pod request works when conduit does support the action.
         """
 
-        txn = self.transactionUnderTest()
-        store1 = self.storeUnderTest()
-        response = yield store1.conduit.send_fake(txn, "user01", "puser01")
+        store = self.theStoreUnderTest(0)
+        response = yield store.conduit.send_fake(self.theTransactionUnderTest(0), "user01", "puser01")
         self.assertEqual(response, {"back2u": "bravo", "more": "bits"})
-        yield txn.commit()
+        yield self.commitTransaction(0)
 
-        store2 = self.otherStoreUnderTest()
-        txn = store2.newTransaction()
-        response = yield store2.conduit.send_fake(txn, "puser01", "user01")
+        store = self.theStoreUnderTest(1)
+        response = yield store.conduit.send_fake(self.theTransactionUnderTest(1), "puser01", "user01")
         self.assertEqual(response, {"back2u": "bravo", "more": "bits"})
-        yield txn.commit()
+        yield self.commitTransaction(1)
 
 
 
@@ -261,23 +259,23 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         shared = yield calendar1.shareeView("puser01")
         self.assertEqual(shared.shareStatus(), _BIND_STATUS_ACCEPTED)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         self.assertTrue(shared is not None)
         self.assertTrue(shared.external())
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.uninviteUIDFromShare("puser01")
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         self.assertTrue(shared is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -288,33 +286,33 @@
 
         yield self.createShare("user01", "puser01")
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         count = yield shared.countObjectResources()
         self.assertEqual(count, 0)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
         count = yield calendar1.countObjectResources()
         self.assertEqual(count, 1)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         count = yield shared.countObjectResources()
         self.assertEqual(count, 1)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         yield object1.remove()
         count = yield calendar1.countObjectResources()
         self.assertEqual(count, 0)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         count = yield shared.countObjectResources()
         self.assertEqual(count, 0)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -325,34 +323,34 @@
 
         yield self.createShare("user01", "puser01")
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         objects = yield shared.listObjectResources()
         self.assertEqual(set(objects), set())
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
         yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
         objects = yield calendar1.listObjectResources()
         self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         objects = yield shared.listObjectResources()
         self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         yield object1.remove()
         objects = yield calendar1.listObjectResources()
         self.assertEqual(set(objects), set(("2.ics",)))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         objects = yield shared.listObjectResources()
         self.assertEqual(set(objects), set(("2.ics",)))
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -363,45 +361,45 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         token1_1 = yield calendar1.syncToken()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         token2_1 = yield shared.syncToken()
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         self.assertEqual(token1_1, token2_1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         token1_2 = yield calendar1.syncToken()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         token2_2 = yield shared.syncToken()
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         self.assertNotEqual(token1_1, token1_2)
         self.assertEqual(token1_2, token2_2)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         yield object1.remove()
         count = yield calendar1.countObjectResources()
         self.assertEqual(count, 0)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         token1_3 = yield calendar1.syncToken()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         token2_3 = yield shared.syncToken()
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         self.assertNotEqual(token1_1, token1_3)
         self.assertNotEqual(token1_2, token1_3)
@@ -416,58 +414,58 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         token1_1 = yield calendar1.syncToken()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         token2_1 = yield shared.syncToken()
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         token1_2 = yield calendar1.syncToken()
         names1 = yield calendar1.resourceNamesSinceToken(token1_1)
         self.assertEqual(names1, ([u"1.ics"], [], [],))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         token2_2 = yield shared.syncToken()
         names2 = yield shared.resourceNamesSinceToken(token2_1)
         self.assertEqual(names2, ([u"1.ics"], [], [],))
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         yield object1.remove()
         count = yield calendar1.countObjectResources()
         self.assertEqual(count, 0)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         token1_3 = yield calendar1.syncToken()
         names1 = yield calendar1.resourceNamesSinceToken(token1_2)
         self.assertEqual(names1, ([], [u"1.ics"], [],))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         token2_3 = yield shared.syncToken()
         names2 = yield shared.resourceNamesSinceToken(token2_2)
         self.assertEqual(names2, ([], [u"1.ics"], [],))
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         names1 = yield calendar1.resourceNamesSinceToken(token1_3)
         self.assertEqual(names1, ([], [], [],))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         names2 = yield shared.resourceNamesSinceToken(token2_3)
         self.assertEqual(names2, ([], [], [],))
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -478,23 +476,23 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         uid = yield calendar1.resourceUIDForName("1.ics")
         self.assertEqual(uid, "uid1")
         uid = yield calendar1.resourceUIDForName("2.ics")
         self.assertTrue(uid is None)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         uid = yield shared.resourceUIDForName("1.ics")
         self.assertEqual(uid, "uid1")
         uid = yield shared.resourceUIDForName("2.ics")
         self.assertTrue(uid is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -505,23 +503,23 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         name = yield calendar1.resourceNameForUID("uid1")
         self.assertEqual(name, "1.ics")
         name = yield calendar1.resourceNameForUID("uid2")
         self.assertTrue(name is None)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         name = yield shared.resourceNameForUID("uid1")
         self.assertEqual(name, "1.ics")
         name = yield shared.resourceNameForUID("uid2")
         self.assertTrue(name is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -532,9 +530,9 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         filter = caldavxml.Filter(
             caldavxml.ComponentFilter(
@@ -546,15 +544,15 @@
         )
         filter = Filter(filter)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         names = [item[0] for item in (yield calendar1.search(filter))]
         self.assertEqual(names, ["1.ics", ])
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         names = [item[0] for item in (yield shared.search(filter))]
         self.assertEqual(names, ["1.ics", ])
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -565,14 +563,14 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
         resource_id1 = resource1.id()
         resource2 = yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
         resource_id2 = resource2.id()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resources = yield shared.objectResources()
         byname = dict([(obj.name(), obj) for obj in resources])
         byuid = dict([(obj.uid(), obj) for obj in resources])
@@ -598,14 +596,13 @@
         self.assertTrue(resource is byname["2.ics"])
         resource = yield shared.objectResourceWithID(0)
         self.assertTrue(resource is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         yield object1.remove()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resources = yield shared.objectResources()
         byname = dict([(obj.name(), obj) for obj in resources])
         byuid = dict([(obj.uid(), obj) for obj in resources])
@@ -631,7 +628,7 @@
         self.assertTrue(resource is byname["2.ics"])
         resource = yield shared.objectResourceWithID(0)
         self.assertTrue(resource is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -642,20 +639,20 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
         resource_id1 = resource1.id()
         yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
         resource3 = yield calendar1.createCalendarObjectWithName("3.ics", Component.fromString(self.caldata3))
         resource_id3 = resource3.id()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resources = yield shared.objectResources()
         self.assertEqual(len(resources), 3)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
         byname = dict([(obj.name(), obj) for obj in resources])
         byuid = dict([(obj.uid(), obj) for obj in resources])
@@ -681,14 +678,13 @@
         self.assertTrue(resource is byname["3.ics"])
         resource = yield shared.objectResourceWithID(0)
         self.assertTrue(resource is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         yield object1.remove()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
         byname = dict([(obj.name(), obj) for obj in resources])
         byuid = dict([(obj.uid(), obj) for obj in resources])
@@ -714,7 +710,7 @@
         self.assertTrue(resource is byname["3.ics"])
         resource = yield shared.objectResourceWithID(0)
         self.assertTrue(resource is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -725,12 +721,12 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         resource = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
         resource_id = resource.id()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resource = yield shared.objectResourceWithName("1.ics")
         self.assertTrue(resource is not None)
         self.assertEqual(resource.name(), "1.ics")
@@ -739,9 +735,9 @@
         resource = yield shared.objectResourceWithName("2.ics")
         self.assertTrue(resource is None)
 
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resource = yield shared.objectResourceWithUID("uid1")
         self.assertTrue(resource is not None)
         self.assertEqual(resource.name(), "1.ics")
@@ -750,9 +746,9 @@
         resource = yield shared.objectResourceWithUID("uid2")
         self.assertTrue(resource is None)
 
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resource = yield shared.objectResourceWithID(resource_id)
         self.assertTrue(resource is not None)
         self.assertEqual(resource.name(), "1.ics")
@@ -761,27 +757,26 @@
         resource = yield shared.objectResourceWithID(0)
         self.assertTrue(resource is None)
 
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         yield object1.remove()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resource = yield shared.objectResourceWithName("1.ics")
         self.assertTrue(resource is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resource = yield shared.objectResourceWithUID("uid1")
         self.assertTrue(resource is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resource = yield shared.objectResourceWithID(resource_id)
         self.assertTrue(resource is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
@@ -792,29 +787,29 @@
 
         yield self.createShare("user01", "puser01")
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resource = yield shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
         resource_id = resource.id()
         self.assertTrue(resource is not None)
         self.assertEqual(resource.name(), "1.ics")
         self.assertEqual(resource.uid(), "uid1")
         self.assertFalse(resource._componentChanged)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         resource = yield shared.objectResourceWithUID("uid1")
         self.assertTrue(resource is not None)
         self.assertEqual(resource.name(), "1.ics")
         self.assertEqual(resource.uid(), "uid1")
         self.assertEqual(resource.id(), resource_id)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         self.assertTrue(object1 is not None)
         self.assertEqual(object1.name(), "1.ics")
         self.assertEqual(object1.uid(), "uid1")
         self.assertEqual(object1.id(), resource_id)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
 
     @inlineCallbacks
@@ -825,17 +820,17 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         yield self.failUnlessFailure(shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)), ObjectResourceNameAlreadyExistsError)
-        yield self.otherAbort()
+        yield self.abortTransaction(1)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
         yield self.failUnlessFailure(shared.createCalendarObjectWithName(".2.ics", Component.fromString(self.caldata2)), ObjectResourceNameNotAllowedError)
-        yield self.otherAbort()
+        yield self.abortTransaction(1)
 
 
     @inlineCallbacks
@@ -846,29 +841,29 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
         ical = yield shared_object.component()
         self.assertTrue(isinstance(ical, Component))
         self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
         changed = yield shared_object.setComponent(Component.fromString(self.caldata1_changed))
         self.assertFalse(changed)
         ical = yield shared_object.component()
         self.assertTrue(isinstance(ical, Component))
         self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         ical = yield object1.component()
         self.assertTrue(isinstance(ical, Component))
         self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
 
     @inlineCallbacks
@@ -879,40 +874,40 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
         ical = yield shared_object.component()
         self.assertTrue(isinstance(ical, Component))
         self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     @inlineCallbacks
     def test_remove(self):
         """
-        Test that action=create works.
+        Test that action=remove works.
         """
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
         yield shared_object.remove()
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
         self.assertTrue(shared_object is None)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         self.assertTrue(object1 is None)
-        yield self.commit()
+        yield self.commitTransaction(0)
 
 
     @inlineCallbacks
@@ -923,14 +918,14 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         fbstart = "{now:04d}0102T000000Z".format(**self.nowYear)
         fbend = "{now:04d}0103T000000Z".format(**self.nowYear)
 
-        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
 
         fbinfo = [[], [], []]
         matchtotal = yield generateFreeBusyInfo(
@@ -951,7 +946,7 @@
         self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
         self.assertEqual(len(fbinfo[1]), 0)
         self.assertEqual(len(fbinfo[2]), 0)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
 
     def attachmentToString(self, attachment):
@@ -977,26 +972,26 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         object1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
         resourceID = object1.id()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
         attachment, location = yield shared_object.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
         managedID = attachment.managedID()
         from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
         self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
         self.assertTrue("user01/attachments/test" in location)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
+        cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID)
         self.assertEqual(cobjs, set((resourceID,)))
-        attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
+        attachment = yield ManagedAttachment.load(self.theTransactionUnderTest(0), resourceID, managedID)
         self.assertEqual(attachment.name(), "test.txt")
         data = yield self.attachmentToString(attachment)
         self.assertEqual(data, "Here is some text.")
-        yield self.commit()
+        yield self.commitTransaction(0)
 
 
     @inlineCallbacks
@@ -1007,31 +1002,31 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         resourceID = object1.id()
         attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
         managedID = attachment.managedID()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
         attachment, location = yield shared_object.updateAttachment(managedID, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some more text."))
         managedID = attachment.managedID()
         from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal
         self.assertTrue(isinstance(attachment, ManagedAttachmentExternal))
         self.assertTrue("user01/attachments/test" in location)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
+        cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID)
         self.assertEqual(cobjs, set((resourceID,)))
         attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
         self.assertEqual(attachment.name(), "test.txt")
         data = yield self.attachmentToString(attachment)
         self.assertEqual(data, "Here is some more text.")
-        yield self.commit()
+        yield self.commitTransaction(0)
 
 
     @inlineCallbacks
@@ -1042,22 +1037,22 @@
 
         yield self.createShare("user01", "puser01")
 
-        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
         yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics")
         resourceID = object1.id()
         attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text."))
         managedID = attachment.managedID()
-        yield self.commit()
+        yield self.commitTransaction(0)
 
-        shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics")
         yield shared_object.removeAttachment(None, managedID)
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
-        cobjs = yield ManagedAttachment.referencesTo(self.transactionUnderTest(), managedID)
+        cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID)
         self.assertEqual(cobjs, set())
-        attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID)
+        attachment = yield ManagedAttachment.load(self.theTransactionUnderTest(0), resourceID, managedID)
         self.assertTrue(attachment is None)
-        yield self.commit()
+        yield self.commitTransaction(0)

Added: CalendarServer/trunk/txdav/common/datastore/podding/test/test_store_api.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_store_api.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_store_api.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -0,0 +1,520 @@
+##
+# Copyright (c) 2005-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest
+from pycalendar.datetime import DateTime
+from twistedcaldav.ical import Component, normalize_iCalStr
+from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError, \
+    InvalidUIDError
+
+
+class TestConduitAPI(MultiStoreConduitTest):
+    """
+    Test that the conduit api works.
+    """
+
+    nowYear = {"now": DateTime.getToday().getYear()}
+
+    caldata1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    caldata1_changed = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance changed
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    caldata1_failed = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1-failed
+DTSTART:{now:04d}0102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance changed
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    caldata2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid2
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    @inlineCallbacks
+    def _remoteHome(self, txn, uid):
+        """
+        Create a synthetic external home object that maps to the actual remote home.
+
+        @param ownerUID: directory uid of the user's home
+        @type ownerUID: L{str}
+        """
+
+        from txdav.caldav.datastore.sql_external import CalendarHomeExternal
+        recipient = yield txn.store().directoryService().recordWithUID(uid)
+        resourceID = yield txn.store().conduit.send_home_resource_id(self, recipient)
+        home = CalendarHomeExternal(txn, recipient.uid, resourceID) if resourceID is not None else None
+        if home:
+            home._childClass = home._childClass._externalClass
+        returnValue(home)
+
+
+    @inlineCallbacks
+    def test_remote_home(self):
+        """
+        Test that a remote home can be accessed.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        self.assertEqual(home.id(), home01.id())
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_homechild_listobjects(self):
+        """
+        Test that a remote home L{listChildren} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        children01 = yield home01.listChildren()
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        self.assertEqual(home.id(), home01.id())
+        children = yield home.listChildren()
+        self.assertEqual(set(children), set(children01))
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_homechild_loadallobjects(self):
+        """
+        Test that a remote home L{loadChildren} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        children01 = yield home01.loadChildren()
+        names01 = [child.name() for child in children01]
+        ids01 = [child.id() for child in children01]
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        self.assertEqual(home.id(), home01.id())
+        children = yield home.loadChildren()
+        names = [child.name() for child in children]
+        ids = [child.id() for child in children]
+        self.assertEqual(set(names), set(names01))
+        self.assertEqual(set(ids), set(ids01))
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_homechild_objectwith(self):
+        """
+        Test that a remote home L{loadChildren} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        self.assertEqual(calendar.id(), calendar01.id())
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_loadallobjects(self):
+        """
+        Test that a remote home child L{objectResources} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        objects = yield calendar.objectResources()
+        self.assertEqual(len(objects), 1)
+        self.assertEqual(objects[0].name(), "1.ics")
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_loadallobjectswithnames(self):
+        """
+        Test that a remote home child L{objectResourcesWithNames} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        objects = yield calendar.objectResourcesWithNames(("2.ics",))
+        self.assertEqual(len(objects), 1)
+        self.assertEqual(objects[0].name(), "2.ics")
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_listobjects(self):
+        """
+        Test that a remote home child L{listObjectResources} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        names = yield calendar.listObjectResources()
+        self.assertEqual(set(names), set(("1.ics", "2.ics",)))
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_countobjects(self):
+        """
+        Test that a remote home child L{countObjectResources} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        count = yield calendar.countObjectResources()
+        self.assertEqual(count, 2)
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_objectwith(self):
+        """
+        Test that a remote home child L{objectResourceWithName} and L{objectResourceWithUID} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        resource01 = yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+
+        resource = yield calendar.objectResourceWithName("2.ics")
+        self.assertEqual(resource.name(), "2.ics")
+
+        resource = yield calendar.objectResourceWithName("foo.ics")
+        self.assertEqual(resource, None)
+
+        resource = yield calendar.objectResourceWithUID("uid1")
+        self.assertEqual(resource.name(), "1.ics")
+
+        resource = yield calendar.objectResourceWithUID("foo")
+        self.assertEqual(resource, None)
+
+        resource = yield calendar.objectResourceWithID(resource01.id())
+        self.assertEqual(resource.name(), "1.ics")
+
+        resource = yield calendar.objectResourceWithID(12345)
+        self.assertEqual(resource, None)
+
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_resourcenameforuid(self):
+        """
+        Test that a remote home child L{resourceNameForUID} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+
+        name = yield calendar.resourceNameForUID("uid1")
+        self.assertEqual(name, "1.ics")
+
+        name = yield calendar.resourceNameForUID("uid2")
+        self.assertEqual(name, "2.ics")
+
+        name = yield calendar.resourceNameForUID("foo")
+        self.assertEqual(name, None)
+
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_resourceuidforname(self):
+        """
+        Test that a remote home child L{resourceUIDForName} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+
+        uid = yield calendar.resourceUIDForName("1.ics")
+        self.assertEqual(uid, "uid1")
+
+        uid = yield calendar.resourceUIDForName("2.ics")
+        self.assertEqual(uid, "uid2")
+
+        uid = yield calendar.resourceUIDForName("foo.ics")
+        self.assertEqual(uid, None)
+
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_create(self):
+        """
+        Test that a remote object resource L{create} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        yield home01.childWithName("calendar")
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        resource = yield calendar.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commitTransaction(1)
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        resource01 = yield calendar01.objectResourceWithName("1.ics")
+        self.assertEqual(resource01.id(), resource.id())
+        caldata = yield resource01.component()
+        self.assertEqual(str(caldata), self.caldata1)
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        resource = yield calendar.objectResourceWithName("1.ics")
+        caldata = yield resource.component()
+        self.assertEqual(str(caldata), self.caldata1)
+        yield self.commitTransaction(1)
+
+        # Recreate fails
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        self.assertFailure(
+            calendar.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)),
+            ObjectResourceNameAlreadyExistsError,
+        )
+        yield self.abortTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_setcomponent(self):
+        """
+        Test that a remote object resource L{setComponent} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        resource = yield calendar.objectResourceWithName("1.ics")
+        changed = yield resource.setComponent(Component.fromString(self.caldata1_changed))
+        self.assertFalse(changed)
+        caldata = yield resource.component()
+        self.assertEqual(normalize_iCalStr(str(caldata)), normalize_iCalStr(self.caldata1_changed))
+        yield self.commitTransaction(1)
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        resource01 = yield calendar01.objectResourceWithName("1.ics")
+        caldata = yield resource01.component()
+        self.assertEqual(normalize_iCalStr(str(caldata)), normalize_iCalStr(self.caldata1_changed))
+        yield self.commitTransaction(0)
+
+        # Fail to set with different UID
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        resource = yield calendar.objectResourceWithName("1.ics")
+        self.assertFailure(
+            resource.setComponent(Component.fromString(self.caldata1_failed)),
+            InvalidUIDError,
+        )
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_component(self):
+        """
+        Test that a remote object resource L{component} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+
+        resource = yield calendar.objectResourceWithName("1.ics")
+        caldata = yield resource.component()
+        self.assertEqual(str(caldata), self.caldata1)
+
+        resource = yield calendar.objectResourceWithName("2.ics")
+        caldata = yield resource.component()
+        self.assertEqual(str(caldata), self.caldata2)
+
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_objectresource_remove(self):
+        """
+        Test that a remote object resource L{component} works.
+        """
+
+        home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        self.assertTrue(home01 is not None)
+        calendar01 = yield home01.childWithName("calendar")
+        yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        resource = yield calendar.objectResourceWithName("1.ics")
+        yield resource.remove()
+        yield self.commitTransaction(1)
+
+        resource01 = yield self.calendarObjectUnderTest(
+            txn=self.theTransactionUnderTest(0),
+            home="user01",
+            calendar_name="calendar",
+            name="1.ics",
+        )
+        self.assertTrue(resource01 is None)
+        yield self.commitTransaction(0)
+
+        home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01")
+        self.assertTrue(home is not None)
+        calendar = yield home.childWithName("calendar")
+        resource = yield calendar.objectResourceWithName("1.ics")
+        self.assertTrue(resource is None)
+        yield self.commitTransaction(1)

Modified: CalendarServer/trunk/txdav/common/datastore/podding/test/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/util.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/util.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -105,59 +105,68 @@
 
 class MultiStoreConduitTest(CommonCommonTests, txweb2.dav.test.util.TestCase):
 
-    theStoreBuilder2 = SQLStoreBuilder(secondary=True)
-    otherTransaction = None
+    numberOfStores = 2
 
+    theStoreBuilders = []
+    theStores = []
+    activeTransactions = []
+
+    def __init__(self, methodName='runTest'):
+        txweb2.dav.test.util.TestCase.__init__(self, methodName)
+        while len(self.theStoreBuilders) < self.numberOfStores:
+            self.theStoreBuilders.append(SQLStoreBuilder(count=len(self.theStoreBuilders)))
+        self.theStores = [None] * self.numberOfStores
+        self.activeTransactions = [None] * self.numberOfStores
+
+
     @inlineCallbacks
     def setUp(self):
         yield super(MultiStoreConduitTest, self).setUp()
 
-        # Store 1
-        serversDB1 = ServersDB()
-        server1a = Server("A", "http://127.0.0.1:8008", "A", True)
-        serversDB1.addServer(server1a)
-        server1b = Server("B", "http://127.0.0.1:8108", "B", False)
-        serversDB1.addServer(server1b)
-        yield self.buildStoreAndDirectory(serversDB=serversDB1)
-        self.store.queryCacher = None     # Cannot use query caching
-        self.store.conduit = self.makeConduit(self.store)
+        # Stores
+        for i in range(self.numberOfStores):
+            serversDB = ServersDB()
+            for j in range(self.numberOfStores):
+                letter = chr(ord("A") + j)
+                port = 8008 + 100 * j
+                server = Server(letter, "http://127.0.0.1:{}".format(port), letter, j == i)
+                serversDB.addServer(server)
 
-        # Store 2
-        serversDB2 = ServersDB()
-        server2a = Server("A", "http://127.0.0.1:8008", "A", False)
-        serversDB2.addServer(server2a)
-        server2b = Server("B", "http://127.0.0.1:8108", "B", True)
-        serversDB2.addServer(server2b)
+            if i == 0:
+                yield self.buildStoreAndDirectory(
+                    serversDB=serversDB,
+                    storeBuilder=self.theStoreBuilders[i]
+                )
+                self.theStores[i] = self.store
+            else:
+                self.theStores[i] = yield self.buildStore(self.theStoreBuilders[i])
+                directory = buildTestDirectory(
+                    self.theStores[i], self.mktemp(), serversDB=serversDB
+                )
+                self.theStores[i].setDirectoryService(directory)
 
-        self.store2 = yield self.buildStore(self.theStoreBuilder2)
-        directory2 = buildTestDirectory(
-            self.store2, self.mktemp(), serversDB=serversDB2
-        )
+            self.theStores[i].queryCacher = None     # Cannot use query caching
+            self.theStores[i].conduit = self.makeConduit(self.theStores[i])
 
-        self.store2.setDirectoryService(directory2)
-        self.store2.queryCacher = None     # Cannot use query caching
-        self.store2.conduit = self.makeConduit(self.store2)
+            FakeConduitRequest.addServerStore(serversDB.getServerById(chr(ord("A") + i)), self.theStores[i])
 
-        FakeConduitRequest.addServerStore(server1a, self.store)
-        FakeConduitRequest.addServerStore(server2b, self.store2)
 
-
     def configure(self):
         super(MultiStoreConduitTest, self).configure()
         self.config.Servers.Enabled = True
 
 
-    def otherStoreUnderTest(self):
+    def theStoreUnderTest(self, count):
         """
         Return a store for testing.
         """
-        return self.store2
+        return self.theStores[count]
 
 
-    def newOtherTransaction(self):
-        assert self.otherTransaction is None
-        store2 = self.otherStoreUnderTest()
-        txn = store2.newTransaction()
+    def makeNewTransaction(self, count):
+        assert self.activeTransactions[count] is None
+        store = self.theStoreUnderTest(count)
+        txn = store.newTransaction()
 
         @inlineCallbacks
         def maybeCommitThis():
@@ -166,28 +175,28 @@
             except AlreadyFinishedError:
                 pass
         self.addCleanup(maybeCommitThis)
-        self.otherTransaction = txn
-        return self.otherTransaction
+        self.activeTransactions[count] = txn
+        return self.activeTransactions[count]
 
 
-    def otherTransactionUnderTest(self):
-        if self.otherTransaction is None:
-            self.newOtherTransaction()
-        return self.otherTransaction
+    def theTransactionUnderTest(self, count):
+        if self.activeTransactions[count] is None:
+            self.makeNewTransaction(count)
+        return self.activeTransactions[count]
 
 
     @inlineCallbacks
-    def otherCommit(self):
-        assert self.otherTransaction is not None
-        yield self.otherTransaction.commit()
-        self.otherTransaction = None
+    def commitTransaction(self, count):
+        assert self.activeTransactions[count] is not None
+        yield self.activeTransactions[count].commit()
+        self.activeTransactions[count] = None
 
 
     @inlineCallbacks
-    def otherAbort(self):
-        assert self.otherTransaction is not None
-        yield self.otherTransaction.abort()
-        self.otherTransaction = None
+    def abortTransaction(self, count):
+        assert self.activeTransactions[count] is not None
+        yield self.activeTransactions[count].abort()
+        self.activeTransactions[count] = None
 
 
     def makeConduit(self, store):
@@ -199,15 +208,15 @@
     @inlineCallbacks
     def createShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
 
-        home = yield self.homeUnderTest(name=ownerGUID, create=True)
+        home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name=ownerGUID, create=True)
         calendar = yield home.calendarWithName(name)
         yield calendar.inviteUIDToShare(shareeGUID, _BIND_MODE_WRITE, "shared", shareName="shared-calendar")
-        yield self.commit()
+        yield self.commitTransaction(0)
 
         # ACK: home2 is None
-        home2 = yield self.homeUnderTest(txn=self.newOtherTransaction(), name=shareeGUID)
+        home2 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=shareeGUID)
         yield home2.acceptShare("shared-calendar")
-        yield self.otherCommit()
+        yield self.commitTransaction(1)
 
         returnValue("shared-calendar")
 
@@ -215,7 +224,7 @@
     @inlineCallbacks
     def removeShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
 
-        home = yield self.homeUnderTest(name=ownerGUID)
+        home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name=ownerGUID)
         calendar = yield home.calendarWithName(name)
         yield calendar.uninviteUIDFromShare(shareeGUID)
-        yield self.commit()
+        yield self.commitTransaction(0)

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -3041,7 +3041,16 @@
         if transaction._disableCache:
             self._cacher = _EmptyCacher()
 
+        # This is used to track whether the originating request is from the store associated
+        # by the transaction, or from a remote store. We need to be able to distinguish store
+        # objects that are locally hosted (_HOME_STATUS_NORMAL) or remotely hosted
+        # (_HOME_STATUS_EXTERNAL). For the later we need to know whether the object is being
+        # accessed from the local store (in which case requests for child objects etc will be
+        # directed at a remote store) or whether it is being accessed as the tresult of a remote
+        # request (in which case requests for child objects etc will be directed at the local store).
+        self._internalRequest = True
 
+
     @classmethod
     def _register(cls, homeType):
         """
@@ -3323,11 +3332,22 @@
         """
         Is this an external home.
 
-        @return: a string.
+        @return: a L{bool}.
         """
         return self._status == _HOME_STATUS_EXTERNAL
 
 
+    def externalClass(self):
+        """
+        Is this an external home which also needs to have any requests directed
+        to a remote pod via the external (conduit using) implementation of this
+        class
+
+        @return: a L{bool}.
+        """
+        return self._status == _HOME_STATUS_EXTERNAL and self._internalRequest
+
+
     def purging(self):
         """
         Is this an external home.
@@ -3337,18 +3357,25 @@
         return self._status == _HOME_STATUS_PURGING
 
 
-    @inlineCallbacks
     def purge(self):
         """
         Mark this home as being purged.
         """
-        # Only if normal
-        if self._status == _HOME_STATUS_NORMAL:
+        return self.setStatus(_HOME_STATUS_PURGING)
+
+
+    @inlineCallbacks
+    def setStatus(self, newStatus):
+        """
+        Mark this home as being purged.
+        """
+        # Only if different
+        if self._status != newStatus:
             yield Update(
-                {self._homeSchema.STATUS: _HOME_STATUS_PURGING},
+                {self._homeSchema.STATUS: newStatus},
                 Where=(self._homeSchema.RESOURCE_ID == self._resourceID),
             ).on(self._txn)
-            self._status = _HOME_STATUS_PURGING
+            self._status = newStatus
             yield self._cacher.delete(self._ownerUID)
 
 
@@ -5434,6 +5461,26 @@
             cls._bindSchema.MESSAGE
         )
 
+
+    @classmethod
+    def bindAttributes(cls):
+        """
+        Return a list of column names for retrieval during creation. This allows
+        different child classes to have their own type specific data, but still make use of the
+        common base logic.
+        """
+
+        return (
+            "_bindMode",
+            "_homeResourceID",
+            "_resourceID",
+            "_externalID",
+            "_name",
+            "_bindStatus",
+            "_bindRevision",
+            "_bindMessage",
+        )
+
     bindColumnCount = 8
 
     @classmethod
@@ -5557,7 +5604,7 @@
         else:
             ownerName = None
 
-        c = cls._externalClass if ownerHome.external() else cls
+        c = cls._externalClass if ownerHome.externalClass() else cls
         child = c(
             home=home,
             name=name,
@@ -5902,6 +5949,47 @@
         return self.ownerHome().external()
 
 
+    def externalClass(self):
+        """
+        Is this an external home.
+
+        @return: a string.
+        """
+        return self.ownerHome().externalClass()
+
+
+    def externalize(self):
+        """
+        Create a dictionary mapping key attributes so this object can be sent over a cross-pod call
+        and reconstituted at the other end. Note that the other end may have a different schema so
+        the attributes may not match exactly and will need to be processed accordingly.
+        """
+        data = {}
+        data["bindData"] = dict([(attr[1:], getattr(self, attr, None)) for attr in self.bindAttributes()])
+        data["additionalBindData"] = dict([(attr[1:], getattr(self, attr, None)) for attr in self.additionalBindAttributes()])
+        data["metadataData"] = dict([(attr[1:], getattr(self, attr, None)) for attr in self.metadataAttributes()])
+        return data
+
+
+    @classmethod
+    @inlineCallbacks
+    def internalize(cls, parent, mapping):
+        """
+        Given a mapping generated by L{externalize}, convert the values into an array of database
+        like items that conforms to the ordering of L{_allColumns} so it can be fed into L{makeClass}.
+        Note that there may be a schema mismatch with the external data, so treat missing items as
+        C{None} and ignore extra items.
+        """
+
+        bindData = [mapping["bindData"].get(row[1:]) for row in cls.bindAttributes()]
+        additionalBindData = [mapping["additionalBindData"].get(row[1:]) for row in cls.additionalBindAttributes()]
+        metadataData = [mapping["metadataData"].get(row[1:]) for row in cls.metadataAttributes()]
+        child = yield cls.makeClass(parent, bindData, additionalBindData, metadataData)
+#        for attr in cls._otherSerializedAttributes():
+#            setattr(child, attr, mapping.get(attr[1:]))
+        returnValue(child)
+
+
     @property
     def _txn(self):
         return self._home._txn
@@ -6058,40 +6146,18 @@
         returnValue(results)
 
 
-    @classproperty
-    def _objectResourceNamesQuery(cls):
-        """
-        DAL query to load all object resource names for a home child.
-        """
-        obj = cls._objectSchema
-        return Select([obj.RESOURCE_NAME], From=obj,
-                      Where=obj.PARENT_RESOURCE_ID == Parameter('resourceID'))
-
-
     @inlineCallbacks
     def listObjectResources(self):
         if self._objectNames is None:
-            rows = yield self._objectResourceNamesQuery.on(
-                self._txn, resourceID=self._resourceID)
-            self._objectNames = sorted([row[0] for row in rows])
+            self._objectNames = yield self._objectResourceClass.listObjects(self)
         returnValue(self._objectNames)
 
 
-    @classproperty
-    def _objectCountQuery(cls):
-        """
-        DAL query to count all object resources for a home child.
-        """
-        obj = cls._objectSchema
-        return Select([Count(ALL_COLUMNS)], From=obj,
-                      Where=obj.PARENT_RESOURCE_ID == Parameter('resourceID'))
-
-
     @inlineCallbacks
     def countObjectResources(self):
         if self._objectNames is None:
-            rows = yield self._objectCountQuery.on(self._txn, resourceID=self._resourceID)
-            returnValue(rows[0][0])
+            count = (yield self._objectResourceClass.countObjects(self))
+            returnValue(count)
         returnValue(len(self._objectNames))
 
 
@@ -6137,19 +6203,6 @@
         returnValue(objectResource)
 
 
-    @classproperty
-    def _resourceNameForUIDQuery(cls):
-        """
-        DAL query to retrieve the resource name for an object resource based on
-        its UID column.
-        """
-        obj = cls._objectSchema
-        return Select(
-            [obj.RESOURCE_NAME], From=obj,
-            Where=(obj.UID == Parameter("uid")).And(
-                obj.PARENT_RESOURCE_ID == Parameter("resourceID")))
-
-
     @inlineCallbacks
     def resourceNameForUID(self, uid):
         try:
@@ -6157,29 +6210,14 @@
             returnValue(resource.name() if resource else None)
         except KeyError:
             pass
-        rows = yield self._resourceNameForUIDQuery.on(
-            self._txn, uid=uid, resourceID=self._resourceID)
-        if rows:
-            returnValue(rows[0][0])
+        name = yield self._objectResourceClass.resourceNameForUID(self, uid)
+        if name:
+            returnValue(name)
         else:
             self._objects[uid] = None
             returnValue(None)
 
 
-    @classproperty
-    def _resourceUIDForNameQuery(cls):
-        """
-        DAL query to retrieve the UID for an object resource based on its
-        resource name column.
-        """
-        obj = cls._objectSchema
-        return Select(
-            [obj.UID], From=obj,
-            Where=(obj.RESOURCE_NAME == Parameter("name")).And(
-                obj.PARENT_RESOURCE_ID == Parameter("resourceID"))
-        )
-
-
     @inlineCallbacks
     def resourceUIDForName(self, name):
         try:
@@ -6187,10 +6225,9 @@
             returnValue(resource.uid() if resource else None)
         except KeyError:
             pass
-        rows = yield self._resourceUIDForNameQuery.on(
-            self._txn, name=name, resourceID=self._resourceID)
-        if rows:
-            returnValue(rows[0][0])
+        uid = yield self._objectResourceClass.resourceUIDForName(self, name)
+        if uid:
+            returnValue(uid)
         else:
             self._objects[name] = None
             returnValue(None)
@@ -6767,7 +6804,7 @@
         @rtype: L{CommonHomeChild}
         """
 
-        c = cls._externalClass if parent.external() else cls
+        c = cls._externalClass if parent.externalClass() else cls
         child = c(
             parent,
             objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
@@ -6912,6 +6949,33 @@
 
 
     @classmethod
+    @inlineCallbacks
+    def listObjects(cls, parent):
+        """
+        Query to load all object resource names for a home child.
+        """
+        obj = cls._objectSchema
+        rows = yield Select(
+            [obj.RESOURCE_NAME],
+            From=obj,
+            Where=obj.PARENT_RESOURCE_ID == Parameter('parentID')
+        ).on(parent._txn, parentID=parent.id())
+        returnValue(sorted([row[0] for row in rows]))
+
+
+    @classmethod
+    @inlineCallbacks
+    def countObjects(cls, parent):
+        obj = cls._objectSchema
+        rows = yield Select(
+            [Count(ALL_COLUMNS)],
+            From=obj,
+            Where=obj.PARENT_RESOURCE_ID == Parameter('parentID')
+        ).on(parent._txn, parentID=parent.id())
+        returnValue(rows[0][0])
+
+
+    @classmethod
     def _allColumnsWithParentAndNamesQuery(cls, names):
         obj = cls._objectSchema
         return Select(cls._allColumns(), From=obj,
@@ -7016,6 +7080,40 @@
 
     @classmethod
     @inlineCallbacks
+    def resourceNameForUID(cls, parent, uid):
+        """
+        Query to retrieve the resource name for an object resource based on
+        its UID column.
+        """
+        obj = cls._objectSchema
+        rows = yield Select(
+            [obj.RESOURCE_NAME],
+            From=obj,
+            Where=(obj.UID == Parameter("uid")).And(
+                obj.PARENT_RESOURCE_ID == Parameter("parentID"))
+        ).on(parent._txn, uid=uid, parentID=parent.id())
+        returnValue(rows[0][0] if rows else "")
+
+
+    @classmethod
+    @inlineCallbacks
+    def resourceUIDForName(cls, parent, name):
+        """
+        Query to retrieve the UID for an object resource based on its
+        resource name column.
+        """
+        obj = cls._objectSchema
+        rows = yield Select(
+            [obj.UID],
+            From=obj,
+            Where=(obj.RESOURCE_NAME == Parameter("name")).And(
+                obj.PARENT_RESOURCE_ID == Parameter("parentID"))
+        ).on(parent._txn, name=name, parentID=parent.id())
+        returnValue(rows[0][0] if rows else "")
+
+
+    @classmethod
+    @inlineCallbacks
     def create(cls, parent, name, component, options=None):
 
         child = (yield parent.objectResourceWithName(name))
@@ -7025,7 +7123,7 @@
         if name.startswith("."):
             raise ObjectResourceNameNotAllowedError(name)
 
-        c = cls._externalClass if parent.external() else cls
+        c = cls._externalClass if parent.externalClass() else cls
         objectResource = c(parent, name, None, None, options=options)
         yield objectResource.setComponent(component, inserting=True)
         yield objectResource._loadPropertyStore(created=True)
@@ -7084,6 +7182,10 @@
 
     @classmethod
     def _rowAttributes(cls):
+        """
+        Object attributes used to store the column values from L{_allColumns}. This used to create
+        a mapping when serializing the object for cross-pod requests.
+        """
         return (
             "_resourceID",
             "_name",
@@ -7098,6 +7200,9 @@
 
     @classmethod
     def _otherSerializedAttributes(cls):
+        """
+        Other object attributes used for serialization.
+        """
         return (
             "_componentChanged",
         )

Modified: CalendarServer/trunk/txdav/common/datastore/sql_external.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_external.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/sql_external.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -62,27 +62,6 @@
         return True
 
 
-    def children(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def loadChildren(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def listChildren(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
     def objectWithShareUID(self, shareUID):
         """
         No children.
@@ -210,6 +189,43 @@
     specific apis to the other pod using cross-pod requests.
     """
 
+    @classmethod
+    @inlineCallbacks
+    def listObjects(cls, home):
+        """
+        Retrieve the names of the children that exist in the given home.
+
+        @return: an iterable of C{str}s.
+        """
+
+        results = yield home._txn.store().conduit.send_homechild_listobjects(home)
+        returnValue(results)
+
+
+    @classmethod
+    @inlineCallbacks
+    def loadAllObjects(cls, home):
+        raw_results = yield home._txn.store().conduit.send_homechild_loadallobjects(home)
+
+        results = []
+        for mapping in raw_results:
+            child = yield cls.internalize(home, mapping)
+            results.append(child)
+        returnValue(results)
+
+
+    @classmethod
+    @inlineCallbacks
+    def objectWith(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
+        mapping = yield home._txn.store().conduit.send_homechild_objectwith(home, name, resourceID, externalID, accepted)
+
+        if mapping:
+            child = yield cls.internalize(home, mapping)
+            returnValue(child)
+        else:
+            returnValue(None)
+
+
     def external(self):
         """
         Is this an external home.
@@ -242,72 +258,6 @@
 
 
     @inlineCallbacks
-    def listObjectResources(self):
-        if self._objectNames is None:
-            try:
-                self._objectNames = yield self._txn.store().conduit.send_listobjects(self)
-            except NonExistentExternalShare:
-                yield self.fixNonExistentExternalShare()
-                raise ExternalShareFailed("External share does not exist")
-
-        returnValue(self._objectNames)
-
-
-    @inlineCallbacks
-    def countObjectResources(self):
-        if self._objectNames is None:
-            try:
-                count = yield self._txn.store().conduit.send_countobjects(self)
-            except NonExistentExternalShare:
-                yield self.fixNonExistentExternalShare()
-                raise ExternalShareFailed("External share does not exist")
-            returnValue(count)
-        returnValue(len(self._objectNames))
-
-
-    @inlineCallbacks
-    def resourceNameForUID(self, uid):
-        try:
-            resource = self._objects[uid]
-            returnValue(resource.name() if resource else None)
-        except KeyError:
-            pass
-
-        try:
-            name = yield self._txn.store().conduit.send_resourcenameforuid(self, uid)
-        except NonExistentExternalShare:
-            yield self.fixNonExistentExternalShare()
-            raise ExternalShareFailed("External share does not exist")
-
-        if name:
-            returnValue(name)
-        else:
-            self._objects[uid] = None
-            returnValue(None)
-
-
-    @inlineCallbacks
-    def resourceUIDForName(self, name):
-        try:
-            resource = self._objects[name]
-            returnValue(resource.uid() if resource else None)
-        except KeyError:
-            pass
-
-        try:
-            uid = yield self._txn.store().conduit.send_resourceuidforname(self, name)
-        except NonExistentExternalShare:
-            yield self.fixNonExistentExternalShare()
-            raise ExternalShareFailed("External share does not exist")
-
-        if uid:
-            returnValue(uid)
-        else:
-            self._objects[name] = None
-            returnValue(None)
-
-
-    @inlineCallbacks
     def moveObjectResource(self, child, newparent, newname=None):
         """
         The base class does an optimization to avoid removing/re-creating
@@ -332,7 +282,7 @@
         """
 
         try:
-            result = yield self._txn.store().conduit.send_movehere(self, name, str(component))
+            result = yield self._txn.store().conduit.send_homechild_movehere(self, name, str(component))
         except NonExistentExternalShare:
             yield self.fixNonExistentExternalShare()
             raise ExternalShareFailed("External share does not exist")
@@ -352,7 +302,7 @@
         """
 
         try:
-            result = yield self._txn.store().conduit.send_moveaway(self, rid)
+            result = yield self._txn.store().conduit.send_homechild_moveaway(self, rid)
         except NonExistentExternalShare:
             yield self.fixNonExistentExternalShare()
             raise ExternalShareFailed("External share does not exist")
@@ -363,7 +313,7 @@
     def syncToken(self):
         if self._syncTokenRevision is None:
             try:
-                token = yield self._txn.store().conduit.send_synctoken(self)
+                token = yield self._txn.store().conduit.send_homechild_synctoken(self)
                 self._syncTokenRevision = self.revisionFromToken(token)
             except NonExistentExternalShare:
                 yield self.fixNonExistentExternalShare()
@@ -374,7 +324,7 @@
     @inlineCallbacks
     def resourceNamesSinceRevision(self, revision):
         try:
-            names = yield self._txn.store().conduit.send_resourcenamessincerevision(self, revision)
+            names = yield self._txn.store().conduit.send_homechild_resourcenamessincerevision(self, revision)
         except NonExistentExternalShare:
             yield self.fixNonExistentExternalShare()
             raise ExternalShareFailed("External share does not exist")
@@ -385,7 +335,7 @@
     @inlineCallbacks
     def search(self, filter, **kwargs):
         try:
-            results = yield self._txn.store().conduit.send_search(self, filter.serialize(), **kwargs)
+            results = yield self._txn.store().conduit.send_homechild_search(self, filter.serialize(), **kwargs)
         except NonExistentExternalShare:
             yield self.fixNonExistentExternalShare()
             raise ExternalShareFailed("External share does not exist")
@@ -403,7 +353,7 @@
     @classmethod
     @inlineCallbacks
     def loadAllObjects(cls, parent):
-        mapping_list = yield parent._txn.store().conduit.send_loadallobjects(parent, None)
+        mapping_list = yield parent._txn.store().conduit.send_objectresource_loadallobjects(parent)
 
         results = []
         if mapping_list:
@@ -416,7 +366,7 @@
     @classmethod
     @inlineCallbacks
     def loadAllObjectsWithNames(cls, parent, names):
-        mapping_list = yield parent._txn.store().conduit.send_loadallobjectswithnames(parent, None, names)
+        mapping_list = yield parent._txn.store().conduit.send_objectresource_loadallobjectswithnames(parent, names)
 
         results = []
         if mapping_list:
@@ -428,8 +378,22 @@
 
     @classmethod
     @inlineCallbacks
+    def listObjects(cls, parent):
+        results = yield parent._txn.store().conduit.send_objectresource_listobjects(parent)
+        returnValue(results)
+
+
+    @classmethod
+    @inlineCallbacks
+    def countObjects(cls, parent):
+        result = yield parent._txn.store().conduit.send_objectresource_countobjects(parent)
+        returnValue(result)
+
+
+    @classmethod
+    @inlineCallbacks
     def objectWith(cls, parent, name=None, uid=None, resourceID=None):
-        mapping = yield parent._txn.store().conduit.send_objectwith(parent, None, name, uid, resourceID)
+        mapping = yield parent._txn.store().conduit.send_objectresource_objectwith(parent, name, uid, resourceID)
 
         if mapping:
             child = yield cls.internalize(parent, mapping)
@@ -440,8 +404,22 @@
 
     @classmethod
     @inlineCallbacks
+    def resourceNameForUID(cls, parent, uid):
+        result = yield parent._txn.store().conduit.send_objectresource_resourcenameforuid(parent, uid)
+        returnValue(result)
+
+
+    @classmethod
+    @inlineCallbacks
+    def resourceUIDForName(cls, parent, name):
+        result = yield parent._txn.store().conduit.send_objectresource_resourceuidforname(parent, name)
+        returnValue(result)
+
+
+    @classmethod
+    @inlineCallbacks
     def create(cls, parent, name, component, options=None):
-        mapping = yield parent._txn.store().conduit.send_create(parent, None, name, str(component), options=options)
+        mapping = yield parent._txn.store().conduit.send_objectresource_create(parent, name, str(component), options=options)
 
         if mapping:
             child = yield cls.internalize(parent, mapping)
@@ -452,7 +430,7 @@
 
     @inlineCallbacks
     def setComponent(self, component, **kwargs):
-        self._componentChanged = yield self._txn.store().conduit.send_setcomponent(self.parentCollection(), self, str(component), **kwargs)
+        self._componentChanged = yield self._txn.store().conduit.send_objectresource_setcomponent(self, str(component), **kwargs)
         self._cachedComponent = None
         returnValue(self._componentChanged)
 
@@ -460,7 +438,7 @@
     @inlineCallbacks
     def component(self):
         if self._cachedComponent is None:
-            text = yield self._txn.store().conduit.send_component(self.parentCollection(), self)
+            text = yield self._txn.store().conduit.send_objectresource_component(self)
             self._cachedComponent = self._componentClass.fromString(text)
 
         returnValue(self._cachedComponent)
@@ -468,4 +446,4 @@
 
     @inlineCallbacks
     def remove(self):
-        yield self._txn.store().conduit.send_remove(self.parentCollection(), self)
+        yield self._txn.store().conduit.send_objectresource_remove(self)

Modified: CalendarServer/trunk/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/util.py	2015-01-21 19:09:42 UTC (rev 14332)
+++ CalendarServer/trunk/txdav/common/datastore/test/util.py	2015-01-21 19:57:08 UTC (rev 14333)
@@ -105,11 +105,11 @@
     """
     Test-fixture-builder which can construct a PostgresStore.
     """
-    def __init__(self, secondary=False):
+    def __init__(self, count=0):
         self.sharedService = None
         self.currentTestID = None
-        self.sharedDBPath = "_test_sql_db" + str(os.getpid()) + ("-2" if secondary else "")
-        self.ampPort = config.WorkQueue.ampPort + (1 if secondary else 0)
+        self.sharedDBPath = "_test_sql_db" + str(os.getpid()) + ("-{}".format(count) if count else "")
+        self.ampPort = config.WorkQueue.ampPort + count
 
 
     def createService(self, serviceFactory):
@@ -127,6 +127,7 @@
                 "-c fsync=FALSE",
                 "-c synchronous_commit=off",
                 "-c full_page_writes=FALSE",
+                "-c client-min-messages=warning",
             ],
             testMode=True
         )
@@ -230,11 +231,6 @@
             txn = store.newTransaction()
             jobs = yield JobItem.all(txn)
             yield txn.commit()
-            if len(jobs):
-                print("Jobs left in job queue {}: {}".format(
-                    testCase,
-                    ",".join([job.workType for job in jobs])
-                ))
 
             if enableJobProcessing:
                 yield pool.stopService()
@@ -243,10 +239,18 @@
             wasBusy = len(cp._busy)
             busyText = repr(cp._busy)
             result = yield cp.stopService()
+
             if deriveValue(testCase, _SPECIAL_TXN_CLEAN, lambda tc: False):
                 if wasBusy:
                     testCase.fail("Outstanding Transactions: " + busyText)
                 returnValue(result)
+
+            if len(jobs):
+                testCase.fail("Jobs left in job queue {}: {}".format(
+                    testCase,
+                    ",".join([job.workType for job in jobs])
+                ))
+
             returnValue(result)
 
         testCase.addCleanup(stopIt)
@@ -733,7 +737,7 @@
     @inlineCallbacks
     def buildStoreAndDirectory(
         self, accounts=None, resources=None, augments=None, proxies=None,
-        extraUids=None, serversDB=None, cacheSeconds=0
+        extraUids=None, serversDB=None, cacheSeconds=0, storeBuilder=theStoreBuilder
     ):
 
         self.serverRoot = self.mktemp()
@@ -745,7 +749,7 @@
         config.reset()
         self.configure()
 
-        self.store = yield self.buildStore()
+        self.store = yield self.buildStore(storeBuilder)
         self._sqlCalendarStore = self.store  # FIXME: remove references to this
 
         self.directory = buildTestDirectory(
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150121/a6558d09/attachment-0001.html>


More information about the calendarserver-changes mailing list