[CalendarServer-changes] [11891] CalendarServer/branches/users/sagen/groupcacher

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 12 11:23:26 PDT 2014


Revision: 11891
          http://trac.calendarserver.org//changeset/11891
Author:   sagen at apple.com
Date:     2013-11-06 12:11:38 -0800 (Wed, 06 Nov 2013)
Log Message:
-----------
Checkpoint of group cacher work: group membership and delegates are in the store

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/groupcacher/calendarserver/tap/caldav.py
    CalendarServer/branches/users/sagen/groupcacher/calendarserver/tools/principals.py
    CalendarServer/branches/users/sagen/groupcacher/calendarserver/tools/util.py
    CalendarServer/branches/users/sagen/groupcacher/twext/enterprise/dal/record.py
    CalendarServer/branches/users/sagen/groupcacher/twext/enterprise/dal/syntax.py
    CalendarServer/branches/users/sagen/groupcacher/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/sagen/groupcacher/txdav/common/datastore/sql.py
    CalendarServer/branches/users/sagen/groupcacher/txdav/common/datastore/sql_schema/current.sql

Added Paths:
-----------
    CalendarServer/branches/users/sagen/groupcacher/twext/who/delegates.py
    CalendarServer/branches/users/sagen/groupcacher/twext/who/groups.py
    CalendarServer/branches/users/sagen/groupcacher/twext/who/test/test_delegates.py
    CalendarServer/branches/users/sagen/groupcacher/twext/who/test/test_groups.py

Modified: CalendarServer/branches/users/sagen/groupcacher/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/calendarserver/tap/caldav.py	2013-11-05 20:05:41 UTC (rev 11890)
+++ CalendarServer/branches/users/sagen/groupcacher/calendarserver/tap/caldav.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -81,11 +81,11 @@
 from twistedcaldav.config import config, ConfigurationError
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
 from twistedcaldav.directory import calendaruserproxy
-from twistedcaldav.directory.directory import GroupMembershipCacheUpdater
+from twext.who.groups import GroupCacher
 from twistedcaldav.localization import processLocalizationFiles
 from twistedcaldav import memcachepool
 from twistedcaldav.upgrade import UpgradeFileSystemFormatStep, PostDBImportStep
-from twistedcaldav.directory.directory import scheduleNextGroupCachingUpdate
+from twext.who.groups import scheduleNextGroupCachingUpdate
 
 try:
     from twistedcaldav.authkerb import NegotiateCredentialFactory
@@ -890,9 +890,10 @@
 
         # Optionally set up group cacher
         if config.GroupCaching.Enabled:
-            groupCacher = GroupMembershipCacheUpdater(
+            groupCacher = GroupCacher(
                 calendaruserproxy.ProxyDBService,
                 directory,
+                store,
                 config.GroupCaching.UpdateSeconds,
                 config.GroupCaching.ExpireSeconds,
                 config.GroupCaching.LockSeconds,
@@ -1200,9 +1201,10 @@
 
             # Optionally set up group cacher
             if config.GroupCaching.Enabled:
-                groupCacher = GroupMembershipCacheUpdater(
+                groupCacher = GroupCacher(
                     calendaruserproxy.ProxyDBService,
                     directory,
+                    store,
                     config.GroupCaching.UpdateSeconds,
                     config.GroupCaching.ExpireSeconds,
                     config.GroupCaching.LockSeconds,
@@ -1713,9 +1715,10 @@
 
             # Optionally set up group cacher
             if config.GroupCaching.Enabled:
-                groupCacher = GroupMembershipCacheUpdater(
+                groupCacher = GroupCacher(
                     calendaruserproxy.ProxyDBService,
                     directory,
+                    store,
                     config.GroupCaching.UpdateSeconds,
                     config.GroupCaching.ExpireSeconds,
                     config.GroupCaching.LockSeconds,

Modified: CalendarServer/branches/users/sagen/groupcacher/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/calendarserver/tools/principals.py	2013-11-05 20:05:41 UTC (rev 11890)
+++ CalendarServer/branches/users/sagen/groupcacher/calendarserver/tools/principals.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -31,7 +31,7 @@
 
 from twistedcaldav.config import config
 from twistedcaldav.directory.directory import UnknownRecordTypeError, DirectoryError
-from twistedcaldav.directory.directory import schedulePolledGroupCachingUpdate
+from twext.who.groups import schedulePolledGroupCachingUpdate
 
 from calendarserver.tools.util import (
     booleanArgument, proxySubprincipal, action_addProxyPrincipal,

Modified: CalendarServer/branches/users/sagen/groupcacher/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/calendarserver/tools/util.py	2013-11-05 20:05:41 UTC (rev 11890)
+++ CalendarServer/branches/users/sagen/groupcacher/calendarserver/tools/util.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -50,7 +50,7 @@
 from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.directory import schedulePolledGroupCachingUpdate
+from twext.who.groups import schedulePolledGroupCachingUpdate
 from calendarserver.push.notifier import NotifierFactory
 
 from txdav.common.datastore.file import CommonDataStore

Modified: CalendarServer/branches/users/sagen/groupcacher/twext/enterprise/dal/record.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/twext/enterprise/dal/record.py	2013-11-05 20:05:41 UTC (rev 11890)
+++ CalendarServer/branches/users/sagen/groupcacher/twext/enterprise/dal/record.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -163,7 +163,7 @@
         """
         words = columnName.lower().split("_")
         def cap(word):
-            if word.lower() == 'id':
+            if word.lower() in ('id', 'guid'):
                 return word.upper()
             else:
                 return word.capitalize()

Modified: CalendarServer/branches/users/sagen/groupcacher/twext/enterprise/dal/syntax.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/twext/enterprise/dal/syntax.py	2013-11-05 20:05:41 UTC (rev 11890)
+++ CalendarServer/branches/users/sagen/groupcacher/twext/enterprise/dal/syntax.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -1816,15 +1816,21 @@
         params = []
         for parameter in self.parameters:
             if isinstance(parameter, Parameter):
+                if parameter.name not in kw:
+                    # TODO UNIT TEST
+                    raise DALError("Missing parameter: {p!r} fragment={f!r}, kw={kw!r}".format(p=parameter.name,
+                        f=self.text, kw=kw))
+                parameterValue = kw[parameter.name]
+
                 if parameter.count is not None:
-                    if parameter.count != len(kw[parameter.name]):
+                    if parameter.count != len(parameterValue):
                         raise DALError("Number of place holders does not match number of items to bind")
-                    for item in kw[parameter.name]:
+                    for item in parameterValue:
                         params.append(item)
                 else:
-                    params.append(kw[parameter.name])
+                    params.append(parameterValue)
             else:
-                params.append(parameter)
+                params.append(parameterValue)
         return SQLFragment(self.text, params)
 
 

Added: CalendarServer/branches/users/sagen/groupcacher/twext/who/delegates.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/twext/who/delegates.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/groupcacher/twext/who/delegates.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -0,0 +1,86 @@
+# -*- test-case-name: twext.who.test.test_delegates -*-
+##
+# Copyright (c) 2013 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.
+##
+
+"""
+Delegate assignments
+"""
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twext.who.idirectory import RecordType
+
+from twext.python.log import Logger
+log = Logger()
+
+
+ at inlineCallbacks
+def addDelegate(txn, delegator, delegate, readWrite):
+    """
+    Args are records
+    """
+    if delegate.recordType == RecordType.group:
+        # find the groupID
+        results = (yield txn.groupByGUID(delegate.guid))
+        while not results:
+            # need to add the group to the groups table so we have a groupID
+
+            # TODO: is there a better pattern for this?
+            yield txn.addGroup(delegate.guid, delegate.fullNames[0], "")
+            results = (yield txn.groupByGUID(delegate.guid))
+
+        groupID = results[0][0]
+        yield txn.addDelegate(delegator.guid, groupID,
+            1 if readWrite else 0, True)
+    else:
+        yield txn.addDelegate(delegator.guid, delegate.guid,
+            1 if readWrite else 0, False)
+
+def removeDelegate(txn, delegator, delegate, readWrite):
+    """
+    Args are records
+    """
+    return txn.removeDelegate(delegator.guid, delegate.guid,
+        1 if readWrite else 0, delegate.recordType==RecordType.group)
+
+ at inlineCallbacks
+def delegatesOf(txn, delegator, readWrite):
+    """
+    Args are records
+    """
+    records = []
+    directory = delegator.service
+    results = (yield txn.delegates(delegator.guid, 1 if readWrite else 0))
+    for row in results:
+        if row[0] != delegator.guid:
+            record = (yield directory.recordWithGUID(row[0]))
+            if record is not None:
+                records.append(record)
+    returnValue(records)
+
+ at inlineCallbacks
+def delegateFor(txn, delegate, readWrite):
+    """
+    Args are records
+    """
+    records = []
+    directory = delegate.service
+    results = (yield txn.delegators(delegate.guid, 1 if readWrite else 0))
+    for row in results:
+        if row[0] != delegate.guid:
+            record = (yield directory.recordWithGUID(row[0]))
+            if record is not None:
+                records.append(record)
+    returnValue(records)

Added: CalendarServer/branches/users/sagen/groupcacher/twext/who/groups.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/twext/who/groups.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/groupcacher/twext/who/groups.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -0,0 +1,284 @@
+# -*- test-case-name: twext.who.test.test_groups -*-
+##
+# Copyright (c) 2013 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.
+##
+
+"""
+Group membership caching
+"""
+
+import datetime
+import hashlib
+from twext.enterprise.dal.record import fromTable
+from twext.enterprise.queue import WorkItem, PeerConnectionPool
+from txdav.common.datastore.sql_tables import schema
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twext.enterprise.dal.syntax import Delete
+from twext.who.idirectory import RecordType
+
+from twext.python.log import Logger
+log = Logger()
+
+
+class GroupCacherPollingWork(WorkItem,
+    fromTable(schema.GROUP_CACHER_POLLING_WORK)):
+
+    group = "group_cacher_polling"
+  
+    @inlineCallbacks
+    def doWork(self):
+
+        # Delete all other work items
+        yield Delete(From=self.table, Where=None).on(self.transaction)
+
+        groupCacher = getattr(self.transaction, "_groupCacher", None)
+        if groupCacher is not None:
+
+            # Schedule next update
+            notBefore = (datetime.datetime.utcnow() +
+                datetime.timedelta(seconds=groupCacher.updateSeconds))
+            log.debug("Scheduling next group cacher update: %s" % (notBefore,))
+            yield self.transaction.enqueue(GroupCacherPollingWork,
+                notBefore=notBefore)
+
+            try:
+                groupCacher.update(self.transaction)
+            except Exception, e:
+                log.error("Failed to update group membership cache (%s)" % (e,))
+
+        else:
+            notBefore = (datetime.datetime.utcnow() +
+                datetime.timedelta(seconds=10))
+            log.debug("Rescheduling group cacher update: %s" % (notBefore,))
+            yield self.transaction.enqueue(GroupCacherPollingWork,
+                notBefore=notBefore)
+
+
+ at inlineCallbacks
+def scheduleNextGroupCachingUpdate(store, seconds):
+    txn = store.newTransaction()
+    notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
+    log.debug("Scheduling next group cacher update: %s" % (notBefore,))
+    wp = (yield txn.enqueue(GroupCacherPollingWork, notBefore=notBefore))
+    yield txn.commit()
+    returnValue(wp)
+
+
+def schedulePolledGroupCachingUpdate(store):
+    """
+    Schedules a group caching update work item in "the past" so PeerConnectionPool's
+    overdue-item logic picks it up quickly.
+    """
+    seconds = -PeerConnectionPool.queueProcessTimeout
+    return scheduleNextGroupCachingUpdate(store, seconds)
+
+
+class GroupRefreshWork(WorkItem, fromTable(schema.GROUP_REFRESH_WORK)):
+
+    group = property(lambda self: self.groupGUID)
+
+    @inlineCallbacks
+    def doWork(self):
+
+        # Delete all other work items
+        yield Delete(From=self.table, Where=None).on(self.transaction)
+
+        groupCacher = getattr(self.transaction, "_groupCacher", None)
+        if groupCacher is not None:
+
+            try:
+                groupCacher.refreshGroup(self.transaction, self.groupGUID)
+            except Exception, e:
+                log.error("Failed to refresh group {group} {err}",
+                    group=self.groupGUID, err=e)
+
+        else:
+            notBefore = (datetime.datetime.utcnow() +
+                datetime.timedelta(seconds=10))
+            log.debug("Rescheduling group refresh for {group}: {when}",
+                group=self.groupGUID, when=notBefore)
+            yield self.transaction.enqueue(GroupRefreshWork,
+                groupGUID=self.groupGUID, notBefore=notBefore)
+
+
+class GroupAttendeeReconciliationWork(WorkItem, fromTable(schema.GROUP_ATTENDEE_RECONCILIATION_WORK)):
+    pass
+
+
+ at inlineCallbacks
+def _expandedMembers(record, members=None, records=None):
+    """
+    Return the expanded set of member records.  Intermediate groups are not returned
+    in the results, but their members are.
+    """
+    if members is None:
+        members = set()
+    if records is None:
+        records = set()
+
+    if record not in records:
+        records.add(record)
+        for member in (yield record.members()):
+            if member not in records:
+                if member.recordType != RecordType.group:
+                    members.add(member)
+                yield _expandedMembers(member, members, records)
+
+    returnValue(members)
+
+
+class GroupCacher(object):
+    log = Logger()
+
+    def __init__(self, proxyDB, directory, store, updateSeconds,
+        useExternalProxies=False, externalProxiesSource=None):
+        self.proxyDB = proxyDB
+        self.directory = directory
+        self.store = store
+        self.updateSeconds = updateSeconds
+        self.useExternalProxies = useExternalProxies
+        if useExternalProxies and externalProxiesSource is None:
+            externalProxiesSource = self.directory.getExternalProxyAssignments
+        self.externalProxiesSource = externalProxiesSource
+
+
+    @inlineCallbacks
+    def update(self, txn):
+        # Pull in external proxy assignments and stick in proxy db
+        # Figure out which groups matter
+        groupGUIDs = yield self.groupsToRefresh()
+        # For each of those groups, create a per-group refresh work item
+        for groupGUID in groupGUIDs:
+            notBefore = (datetime.datetime.utcnow() +
+                datetime.timedelta(seconds=1))
+            yield txn.enqueue(GroupRefreshWork,
+                groupGUID=groupGUID, notBefore=notBefore)
+
+        pass
+
+
+    @inlineCallbacks
+    def refreshGroup(self, txn, groupGUID):
+        # Does the work of a per-group refresh work item
+        # Faults in the flattened membership of a group, as GUIDs
+        # and updates the GROUP_MEMBERSHIP table
+        record = (yield self.directory.recordWithGUID(groupGUID))
+        membershipHashContent = hashlib.md5()
+        members = (yield _expandedMembers(record))
+        members = list(members)
+        members.sort(cmp=lambda x,y: cmp(x.guid, y.guid))
+        for member in members:
+            membershipHashContent.update(member.guid)
+        membershipHash = membershipHashContent.hexdigest()
+        results = (yield txn.groupByGUID(groupGUID))
+        if not results:
+            # Group is not yet in the DB
+            cachedName = ""
+            cachedMembershipHash = ""
+            addGroup = True
+        else:
+            groupID, cachedName, cachedMembershipHash = results[0]
+            addGroup = False
+
+        if cachedMembershipHash != membershipHash:
+            membershipChanged = True
+            self.log.debug("Group '{group}' changed", group=record.fullNames[0])
+        else:
+            membershipChanged = False
+
+        if addGroup:
+            yield txn.addGroup(groupGUID, record.fullNames[0], membershipHash)
+        else:
+            yield txn.updateGroup(groupGUID, record.fullNames[0],
+                membershipHash)
+
+        results = (yield txn.groupByGUID(groupGUID))
+        if len(results) == 1:
+            groupID, name, cachedMembershipHash = results[0]
+        else:
+            self.log.error("Multiple group entries for {guid}", guid=groupGUID)
+
+        if membershipChanged:
+            newMemberGUIDs = set()
+            for member in members:
+                newMemberGUIDs.add(member.guid)
+            yield self.synchronizeMembers(txn, groupID, newMemberGUIDs)
+
+        yield self.scheduleEventReconciliations(txn, groupID)
+
+
+    @inlineCallbacks
+    def synchronizeMembers(self, txn, groupID, newMemberGUIDs):
+        numRemoved = numAdded = 0
+        cachedMemberGUIDs = set()
+        results = (yield txn.membersOfGroup(groupID))
+        for row in results:
+            cachedMemberGUIDs.add(row[0])
+
+        for memberGUID in cachedMemberGUIDs:
+            if memberGUID not in newMemberGUIDs:
+                numRemoved += 1
+                yield txn.removeMemberFromGroup(memberGUID, groupID)
+
+        for memberGUID in newMemberGUIDs:
+            if memberGUID not in cachedMemberGUIDs:
+                numAdded += 1
+                yield txn.addMemberToGroup(memberGUID, groupID)
+
+        returnValue((numAdded, numRemoved))
+
+
+    @inlineCallbacks
+    def cachedMembers(self, txn, groupID):
+        members = set()
+        results = (yield txn.membersOfGroup(groupID))
+        for row in results:
+            record = (yield self.directory.recordWithGUID(row[0]))
+            if record is not None:
+                members.add(record)
+        returnValue(members)
+
+
+
+
+    # @inlineCallbacks
+    def scheduleEventReconciliations(self, txn, groupID):
+        """
+        Find all events who have this groupID as an attendee and create
+        work items for them.
+        """
+        return succeed(None)
+
+
+
+    @inlineCallbacks
+    def groupsToRefresh(self):
+        delegatedGUIDs = set((yield self.proxyDB.getAllMembers()))
+        self.log.info("There are %d proxies" % (len(delegatedGUIDs),))
+        self.log.info("Retrieving group hierarchy from directory")
+
+        # "groups" maps a group to its members; the keys and values consist
+        # of whatever directory attribute is used to refer to members.  The
+        # attribute value comes from record.cachedGroupsAlias().
+        # "aliases" maps the record.cachedGroupsAlias() value for a group
+        # back to the group's guid.
+        groups, aliases = (yield self.getGroups(guids=delegatedGUIDs))
+        groupGUIDs = set(aliases.keys())
+        self.log.info("%d groups retrieved from the directory" %
+            (len(groupGUIDs),))
+
+        delegatedGUIDs = delegatedGUIDs.intersection(groupGUIDs)
+        self.log.info("%d groups are proxies" % (len(delegatedGUIDs),))
+        returnValue(delegatedGUIDs)

Added: CalendarServer/branches/users/sagen/groupcacher/twext/who/test/test_delegates.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/twext/who/test/test_delegates.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/groupcacher/twext/who/test/test_delegates.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -0,0 +1,186 @@
+##
+# Copyright (c) 2013 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.
+##
+
+"""
+Delegates implementation tests
+"""
+
+from twext.who.delegates import (
+    addDelegate, removeDelegate, delegatesOf, delegateFor
+)
+from twext.who.groups import GroupCacher
+from twext.who.test.test_xml import xmlService
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.test.util import StoreTestCase
+
+class DelegationTest(StoreTestCase):
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(DelegationTest, self).setUp()
+        self.xmlService = xmlService(self.mktemp(), xmlData=testXMLConfig)
+        self.groupCacher = GroupCacher(
+            None,
+            self.xmlService,
+            None,
+            0
+        )
+
+
+    @inlineCallbacks
+    def test_directDelegation(self):
+        store = self.storeUnderTest()
+        txn = store.newTransaction()
+
+        delegator = yield self.xmlService.recordWithUID("__wsanchez__")
+        delegate1 = yield self.xmlService.recordWithUID("__sagen__")
+        delegate2 = yield self.xmlService.recordWithUID("__cdaboo__")
+
+        # Add 1 delegate
+        yield addDelegate(txn, delegator, delegate1, True)
+        delegates = (yield delegatesOf(txn, delegator, True))
+        self.assertEquals(["sagen"], [d.shortNames[0] for d in delegates])
+        delegators = (yield delegateFor(txn, delegate1, True))
+        self.assertEquals(["wsanchez"], [d.shortNames[0] for d in delegators])
+
+        # Add another delegate
+        yield addDelegate(txn, delegator, delegate2, True)
+        delegates = (yield delegatesOf(txn, delegator, True))
+        self.assertEquals(set(["sagen", "cdaboo"]),
+            set([d.shortNames[0] for d in delegates]))
+        delegators = (yield delegateFor(txn, delegate2, True))
+        self.assertEquals(["wsanchez"], [d.shortNames[0] for d in delegators])
+
+        # Remove 1 delegate
+        yield removeDelegate(txn, delegator, delegate1, True)
+        delegates = (yield delegatesOf(txn, delegator, True))
+        self.assertEquals(["cdaboo"], [d.shortNames[0] for d in delegates])
+        delegators = (yield delegateFor(txn, delegate1, True))
+        self.assertEquals(0, len(delegators))
+
+        # Remove the other delegate
+        yield removeDelegate(txn, delegator, delegate2, True)
+        delegates = (yield delegatesOf(txn, delegator, True))
+        self.assertEquals(0, len(delegates))
+        delegators = (yield delegateFor(txn, delegate2, True))
+        self.assertEquals(0, len(delegators))
+
+
+    @inlineCallbacks
+    def test_indirectDelegation(self):
+        store = self.storeUnderTest()
+        txn = store.newTransaction()
+
+        delegator = yield self.xmlService.recordWithUID("__wsanchez__")
+        group1 = yield self.xmlService.recordWithUID("__top_group_1__")
+        delegate1 = yield self.xmlService.recordWithUID("__sagen__")
+
+        # Add group delegate, but before the group membership has been
+        # pulled in
+        yield addDelegate(txn, delegator, group1, True)
+        delegates = (yield delegatesOf(txn, delegator, True))
+        self.assertEquals(0, len(delegates))
+
+        # Now refresh the group and there will be 3 delegates (contained
+        # within 2 nested groups)
+        guid = "49b350c69611477b94d95516b13856ab"
+        yield self.groupCacher.refreshGroup(txn, guid)
+        delegates = (yield delegatesOf(txn, delegator, True))
+        self.assertEquals(set(["sagen", "cdaboo", "glyph"]),
+            set([d.shortNames[0] for d in delegates]))
+        delegators = (yield delegateFor(txn, delegate1, True))
+        self.assertEquals(["wsanchez"], [d.shortNames[0] for d in delegators])
+
+
+
+
+testXMLConfig = """<?xml version="1.0" encoding="utf-8"?>
+
+<directory realm="xyzzy">
+
+  <record type="user">
+    <uid>__wsanchez__</uid>
+    <guid>3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F</guid>
+    <short-name>wsanchez</short-name>
+    <short-name>wilfredo_sanchez</short-name>
+    <full-name>Wilfredo Sanchez</full-name>
+    <password>zehcnasw</password>
+    <email>wsanchez at bitbucket.calendarserver.org</email>
+    <email>wsanchez at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__glyph__</uid>
+    <guid>9064DF91-1DBC-4E07-9C2B-6839B0953876</guid>
+    <short-name>glyph</short-name>
+    <full-name>Glyph Lefkowitz</full-name>
+    <password>hpylg</password>
+    <email>glyph at bitbucket.calendarserver.org</email>
+    <email>glyph at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__sagen__</uid>
+    <guid>4AD155CB-AE9B-475F-986C-E08A7537893E</guid>
+    <short-name>sagen</short-name>
+    <full-name>Morgen Sagen</full-name>
+    <password>negas</password>
+    <email>sagen at bitbucket.calendarserver.org</email>
+    <email>shared at example.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__cdaboo__</uid>
+    <guid>7D45CB10-479E-456B-B54D-528958C5734B</guid>
+    <short-name>cdaboo</short-name>
+    <full-name>Cyrus Daboo</full-name>
+    <password>suryc</password>
+    <email>cdaboo at bitbucket.calendarserver.org</email>
+  </record>
+
+  <record type="user">
+    <uid>__dre__</uid>
+    <guid>CFC88493-DBFF-42B9-ADC7-9B3DA0B0769B</guid>
+    <short-name>dre</short-name>
+    <full-name>Andre LaBranche</full-name>
+    <password>erd</password>
+    <email>dre at bitbucket.calendarserver.org</email>
+    <email>shared at example.com</email>
+  </record>
+
+  <record type="group">
+    <uid>__top_group_1__</uid>
+    <guid>49B350C6-9611-477B-94D9-5516B13856AB</guid>
+    <short-name>top-group-1</short-name>
+    <full-name>Top Group 1</full-name>
+    <email>topgroup1 at example.com</email>
+    <member-uid>__wsanchez__</member-uid>
+    <member-uid>__glyph__</member-uid>
+    <member-uid>__sub_group_1__</member-uid>
+  </record>
+
+  <record type="group">
+    <uid>__sub_group_1__</uid>
+    <guid>86144F73-345A-4097-82F1-B782672087C7</guid>
+    <short-name>sub-group-1</short-name>
+    <full-name>Sub Group 1</full-name>
+    <email>subgroup1 at example.com</email>
+    <member-uid>__sagen__</member-uid>
+    <member-uid>__cdaboo__</member-uid>
+  </record>
+
+</directory>
+"""

Added: CalendarServer/branches/users/sagen/groupcacher/twext/who/test/test_groups.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/twext/who/test/test_groups.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/groupcacher/twext/who/test/test_groups.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -0,0 +1,205 @@
+##
+# Copyright (c) 2013 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.
+##
+
+"""
+Group membership caching implementation tests
+"""
+
+from twext.who.groups import GroupCacher, _expandedMembers
+from twext.who.test.test_xml import xmlService
+from twext.who.idirectory import RecordType
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.test.util import StoreTestCase
+
+class GroupCacherTest(StoreTestCase):
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(GroupCacherTest, self).setUp()
+        self.xmlService = xmlService(self.mktemp(), xmlData=testXMLConfig)
+        self.groupCacher = GroupCacher(
+            None,
+            self.xmlService,
+            None,
+            0
+        )
+
+
+    @inlineCallbacks
+    def test_expandedMembers(self):
+        record = yield self.xmlService.recordWithUID("__top_group_1__")
+        memberUIDs = set()
+        for member in (yield _expandedMembers(record)):
+            memberUIDs.add(member.uid)
+        self.assertEquals(memberUIDs, set(["__cdaboo__",
+            "__glyph__", "__sagen__", "__wsanchez__"]))
+
+        record = yield self.xmlService.recordWithUID("__sagen__")
+        members = yield _expandedMembers(record)
+        self.assertEquals(0, len(list(members)))
+
+
+    @inlineCallbacks
+    def test_refreshGroup(self):
+        """
+        Verify refreshGroup() adds a group to the Groups table with the
+        expected membership hash value and members
+        """
+
+        store = self.storeUnderTest()
+        txn = store.newTransaction()
+
+        guid = "49b350c69611477b94d95516b13856ab"
+        yield self.groupCacher.refreshGroup(txn, guid)
+
+        groupID, name, membershipHash = (yield txn.groupByGUID(guid))[0]
+        self.assertEquals(membershipHash, "e90052eb63d47f32d5b03df0073f7854")
+
+        groupGUID, name, membershipHash = (yield txn.groupByID(groupID))[0]
+        self.assertEquals(groupGUID, guid)
+        self.assertEquals(name, "Top Group 1")
+        self.assertEquals(membershipHash, "e90052eb63d47f32d5b03df0073f7854")
+
+        results = (yield txn.membersOfGroup(groupID))
+        for row in results:
+            print row[0]
+
+        records = (yield self.groupCacher.cachedMembers(txn, groupID))
+        self.assertEquals(
+            set([r.shortNames[0] for r in records]),
+            set(["wsanchez", "cdaboo", "glyph", "sagen"])
+        )
+        print records
+
+
+    @inlineCallbacks
+    def test_synchronizeMembers(self):
+        """
+        After loading in a group via refreshGroup(), pass new member sets to
+        synchronizeMembers() and verify members are added and removed as
+        expected
+        """
+
+        store = self.storeUnderTest()
+        txn = store.newTransaction()
+
+        # Refresh the group so it's assigned a group_id
+        guid = "49b350c69611477b94d95516b13856ab"
+        yield self.groupCacher.refreshGroup(txn, guid)
+        groupID, name, membershipHash = (yield txn.groupByGUID(guid))[0]
+
+        # Remove two members, and add one member
+        newSet = set()
+        for name in ("wsanchez", "cdaboo", "dre"):
+            record = (yield self.xmlService.recordWithShortName(RecordType.user,
+                name))
+            newSet.add(record.guid)
+        numAdded, numRemoved = (yield self.groupCacher.synchronizeMembers(txn,
+            groupID, newSet))
+        self.assertEquals(numAdded, 1)
+        self.assertEquals(numRemoved, 2)
+        records = (yield self.groupCacher.cachedMembers(txn, groupID))
+        self.assertEquals(
+            set([r.shortNames[0] for r in records]),
+            set(["wsanchez", "cdaboo", "dre"])
+        )
+
+        # Remove all members
+        numAdded, numRemoved = (yield self.groupCacher.synchronizeMembers(txn,
+            groupID, set()))
+        self.assertEquals(numAdded, 0)
+        self.assertEquals(numRemoved, 3)
+        records = (yield self.groupCacher.cachedMembers(txn, groupID))
+        self.assertEquals(len(records), 0)
+
+
+testXMLConfig = """<?xml version="1.0" encoding="utf-8"?>
+
+<directory realm="xyzzy">
+
+  <record type="user">
+    <uid>__wsanchez__</uid>
+    <guid>3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F</guid>
+    <short-name>wsanchez</short-name>
+    <short-name>wilfredo_sanchez</short-name>
+    <full-name>Wilfredo Sanchez</full-name>
+    <password>zehcnasw</password>
+    <email>wsanchez at bitbucket.calendarserver.org</email>
+    <email>wsanchez at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__glyph__</uid>
+    <guid>9064DF91-1DBC-4E07-9C2B-6839B0953876</guid>
+    <short-name>glyph</short-name>
+    <full-name>Glyph Lefkowitz</full-name>
+    <password>hpylg</password>
+    <email>glyph at bitbucket.calendarserver.org</email>
+    <email>glyph at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__sagen__</uid>
+    <guid>4AD155CB-AE9B-475F-986C-E08A7537893E</guid>
+    <short-name>sagen</short-name>
+    <full-name>Morgen Sagen</full-name>
+    <password>negas</password>
+    <email>sagen at bitbucket.calendarserver.org</email>
+    <email>shared at example.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__cdaboo__</uid>
+    <guid>7D45CB10-479E-456B-B54D-528958C5734B</guid>
+    <short-name>cdaboo</short-name>
+    <full-name>Cyrus Daboo</full-name>
+    <password>suryc</password>
+    <email>cdaboo at bitbucket.calendarserver.org</email>
+  </record>
+
+  <record type="user">
+    <uid>__dre__</uid>
+    <guid>CFC88493-DBFF-42B9-ADC7-9B3DA0B0769B</guid>
+    <short-name>dre</short-name>
+    <full-name>Andre LaBranche</full-name>
+    <password>erd</password>
+    <email>dre at bitbucket.calendarserver.org</email>
+    <email>shared at example.com</email>
+  </record>
+
+  <record type="group">
+    <uid>__top_group_1__</uid>
+    <guid>49B350C6-9611-477B-94D9-5516B13856AB</guid>
+    <short-name>top-group-1</short-name>
+    <full-name>Top Group 1</full-name>
+    <email>topgroup1 at example.com</email>
+    <member-uid>__wsanchez__</member-uid>
+    <member-uid>__glyph__</member-uid>
+    <member-uid>__sub_group_1__</member-uid>
+  </record>
+
+  <record type="group">
+    <uid>__sub_group_1__</uid>
+    <guid>86144F73-345A-4097-82F1-B782672087C7</guid>
+    <short-name>sub-group-1</short-name>
+    <full-name>Sub Group 1</full-name>
+    <email>subgroup1 at example.com</email>
+    <member-uid>__sagen__</member-uid>
+    <member-uid>__cdaboo__</member-uid>
+  </record>
+
+</directory>
+"""

Modified: CalendarServer/branches/users/sagen/groupcacher/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/twistedcaldav/directory/directory.py	2013-11-05 20:05:41 UTC (rev 11890)
+++ CalendarServer/branches/users/sagen/groupcacher/twistedcaldav/directory/directory.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -31,9 +31,6 @@
 
 from plistlib import readPlistFromString
 
-from twext.enterprise.dal.record import fromTable
-from twext.enterprise.dal.syntax import Delete
-from twext.enterprise.queue import WorkItem, PeerConnectionPool
 from twext.python.log import Logger
 from twext.web2.dav.auth import IPrincipalCredentials
 from twext.web2.dav.util import joinURL
@@ -52,7 +49,6 @@
 
 from txdav.caldav.icalendardirectoryservice import ICalendarStoreDirectoryService, \
     ICalendarStoreDirectoryRecord
-from txdav.common.datastore.sql_tables import schema
 
 from xml.parsers.expat import ExpatError
 
@@ -1038,62 +1034,6 @@
         returnValue((fast, len(members), len(changedMembers)))
 
 
-
-class GroupCacherPollingWork(WorkItem, fromTable(schema.GROUP_CACHER_POLLING_WORK)):
-
-    group = "group_cacher_polling"
-
-    @inlineCallbacks
-    def doWork(self):
-
-        # Delete all other work items
-        yield Delete(From=self.table, Where=None).on(self.transaction)
-
-        groupCacher = getattr(self.transaction, "_groupCacher", None)
-        if groupCacher is not None:
-
-            # Schedule next update
-            notBefore = (datetime.datetime.utcnow() +
-                datetime.timedelta(seconds=groupCacher.updateSeconds))
-            log.debug("Scheduling next group cacher update: %s" % (notBefore,))
-            yield self.transaction.enqueue(GroupCacherPollingWork,
-                notBefore=notBefore)
-
-            try:
-                groupCacher.updateCache()
-            except Exception, e:
-                log.error("Failed to update group membership cache (%s)" % (e,))
-
-        else:
-            notBefore = (datetime.datetime.utcnow() +
-                datetime.timedelta(seconds=10))
-            log.debug("Rescheduling group cacher update: %s" % (notBefore,))
-            yield self.transaction.enqueue(GroupCacherPollingWork,
-                notBefore=notBefore)
-
-
-
- at inlineCallbacks
-def scheduleNextGroupCachingUpdate(store, seconds):
-    txn = store.newTransaction()
-    notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
-    log.debug("Scheduling next group cacher update: %s" % (notBefore,))
-    wp = (yield txn.enqueue(GroupCacherPollingWork, notBefore=notBefore))
-    yield txn.commit()
-    returnValue(wp)
-
-
-
-def schedulePolledGroupCachingUpdate(store):
-    """
-    Schedules a group caching update work item in "the past" so PeerConnectionPool's
-    overdue-item logic picks it up quickly.
-    """
-    seconds = -PeerConnectionPool.queueProcessTimeout
-    return scheduleNextGroupCachingUpdate(store, seconds)
-
-
-
 def diffAssignments(old, new):
     """
     Compare two proxy assignment lists and return their differences in the form of

Modified: CalendarServer/branches/users/sagen/groupcacher/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/txdav/common/datastore/sql.py	2013-11-05 20:05:41 UTC (rev 11890)
+++ CalendarServer/branches/users/sagen/groupcacher/txdav/common/datastore/sql.py	2013-11-06 20:11:38 UTC (rev 11891)
@@ -80,6 +80,7 @@
 
 import sys
 import time
+import datetime
 
 current_sql_schema = getModule(__name__).filePath.sibling("sql_schema").child("current.sql").getContent()
 
@@ -868,7 +869,282 @@
 
     # End of IMIP
 
+    # Groups
 
+    @classproperty
+    def _addGroupQuery(cls): #@NoSelf
+        gr = schema.GROUPS
+        return Insert({gr.NAME: Parameter("name"),
+                       gr.GROUP_GUID: Parameter("groupGUID"),
+                       gr.MEMBERSHIP_HASH: Parameter("membershipHash")})
+
+
+    @classproperty
+    def _updateGroupQuery(cls): #@NoSelf
+        gr = schema.GROUPS
+        return Update({gr.MEMBERSHIP_HASH: Parameter("membershipHash"),
+            gr.NAME: Parameter("name"), gr.MODIFIED: Parameter("timestamp")},
+            Where=(gr.GROUP_GUID == Parameter("groupGUID")))
+
+
+    @classproperty
+    def _groupByGUID(cls): #@NoSelf
+        gr = schema.GROUPS
+        return Select([gr.GROUP_ID, gr.NAME, gr.MEMBERSHIP_HASH], From=gr,
+                Where=(
+                    gr.GROUP_GUID == Parameter("groupGUID")
+                )
+            )
+
+
+    @classproperty
+    def _groupByID(cls): #@NoSelf
+        gr = schema.GROUPS
+        return Select([gr.GROUP_GUID, gr.NAME, gr.MEMBERSHIP_HASH], From=gr,
+                Where=(
+                    gr.GROUP_ID == Parameter("groupID")
+                )
+            )
+
+
+    @classproperty
+    def _deleteGroup(cls): #@NoSelf
+        gr = schema.GROUPS
+        return Delete(From=gr,
+              Where=(gr.GROUP_ID == Parameter("groupID")))
+
+
+    def addGroup(self, groupGUID, name, membershipHash):
+        return self._addGroupQuery.on(self, name=name,
+            groupGUID=groupGUID, membershipHash=membershipHash)
+
+
+    def updateGroup(self, groupGUID, name, membershipHash):
+        timestamp = datetime.datetime.utcnow()
+        return self._updateGroupQuery.on(self, name=name,
+            groupGUID=groupGUID, timestamp=timestamp,
+            membershipHash=membershipHash)
+
+
+    def groupByGUID(self, groupGUID):
+        return self._groupByGUID.on(self, groupGUID=groupGUID)
+
+
+    def groupByID(self, groupID):
+        return self._groupByID.on(self, groupID=groupID)
+
+
+    def deleteGroup(self, groupID):
+        return self._deleteGroup.on(self, groupID=groupID)
+
+    # End of Groups
+
+
+    # Group Members
+
+    @classproperty
+    def _addMemberToGroupQuery(cls): #@NoSelf
+        gm = schema.GROUP_MEMBERSHIP
+        return Insert({gm.GROUP_ID: Parameter("groupID"),
+                       gm.MEMBER_GUID: Parameter("memberGUID")})
+    @classproperty
+    def _removeMemberFromGroupQuery(cls): #@NoSelf
+        gm = schema.GROUP_MEMBERSHIP
+        return Delete(From=gm,
+            Where=(gm.GROUP_ID == Parameter("groupID")).And(
+                   gm.MEMBER_GUID == Parameter("memberGUID")))
+
+    @classproperty
+    def _selectGroupMembersQuery(cls): #@NoSelf
+        gm = schema.GROUP_MEMBERSHIP
+        return Select([gm.MEMBER_GUID], From=gm,
+                Where=(
+                    gm.GROUP_ID == Parameter("groupID")
+                )
+            )
+
+    def addMemberToGroup(self, memberGUID, groupID):
+        return self._addMemberToGroupQuery.on(self,
+            groupID=groupID, memberGUID=memberGUID)
+
+    def removeMemberFromGroup(self, memberGUID, groupID):
+        return self._removeMemberFromGroupQuery.on(self,
+            groupID=groupID, memberGUID=memberGUID)
+
+    def membersOfGroup(self, groupID):
+        return self._selectGroupMembersQuery.on(self, groupID=groupID)
+
+    # End of Group Members
+
+    # Delegates
+
+    @classproperty
+    def _addDelegateQuery(cls): #@NoSelf
+        de = schema.DELEGATES
+        return Insert({de.DELEGATOR: Parameter("delegator"),
+                       de.DELEGATE: Parameter("delegate"),
+                       de.READ_WRITE: Parameter("readWrite"),
+                       })
+
+    @classproperty
+    def _addDelegateGroupQuery(cls): #@NoSelf
+        ds = schema.DELEGATE_GROUPS
+        return Insert({ds.DELEGATOR: Parameter("delegator"),
+                       ds.GROUP_ID: Parameter("groupID"),
+                       ds.READ_WRITE: Parameter("readWrite"),
+                       })
+
+    @classproperty
+    def _removeDelegateQuery(cls): #@NoSelf
+        de = schema.DELEGATES
+        return Delete(From=de,
+            Where=(de.DELEGATOR == Parameter("delegator")).And(
+                   de.DELEGATE == Parameter("delegate")).And(
+                   de.READ_WRITE == Parameter("readWrite"))
+            )
+
+    @classproperty
+    def _removeDelegateGroupQuery(cls): #@NoSelf
+        ds = schema.DELEGATE_GROUPS
+        return Delete(From=ds,
+            Where=(ds.DELEGATOR == Parameter("delegator")).And(
+                   ds.GROUP_ID == Parameter("groupID")).And(
+                   ds.READ_WRITE == Parameter("readWrite"))
+            )
+
+    @classproperty
+    def _selectDelegatesQuery(cls): #@NoSelf
+        de = schema.DELEGATES
+        return Select([de.DELEGATE], From=de,
+                Where=(de.DELEGATOR == Parameter("delegator")).And(
+                    de.READ_WRITE == Parameter("readWrite"))
+                )
+
+    @classproperty
+    def _selectDelegateGroupsQuery(cls): #@NoSelf
+        ds = schema.DELEGATE_GROUPS
+        return Select([ds.GROUP_ID], From=ds,
+                Where=(
+                    ds.DELEGATOR == Parameter("delegator")).And(
+                    ds.READ_WRITE == Parameter("readWrite"))
+                )
+
+    @classproperty
+    def _selectDirectDelegatorsQuery(cls): #@NoSelf
+        de = schema.DELEGATES
+        return Select([de.DELEGATOR], From=de,
+                Where=(
+                    de.DELEGATE == Parameter("delegate")).And(
+                    de.READ_WRITE == Parameter("readWrite"))
+                )
+
+    """
+    @classproperty
+    def _selectDelegatorsGroupsQuery(cls): #@NoSelf
+        ds = schema.DELEGATE_GROUPS
+        return Select([ds.DELEGATOR], From=ds,
+                Where=(
+                    ds.GROUP_ID == Parameter("groupID").And(
+                    ds.READ_WRITE == Parameter("readWrite"))
+                )
+            )
+    """
+
+    @classproperty
+    def _selectIndirectDelegatorsQuery(cls): #@NoSelf
+        dg = schema.DELEGATE_GROUPS
+        gm = schema.GROUP_MEMBERSHIP
+
+        return Select(
+            [dg.DELEGATOR],
+            From=dg,
+            Where=(
+                dg.GROUP_ID.In(
+                    Select(
+                        [gm.GROUP_ID],
+                        From=gm,
+                        Where=(gm.MEMBER_GUID == Parameter("delegate"))
+                    )
+                ).And(
+                    dg.READ_WRITE == Parameter("readWrite")
+                )
+            )
+        )
+
+    @classproperty
+    def _selectIndirectDelegatesQuery(cls): #@NoSelf
+        dg = schema.DELEGATE_GROUPS
+        gm = schema.GROUP_MEMBERSHIP
+
+        return Select(
+            [gm.MEMBER_GUID],
+            From=gm,
+            Where=(
+                gm.GROUP_ID.In(
+                    Select(
+                        [dg.GROUP_ID],
+                        From=dg,
+                        Where=(dg.DELEGATOR == Parameter("delegator")).And(
+                            dg.READ_WRITE == Parameter("readWrite"))
+                    )
+                )
+            )
+        )
+
+    def addDelegate(self, delegator, delegate, readWrite, isGroup):
+        if isGroup:
+            return self._addDelegateGroupQuery.on(self, delegator=delegator,
+                groupID=delegate, readWrite=readWrite)
+        else:
+            return self._addDelegateQuery.on(self, delegator=delegator,
+                delegate=delegate, readWrite=readWrite)
+
+    def removeDelegate(self, delegator, delegate, readWrite, isGroup):
+        if isGroup:
+            return self._removeDelegateGroupQuery.on(self, delegator=delegator,
+                groupID=delegate, readWrite=readWrite)
+        else:
+            return self._removeDelegateQuery.on(self, delegator=delegator,
+                delegate=delegate, readWrite=readWrite)
+
+    def directDelegates(self, delegator, readWrite):
+        return self._selectDelegatesQuery.on(self, delegator=delegator,
+            readWrite=readWrite)
+
+    def groupDelegates(self, delegator, readWrite):
+        return self._selectDelegateGroupssQuery.on(self, delegator=delegator,
+            readWrite=readWrite)
+
+    @inlineCallbacks
+    def delegates(self, delegator, readWrite):
+
+        # First get the direct delegates
+        results = (yield self._selectDelegatesQuery.on(self,
+            delegator=delegator, readWrite=readWrite))
+
+        # Finally get those who are in groups which have been delegated to
+        results.extend((yield self._selectIndirectDelegatesQuery.on(self,
+            delegator=delegator, readWrite=readWrite)))
+
+        returnValue(results)
+
+
+    @inlineCallbacks
+    def delegators(self, delegate, readWrite):
+
+        # First get the direct delegators
+        results = (yield self._selectDirectDelegatorsQuery.on(self,
+            delegate=delegate, readWrite=readWrite))
+
+        # Finally get those who have delegated to groups the delegate
+        # is a member of
+        results.extend((yield self._selectIndirectDelegatorsQuery.on(self,
+            delegate=delegate, readWrite=readWrite)))
+        returnValue(results)
+
+    # End of Delegates
+
+
     def preCommit(self, operation):
         """
         Run things before C{commit}.  (Note: only provided by SQL

Modified: CalendarServer/branches/users/sagen/groupcacher/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/sagen/groupcacher/txdav/common/datastore/sql_schema/current.sql	2013-11-05 20:05:41 UTC (rev 11890)
+++ CalendarServer/branches/users/sagen/groupcacher/txdav/common/datastore/sql_schema/current.sql	2013-11-06 20:11:38 UTC (rev 11891)
@@ -685,10 +685,9 @@
   GROUP_ID                      integer
 );
 
-create sequence GROUP_ID_SEQ;
 
 create table GROUPS (
-  GROUP_ID                      integer      primary key default nextval('GROUP_ID_SEQ'),    -- implicit index
+  GROUP_ID                      integer      primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
   NAME                          varchar(255) not null,
   GROUP_GUID                    varchar(255) not null,
   MEMBERSHIP_HASH               varchar(255) not null,
@@ -696,6 +695,7 @@
   CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
   MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP)
 );
+create index GROUPS_GROUP_GUID on GROUPS(GROUP_GUID);
 
 create table GROUP_MEMBERSHIP (
   GROUP_ID                      integer,
@@ -710,6 +710,22 @@
   MEMBERSHIP_HASH               varchar(255) not null
 );
 
+---------------
+-- Delegates --
+---------------
+
+create table DELEGATES (
+  DELEGATOR                     varchar(255) not null,
+  DELEGATE                      varchar(255) not null,
+  READ_WRITE                    integer      not null -- 1 = ReadWrite, 0 = ReadOnly
+);
+
+create table DELEGATE_GROUPS (
+  DELEGATOR                     varchar(255) not null,
+  GROUP_ID                      integer      not null,
+  READ_WRITE                    integer      not null -- 1 = ReadWrite, 0 = ReadOnly
+);
+
 --------------------------
 -- Object Splitter Work --
 --------------------------
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/f9643e87/attachment.html>


More information about the calendarserver-changes mailing list