<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[14103] CalendarServer/trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/14103">14103</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2014-10-24 14:12:40 -0700 (Fri, 24 Oct 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Directory proxy/delegate memcache based caching.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertoolsgatewaypy">CalendarServer/trunk/calendarserver/tools/gateway.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertoolsprincipalspy">CalendarServer/trunk/calendarserver/tools/principals.py</a></li>
<li><a href="#CalendarServertrunkconfcaldavdtestpodAplist">CalendarServer/trunk/conf/caldavd-test-podA.plist</a></li>
<li><a href="#CalendarServertrunkconfcaldavdtestpodBplist">CalendarServer/trunk/conf/caldavd-test-podB.plist</a></li>
<li><a href="#CalendarServertrunktwistedcaldavdirectorycalendaruserproxypy">CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavdirectorycalendaruserproxyloaderpy">CalendarServer/trunk/twistedcaldav/directory/calendaruserproxyloader.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavdirectoryprincipalpy">CalendarServer/trunk/twistedcaldav/directory/principal.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavdirectorytesttest_principalpy">CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavstdconfigpy">CalendarServer/trunk/twistedcaldav/stdconfig.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavtesttest_upgradepy">CalendarServer/trunk/twistedcaldav/test/test_upgrade.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavupgradepy">CalendarServer/trunk/twistedcaldav/upgrade.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastorepoddingdirectorypy">CalendarServer/trunk/txdav/common/datastore/podding/directory.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresqlpy">CalendarServer/trunk/txdav/common/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavdpsserverpy">CalendarServer/trunk/txdav/dps/server.py</a></li>
<li><a href="#CalendarServertrunktxdavwhodelegatespy">CalendarServer/trunk/txdav/who/delegates.py</a></li>
<li><a href="#CalendarServertrunktxdavwhotesttest_delegatespy">CalendarServer/trunk/txdav/who/test/test_delegates.py</a></li>
<li><a href="#CalendarServertrunktxdavwhotesttest_groupspy">CalendarServer/trunk/txdav/who/test/test_groups.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertoolsgatewaypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/gateway.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/gateway.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/calendarserver/tools/gateway.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -48,9 +48,7 @@
</span><span class="cx"> from txdav.who.idirectory import RecordType as CalRecordType
</span><span class="cx"> from twext.who.idirectory import FieldName
</span><span class="cx"> from twisted.python.constants import Names, NamedConstant
</span><del>-from txdav.who.delegates import (
-    addDelegate, removeDelegate, RecordType as DelegateRecordType
-)
</del><ins>+from txdav.who.delegates import Delegates, RecordType as DelegateRecordType
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> attrMap = {
</span><span class="lines">@@ -529,7 +527,7 @@
</span><span class="cx">             returnValue(None)
</span><span class="cx"> 
</span><span class="cx">         txn = self.store.newTransaction()
</span><del>-        yield addDelegate(txn, record, proxyRecord, (proxyType == &quot;write&quot;))
</del><ins>+        yield Delegates.addDelegate(txn, record, proxyRecord, (proxyType == &quot;write&quot;))
</ins><span class="cx">         yield txn.commit()
</span><span class="cx">         yield self.respondWithProxies(command, record, proxyType)
</span><span class="cx"> 
</span><span class="lines">@@ -555,7 +553,7 @@
</span><span class="cx">             returnValue(None)
</span><span class="cx"> 
</span><span class="cx">         txn = self.store.newTransaction()
</span><del>-        yield removeDelegate(txn, record, proxyRecord, (proxyType == &quot;write&quot;))
</del><ins>+        yield Delegates.removeDelegate(txn, record, proxyRecord, (proxyType == &quot;write&quot;))
</ins><span class="cx">         yield txn.commit()
</span><span class="cx">         yield self.respondWithProxies(command, record, proxyType)
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunkcalendarservertoolsprincipalspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/principals.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/principals.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/calendarserver/tools/principals.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -32,9 +32,7 @@
</span><span class="cx"> from twisted.internet import reactor
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue, succeed
</span><span class="cx"> from twistedcaldav.config import config
</span><del>-from txdav.who.delegates import (
-    addDelegate, removeDelegate, RecordType as DelegateRecordType
-)
</del><ins>+from txdav.who.delegates import Delegates, RecordType as DelegateRecordType
</ins><span class="cx"> from txdav.who.idirectory import AutoScheduleMode
</span><span class="cx"> from txdav.who.groups import GroupCacherPollingWork
</span><span class="cx"> 
</span><span class="lines">@@ -608,7 +606,7 @@
</span><span class="cx">             print(&quot;You are not allowed to add proxies for locations or resources via command line when their proxy assignments come from the directory service.&quot;)
</span><span class="cx">             returnValue(None)
</span><span class="cx"> 
</span><del>-    yield _addRemoveProxy(&quot;Added&quot;, addDelegate, store, record, proxyType, *proxyIDs)
</del><ins>+    yield _addRemoveProxy(&quot;Added&quot;, Delegates.addDelegate, store, record, proxyType, *proxyIDs)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -623,9 +621,9 @@
</span><span class="cx">             returnValue(None)
</span><span class="cx"> 
</span><span class="cx">     # Write
</span><del>-    yield _addRemoveProxy(&quot;Removed&quot;, removeDelegate, store, record, &quot;write&quot;, *proxyIDs)
</del><ins>+    yield _addRemoveProxy(&quot;Removed&quot;, Delegates.removeDelegate, store, record, &quot;write&quot;, *proxyIDs)
</ins><span class="cx">     # Read
</span><del>-    yield _addRemoveProxy(&quot;Removed&quot;, removeDelegate, store, record, &quot;read&quot;, *proxyIDs)
</del><ins>+    yield _addRemoveProxy(&quot;Removed&quot;, Delegates.removeDelegate, store, record, &quot;read&quot;, *proxyIDs)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunkconfcaldavdtestpodAplist"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/conf/caldavd-test-podA.plist (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/conf/caldavd-test-podA.plist        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/conf/caldavd-test-podA.plist        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -140,7 +140,7 @@
</span><span class="cx">                     &lt;key&gt;Port&lt;/key&gt;
</span><span class="cx">                     &lt;integer&gt;11211&lt;/integer&gt;
</span><span class="cx">                   &lt;/dict&gt;
</span><del>-                  &lt;key&gt;ProxyDB&lt;/key&gt;
</del><ins>+                  &lt;key&gt;AllPods&lt;/key&gt;
</ins><span class="cx">                   &lt;dict&gt;
</span><span class="cx">                     &lt;key&gt;ClientEnabled&lt;/key&gt;
</span><span class="cx">                     &lt;true/&gt;
</span><span class="lines">@@ -153,6 +153,7 @@
</span><span class="cx">                     &lt;key&gt;HandleCacheTypes&lt;/key&gt;
</span><span class="cx">                     &lt;array&gt;
</span><span class="cx">                       &lt;string&gt;ProxyDB&lt;/string&gt;
</span><ins>+                      &lt;string&gt;DelegatesDB&lt;/string&gt;
</ins><span class="cx">                       &lt;string&gt;PrincipalToken&lt;/string&gt;
</span><span class="cx">                       &lt;string&gt;DIGESTCREDENTIALS&lt;/string&gt;
</span><span class="cx">                     &lt;/array&gt;
</span></span></pre></div>
<a id="CalendarServertrunkconfcaldavdtestpodBplist"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/conf/caldavd-test-podB.plist (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/conf/caldavd-test-podB.plist        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/conf/caldavd-test-podB.plist        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -147,7 +147,7 @@
</span><span class="cx">                     &lt;key&gt;Port&lt;/key&gt;
</span><span class="cx">                     &lt;integer&gt;11411&lt;/integer&gt;
</span><span class="cx">                   &lt;/dict&gt;
</span><del>-                  &lt;key&gt;ProxyDB&lt;/key&gt;
</del><ins>+                  &lt;key&gt;AllPods&lt;/key&gt;
</ins><span class="cx">                   &lt;dict&gt;
</span><span class="cx">                     &lt;key&gt;ClientEnabled&lt;/key&gt;
</span><span class="cx">                     &lt;true/&gt;
</span><span class="lines">@@ -160,6 +160,7 @@
</span><span class="cx">                     &lt;key&gt;HandleCacheTypes&lt;/key&gt;
</span><span class="cx">                     &lt;array&gt;
</span><span class="cx">                       &lt;string&gt;ProxyDB&lt;/string&gt;
</span><ins>+                      &lt;string&gt;DelegatesDB&lt;/string&gt;
</ins><span class="cx">                       &lt;string&gt;PrincipalToken&lt;/string&gt;
</span><span class="cx">                       &lt;string&gt;DIGESTCREDENTIALS&lt;/string&gt;
</span><span class="cx">                     &lt;/array&gt;
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavdirectorycalendaruserproxypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -208,8 +208,12 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def __repr__(self):
+        return &quot;&lt;{}: {}&gt;&quot;.format(self.__class__.__name__, str(self))
+
+
</ins><span class="cx">     def __str__(self):
</span><del>-        return &quot;%s [%s]&quot; % (self.parent, self.proxyType)
</del><ins>+        return &quot;{} [{}]&quot;.format(self.parent, self.proxyType)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def _index(self):
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavdirectorycalendaruserproxyloaderpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxyloader.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxyloader.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxyloader.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -31,7 +31,7 @@
</span><span class="cx"> from twistedcaldav.config import config, fullServerPath
</span><span class="cx"> from twistedcaldav.xmlutil import readXML
</span><span class="cx"> 
</span><del>-from txdav.who.delegates import addDelegate
</del><ins>+from txdav.who.delegates import Delegates
</ins><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="lines">@@ -160,13 +160,13 @@
</span><span class="cx">                 if delegateRecord is None:
</span><span class="cx">                     continue
</span><span class="cx"> 
</span><del>-                yield addDelegate(txn, delegatorRecord, delegateRecord, True)
</del><ins>+                yield Delegates.addDelegate(txn, delegatorRecord, delegateRecord, True)
</ins><span class="cx"> 
</span><span class="cx">             for proxy in read_proxies:
</span><span class="cx">                 delegateRecord = yield directory.recordWithUID(proxy)
</span><span class="cx">                 if delegateRecord is None:
</span><span class="cx">                     continue
</span><span class="cx"> 
</span><del>-                yield addDelegate(txn, delegatorRecord, delegateRecord, False)
</del><ins>+                yield Delegates.addDelegate(txn, delegatorRecord, delegateRecord, False)
</ins><span class="cx"> 
</span><span class="cx">         yield txn.commit()
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavdirectoryprincipalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/directory/principal.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -821,8 +821,12 @@
</span><span class="cx">         ])
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def __repr__(self):
+        return &quot;&lt;{}: {}&gt;&quot;.format(self.__class__.__name__, str(self))
+
+
</ins><span class="cx">     def __str__(self):
</span><del>-        return &quot;(%s)%s&quot; % (self.record.recordType, self.record.uid)
</del><ins>+        return &quot;({}){}&quot;.format(self.record.recordType, self.record.uid)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __eq__(self, other):
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavdirectorytesttest_principalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -32,7 +32,7 @@
</span><span class="cx"> )
</span><span class="cx"> from twistedcaldav.test.util import StoreTestCase
</span><span class="cx"> 
</span><del>-from txdav.who.delegates import addDelegate
</del><ins>+from txdav.who.delegates import Delegates
</ins><span class="cx"> from txdav.who.idirectory import AutoScheduleMode, RecordType as CalRecordType
</span><span class="cx"> from txdav.xml import element as davxml
</span><span class="cx"> 
</span><span class="lines">@@ -1072,8 +1072,8 @@
</span><span class="cx">         self.assertTrue(len((yield principal03.proxyFor(True))) == 0)
</span><span class="cx"> 
</span><span class="cx">         # Make user01 a read-only proxy for user02 and user03
</span><del>-        yield addDelegate(self.transactionUnderTest(), principal02.record, principal01.record, False)
-        yield addDelegate(self.transactionUnderTest(), principal03.record, principal01.record, False)
</del><ins>+        yield Delegates.addDelegate(self.transactionUnderTest(), principal02.record, principal01.record, False)
+        yield Delegates.addDelegate(self.transactionUnderTest(), principal03.record, principal01.record, False)
</ins><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         self.assertTrue(len((yield principal01.proxyFor(False))) == 2)
</span><span class="lines">@@ -1097,7 +1097,15 @@
</span><span class="cx">         self.assertTrue(len((yield principal01.proxyFor(False))) == 1)
</span><span class="cx">         self.assertTrue(len((yield principal01.proxyFor(True))) == 0)
</span><span class="cx"> 
</span><ins>+        # Remove user01 as read-only proxy for user02 and user03
+        yield Delegates.removeDelegate(self.transactionUnderTest(), principal02.record, principal01.record, False)
+        yield Delegates.removeDelegate(self.transactionUnderTest(), principal03.record, principal01.record, False)
+        yield self.commit()
</ins><span class="cx"> 
</span><ins>+        self.assertTrue(len((yield principal01.proxyFor(False))) == 0)
+        self.assertTrue(len((yield principal01.proxyFor(True))) == 0)
+
+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def test_isProxyFor(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -1119,8 +1127,8 @@
</span><span class="cx">         self.assertFalse((yield principal03.isProxyFor(principal03)))
</span><span class="cx"> 
</span><span class="cx">         # Make user02 a read-only proxy for user01, and user03 a read-write proxy for user01
</span><del>-        yield addDelegate(self.transactionUnderTest(), principal01.record, principal02.record, False)
-        yield addDelegate(self.transactionUnderTest(), principal01.record, principal03.record, True)
</del><ins>+        yield Delegates.addDelegate(self.transactionUnderTest(), principal01.record, principal02.record, False)
+        yield Delegates.addDelegate(self.transactionUnderTest(), principal01.record, principal03.record, True)
</ins><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         # Check proxy for
</span><span class="lines">@@ -1134,7 +1142,26 @@
</span><span class="cx">         self.assertFalse((yield principal03.isProxyFor(principal02)))
</span><span class="cx">         self.assertFalse((yield principal03.isProxyFor(principal03)))
</span><span class="cx"> 
</span><ins>+        # Remove user02 as read-only proxy for user01, and user03 as read-write proxy for user01
+        yield Delegates.removeDelegate(self.transactionUnderTest(), principal01.record, principal02.record, False)
+        yield Delegates.removeDelegate(self.transactionUnderTest(), principal01.record, principal03.record, True)
+        yield self.commit()
</ins><span class="cx"> 
</span><ins>+        # Check proxy for
+        proxies = yield principal01.proxyFor(False)
+        self.assertEqual(proxies, set())
+        proxies = yield principal01.proxyFor(True)
+        self.assertEqual(proxies, set())
+        proxies = yield principal02.proxyFor(False)
+        self.assertEqual(proxies, set())
+        proxies = yield principal02.proxyFor(True)
+        self.assertEqual(proxies, set())
+        proxies = yield principal03.proxyFor(False)
+        self.assertEqual(proxies, set())
+        proxies = yield principal03.proxyFor(True)
+        self.assertEqual(proxies, set())
+
+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def test_proxyMode(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -1155,8 +1182,8 @@
</span><span class="cx">         self.assertEqual(mode, &quot;none&quot;)
</span><span class="cx"> 
</span><span class="cx">         # Make user02 a read-only proxy for user01, and user03 a read-write proxy for user01
</span><del>-        yield addDelegate(self.transactionUnderTest(), principal01.record, principal02.record, False)
-        yield addDelegate(self.transactionUnderTest(), principal01.record, principal03.record, True)
</del><ins>+        yield Delegates.addDelegate(self.transactionUnderTest(), principal01.record, principal02.record, False)
+        yield Delegates.addDelegate(self.transactionUnderTest(), principal01.record, principal03.record, True)
</ins><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         # Check proxy mode
</span><span class="lines">@@ -1169,7 +1196,26 @@
</span><span class="cx">         mode = yield principal01.proxyMode(principal03)
</span><span class="cx">         self.assertEqual(mode, &quot;none&quot;)
</span><span class="cx"> 
</span><ins>+        # Remove user02 as read-only proxy for user01, and user03 as read-write proxy for user01
+        yield Delegates.removeDelegate(self.transactionUnderTest(), principal01.record, principal02.record, False)
+        yield Delegates.removeDelegate(self.transactionUnderTest(), principal01.record, principal03.record, True)
+        yield self.commit()
</ins><span class="cx"> 
</span><ins>+        # Check proxy for
+        proxies = yield principal01.proxyFor(False)
+        self.assertEqual(proxies, set())
+        proxies = yield principal01.proxyFor(True)
+        self.assertEqual(proxies, set())
+        proxies = yield principal02.proxyFor(False)
+        self.assertEqual(proxies, set())
+        proxies = yield principal02.proxyFor(True)
+        self.assertEqual(proxies, set())
+        proxies = yield principal03.proxyFor(False)
+        self.assertEqual(proxies, set())
+        proxies = yield principal03.proxyFor(True)
+        self.assertEqual(proxies, set())
+
+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def test_proxyFor(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -1194,8 +1240,8 @@
</span><span class="cx">         self.assertEqual(proxies, set())
</span><span class="cx"> 
</span><span class="cx">         # Make user02 a read-only proxy for user01, and user03 a read-write proxy for user01
</span><del>-        yield addDelegate(self.transactionUnderTest(), principal01.record, principal02.record, False)
-        yield addDelegate(self.transactionUnderTest(), principal01.record, principal03.record, True)
</del><ins>+        yield Delegates.addDelegate(self.transactionUnderTest(), principal01.record, principal02.record, False)
+        yield Delegates.addDelegate(self.transactionUnderTest(), principal01.record, principal03.record, True)
</ins><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         # Check proxy for
</span><span class="lines">@@ -1211,3 +1257,22 @@
</span><span class="cx">         self.assertEqual(proxies, set())
</span><span class="cx">         proxies = yield principal03.proxyFor(True)
</span><span class="cx">         self.assertEqual(proxies, set((principal01,)))
</span><ins>+
+        # Remove user02 as read-only proxy for user01, and user03 as read-write proxy for user01
+        yield Delegates.removeDelegate(self.transactionUnderTest(), principal01.record, principal02.record, False)
+        yield Delegates.removeDelegate(self.transactionUnderTest(), principal01.record, principal03.record, True)
+        yield self.commit()
+
+        # Check proxy for
+        proxies = yield principal01.proxyFor(False)
+        self.assertEqual(proxies, set())
+        proxies = yield principal01.proxyFor(True)
+        self.assertEqual(proxies, set())
+        proxies = yield principal02.proxyFor(False)
+        self.assertEqual(proxies, set())
+        proxies = yield principal02.proxyFor(True)
+        self.assertEqual(proxies, set())
+        proxies = yield principal03.proxyFor(False)
+        self.assertEqual(proxies, set())
+        proxies = yield principal03.proxyFor(True)
+        self.assertEqual(proxies, set())
</ins></span></pre></div>
<a id="CalendarServertrunktwistedcaldavstdconfigpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -930,6 +930,7 @@
</span><span class="cx">             #     &quot;Port&quot;: 11211,
</span><span class="cx">             #     &quot;HandleCacheTypes&quot;: [
</span><span class="cx">             #         &quot;ProxyDB&quot;,
</span><ins>+            #         &quot;DelegatesDB&quot;,
</ins><span class="cx">             #         &quot;PrincipalToken&quot;,
</span><span class="cx">             #     ]
</span><span class="cx">             # },
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavtesttest_upgradepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/test/test_upgrade.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/test/test_upgrade.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/twistedcaldav/test/test_upgrade.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -32,7 +32,7 @@
</span><span class="cx"> )
</span><span class="cx"> from txdav.caldav.datastore.index_file import db_basename
</span><span class="cx"> from txdav.caldav.datastore.scheduling.imip.mailgateway import MailGatewayTokensDatabase
</span><del>-from txdav.who.delegates import delegatesOf
</del><ins>+from txdav.who.delegates import Delegates
</ins><span class="cx"> from txdav.xml.parser import WebDAVDocument
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -1470,7 +1470,7 @@
</span><span class="cx">         store = self.storeUnderTest()
</span><span class="cx">         record = yield self.directory.recordWithUID(u&quot;mercury&quot;)
</span><span class="cx">         txn = store.newTransaction()
</span><del>-        writeDelegates = yield delegatesOf(txn, record, True)
</del><ins>+        writeDelegates = yield Delegates.delegatesOf(txn, record, True)
</ins><span class="cx">         self.assertEquals(len(writeDelegates), 0)
</span><span class="cx">         yield txn.commit()
</span><span class="cx"> 
</span><span class="lines">@@ -1484,7 +1484,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Check delegates in store
</span><span class="cx">         txn = store.newTransaction()
</span><del>-        writeDelegates = yield delegatesOf(txn, record, True)
</del><ins>+        writeDelegates = yield Delegates.delegatesOf(txn, record, True)
</ins><span class="cx">         self.assertEquals(len(writeDelegates), 1)
</span><span class="cx">         self.assertEquals(
</span><span class="cx">             set([d.uid for d in writeDelegates]),
</span><span class="lines">@@ -1493,7 +1493,7 @@
</span><span class="cx"> 
</span><span class="cx">         record = yield self.directory.recordWithUID(u&quot;non_calendar_proxy&quot;)
</span><span class="cx"> 
</span><del>-        readDelegates = yield delegatesOf(txn, record, False)
</del><ins>+        readDelegates = yield Delegates.delegatesOf(txn, record, False)
</ins><span class="cx">         self.assertEquals(len(readDelegates), 1)
</span><span class="cx">         self.assertEquals(
</span><span class="cx">             set([d.uid for d in readDelegates]),
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavupgradepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/upgrade.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/upgrade.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/twistedcaldav/upgrade.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -53,7 +53,7 @@
</span><span class="cx"> from txdav.caldav.datastore.scheduling.imip.mailgateway import migrateTokensToStore
</span><span class="cx"> from txdav.caldav.datastore.scheduling.scheduler import DirectScheduler
</span><span class="cx"> from txdav.caldav.datastore.util import normalizationLookup
</span><del>-from txdav.who.delegates import addDelegate
</del><ins>+from txdav.who.delegates import Delegates
</ins><span class="cx"> from txdav.who.idirectory import RecordType as CalRecordType
</span><span class="cx"> from txdav.xml import element
</span><span class="cx"> 
</span><span class="lines">@@ -1078,7 +1078,7 @@
</span><span class="cx">             continue
</span><span class="cx"> 
</span><span class="cx">         readWrite = (groupType == &quot;calendar-proxy-write&quot;)
</span><del>-        yield addDelegate(txn, delegatorRecord, delegateRecord, readWrite)
</del><ins>+        yield Delegates.addDelegate(txn, delegatorRecord, delegateRecord, readWrite)
</ins><span class="cx"> 
</span><span class="cx">     yield txn.commit()
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastorepoddingdirectorypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/podding/directory.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/podding/directory.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/txdav/common/datastore/podding/directory.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -17,7 +17,7 @@
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> from twisted.python.reflect import namedClass
</span><span class="cx"> from txdav.common.datastore.podding.base import FailedCrossPodRequestError
</span><del>-from txdav.who.delegates import _delegatesOfUIDs, _delegatedToUIDs, setDelegates
</del><ins>+from txdav.who.delegates import Delegates
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class DirectoryPoddingConduitMixin(object):
</span><span class="lines">@@ -81,7 +81,7 @@
</span><span class="cx">                     raise FailedCrossPodRequestError(&quot;Cross-pod delegate missing on this server: {}&quot;.format(uid))
</span><span class="cx">                 delegates.append(delegate)
</span><span class="cx"> 
</span><del>-            yield setDelegates(txn, delegator, delegates, request[&quot;read-write&quot;])
</del><ins>+            yield Delegates.setDelegates(txn, delegator, delegates, request[&quot;read-write&quot;])
</ins><span class="cx">         except Exception as e:
</span><span class="cx">             returnValue({
</span><span class="cx">                 &quot;result&quot;: &quot;exception&quot;,
</span><span class="lines">@@ -140,7 +140,7 @@
</span><span class="cx">             if delegator is None or not delegator.thisServer():
</span><span class="cx">                 raise FailedCrossPodRequestError(&quot;Cross-pod delegate not on this server: {}&quot;.format(delegator.uid))
</span><span class="cx"> 
</span><del>-            delegates = yield _delegatesOfUIDs(txn, delegator, request[&quot;read-write&quot;], request[&quot;expanded&quot;])
</del><ins>+            delegates = yield Delegates._delegatesOfUIDs(txn, delegator, request[&quot;read-write&quot;], request[&quot;expanded&quot;])
</ins><span class="cx">         except Exception as e:
</span><span class="cx">             returnValue({
</span><span class="cx">                 &quot;result&quot;: &quot;exception&quot;,
</span><span class="lines">@@ -201,7 +201,7 @@
</span><span class="cx">             if delegate is None or delegate.thisServer():
</span><span class="cx">                 raise FailedCrossPodRequestError(&quot;Cross-pod delegate missing or on this server: {}&quot;.format(delegate.uid))
</span><span class="cx"> 
</span><del>-            delegateors = yield _delegatedToUIDs(txn, delegate, request[&quot;read-write&quot;], onlyThisServer=True)
</del><ins>+            delegateors = yield Delegates._delegatedToUIDs(txn, delegate, request[&quot;read-write&quot;], onlyThisServer=True)
</ins><span class="cx">         except Exception as e:
</span><span class="cx">             returnValue({
</span><span class="cx">                 &quot;result&quot;: &quot;exception&quot;,
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/sql.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -80,6 +80,7 @@
</span><span class="cx"> from txdav.common.inotifications import INotificationCollection, \
</span><span class="cx">     INotificationObject
</span><span class="cx"> from txdav.idav import ChangeCategory
</span><ins>+from txdav.who.delegates import Delegates
</ins><span class="cx"> from txdav.xml import element
</span><span class="cx"> 
</span><span class="cx"> from uuid import uuid4, UUID
</span><span class="lines">@@ -1103,12 +1104,12 @@
</span><span class="cx">         if record is None:
</span><span class="cx">             returnValue(None)
</span><span class="cx"> 
</span><del>-        groupID = yield self._addGroupQuery.on(
</del><ins>+        groupID = (yield self._addGroupQuery.on(
</ins><span class="cx">             self,
</span><span class="cx">             name=name.encode(&quot;utf-8&quot;),
</span><span class="cx">             groupUID=groupUID.encode(&quot;utf-8&quot;),
</span><span class="cx">             membershipHash=membershipHash
</span><del>-        )
</del><ins>+        ))[0][0]
</ins><span class="cx"> 
</span><span class="cx">         yield self.refreshGroup(
</span><span class="cx">             groupUID, record, groupID, name.encode(&quot;utf-8&quot;), membershipHash, True
</span><span class="lines">@@ -1184,7 +1185,7 @@
</span><span class="cx">                         bool(results[0][4]),  # extant
</span><span class="cx">                     ))
</span><span class="cx">                 else:
</span><del>-                    raise
</del><ins>+                    returnValue((None, None, None, None, None))
</ins><span class="cx">             else:
</span><span class="cx">                 yield savepoint.release(self)
</span><span class="cx">                 results = (
</span><span class="lines">@@ -1201,7 +1202,7 @@
</span><span class="cx">                         bool(results[0][4]),  # extant
</span><span class="cx">                     ))
</span><span class="cx">                 else:
</span><del>-                    raise
</del><ins>+                    returnValue((None, None, None, None, None))
</ins><span class="cx">         else:
</span><span class="cx">             returnValue((None, None, None, None, None))
</span><span class="cx"> 
</span><span class="lines">@@ -1375,33 +1376,58 @@
</span><span class="cx">             )
</span><span class="cx"> 
</span><span class="cx">         if membershipChanged:
</span><del>-            newMemberUIDs = set()
-            for memberUID in memberUIDs:
-                newMemberUIDs.add(memberUID)
-            yield self.synchronizeMembers(groupID, newMemberUIDs)
</del><ins>+            yield self.synchronizeMembers(groupID, set(memberUIDs))
</ins><span class="cx"> 
</span><span class="cx">         returnValue(membershipChanged)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def synchronizeMembers(self, groupID, newMemberUIDs):
</span><ins>+        &quot;&quot;&quot;
+        Update the group membership table in the database to match the new membership list. This
+        method will diff the existing set with the new set and apply the changes. It also calls out
+        to a groupChanged() method with the set of added and removed members so that other modules
+        that depend on groups can monitor the changes.
+
+        @param groupID: group id of group to update
+        @type groupID: L{str}
+        @param newMemberUIDs: set of new member UIDs in the group
+        @type newMemberUIDs: L{set} of L{str}
+        &quot;&quot;&quot;
</ins><span class="cx">         numRemoved = numAdded = 0
</span><span class="cx">         cachedMemberUIDs = (yield self.groupMemberUIDs(groupID))
</span><span class="cx"> 
</span><del>-        for memberUID in cachedMemberUIDs:
-            if memberUID not in newMemberUIDs:
-                numRemoved += 1
-                yield self.removeMemberFromGroup(memberUID, groupID)
</del><ins>+        removed = cachedMemberUIDs - newMemberUIDs
+        for memberUID in removed:
+            numRemoved += 1
+            yield self.removeMemberFromGroup(memberUID, groupID)
</ins><span class="cx"> 
</span><del>-        for memberUID in newMemberUIDs:
-            if memberUID not in cachedMemberUIDs:
-                numAdded += 1
-                yield self.addMemberToGroup(memberUID, groupID)
</del><ins>+        added = newMemberUIDs - cachedMemberUIDs
+        for memberUID in added:
+            numAdded += 1
+            yield self.addMemberToGroup(memberUID, groupID)
</ins><span class="cx"> 
</span><ins>+        yield self.groupChanged(groupID, added, removed)
+
</ins><span class="cx">         returnValue((numAdded, numRemoved))
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><ins>+    def groupChanged(self, groupID, addedUIDs, removedUIDs):
+        &quot;&quot;&quot;
+        Called when membership of a group changes.
+
+        @param groupID: group id of group that changed
+        @type groupID: L{str}
+        @param addedUIDs: set of new member UIDs added to the group
+        @type addedUIDs: L{set} of L{str}
+        @param removedUIDs: set of old member UIDs removed from the group
+        @type removedUIDs: L{set} of L{str}
+        &quot;&quot;&quot;
+        yield Delegates.groupChanged(self, groupID, addedUIDs, removedUIDs)
+
+
+    @inlineCallbacks
</ins><span class="cx">     def groupMembers(self, groupID):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         The members of the given group as recorded in the db
</span><span class="lines">@@ -1826,41 +1852,40 @@
</span><span class="cx">         @rtype: a Deferred resulting in a set
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         delegates = set()
</span><ins>+        delegatorU = delegator.encode(&quot;utf-8&quot;)
</ins><span class="cx"> 
</span><span class="cx">         # First get the direct delegates
</span><span class="cx">         results = (
</span><span class="cx">             yield self._selectDelegatesQuery.on(
</span><span class="cx">                 self,
</span><del>-                delegator=delegator.encode(&quot;utf-8&quot;),
</del><ins>+                delegator=delegatorU,
</ins><span class="cx">                 readWrite=1 if readWrite else 0
</span><span class="cx">             )
</span><span class="cx">         )
</span><del>-        for row in results:
-            delegates.add(row[0].decode(&quot;utf-8&quot;))
</del><ins>+        delegates.update([row[0].decode(&quot;utf-8&quot;) for row in results])
</ins><span class="cx"> 
</span><span class="cx">         if expanded:
</span><span class="cx">             # Get those who are in groups which have been delegated to
</span><span class="cx">             results = (
</span><span class="cx">                 yield self._selectIndirectDelegatesQuery.on(
</span><span class="cx">                     self,
</span><del>-                    delegator=delegator.encode(&quot;utf-8&quot;),
</del><ins>+                    delegator=delegatorU,
</ins><span class="cx">                     readWrite=1 if readWrite else 0
</span><span class="cx">                 )
</span><span class="cx">             )
</span><del>-            for row in results:
-                delegates.add(row[0].decode(&quot;utf-8&quot;))
</del><ins>+            # Skip the delegator if they are in one of the groups
+            delegates.update([row[0].decode(&quot;utf-8&quot;) for row in results if row[0] != delegatorU])
</ins><span class="cx"> 
</span><span class="cx">         else:
</span><span class="cx">             # Get the directly-delegated-to groups
</span><span class="cx">             results = (
</span><span class="cx">                 yield self._selectDelegateGroupsQuery.on(
</span><span class="cx">                     self,
</span><del>-                    delegator=delegator.encode(&quot;utf-8&quot;),
</del><ins>+                    delegator=delegatorU,
</ins><span class="cx">                     readWrite=1 if readWrite else 0
</span><span class="cx">                 )
</span><span class="cx">             )
</span><del>-            for row in results:
-                delegates.add(row[0].decode(&quot;utf-8&quot;))
</del><ins>+            delegates.update([row[0].decode(&quot;utf-8&quot;) for row in results])
</ins><span class="cx"> 
</span><span class="cx">         returnValue(delegates)
</span><span class="cx"> 
</span><span class="lines">@@ -1881,29 +1906,29 @@
</span><span class="cx">         @rtype: a Deferred resulting in a set
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         delegators = set()
</span><ins>+        delegateU = delegate.encode(&quot;utf-8&quot;)
</ins><span class="cx"> 
</span><span class="cx">         # First get the direct delegators
</span><span class="cx">         results = (
</span><span class="cx">             yield self._selectDirectDelegatorsQuery.on(
</span><span class="cx">                 self,
</span><del>-                delegate=delegate.encode(&quot;utf-8&quot;),
</del><ins>+                delegate=delegateU,
</ins><span class="cx">                 readWrite=1 if readWrite else 0
</span><span class="cx">             )
</span><span class="cx">         )
</span><del>-        for row in results:
-            delegators.add(row[0].decode(&quot;utf-8&quot;))
</del><ins>+        delegators.update([row[0].decode(&quot;utf-8&quot;) for row in results])
</ins><span class="cx"> 
</span><span class="cx">         # Finally get those who have delegated to groups the delegate
</span><span class="cx">         # is a member of
</span><span class="cx">         results = (
</span><span class="cx">             yield self._selectIndirectDelegatorsQuery.on(
</span><span class="cx">                 self,
</span><del>-                delegate=delegate.encode(&quot;utf-8&quot;),
</del><ins>+                delegate=delegateU,
</ins><span class="cx">                 readWrite=1 if readWrite else 0
</span><span class="cx">             )
</span><span class="cx">         )
</span><del>-        for row in results:
-            delegators.add(row[0].decode(&quot;utf-8&quot;))
</del><ins>+        # Skip the delegator if they are in one of the groups
+        delegators.update([row[0].decode(&quot;utf-8&quot;) for row in results if row[0] != delegateU])
</ins><span class="cx"> 
</span><span class="cx">         returnValue(delegators)
</span><span class="cx"> 
</span><span class="lines">@@ -1924,7 +1949,6 @@
</span><span class="cx">         @rtype: a Deferred resulting in a set
</span><span class="cx"> 
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        delegators = set()
</del><span class="cx">         results = (
</span><span class="cx">             yield self._selectDelegatorsToGroupQuery.on(
</span><span class="cx">                 self,
</span><span class="lines">@@ -1932,8 +1956,7 @@
</span><span class="cx">                 readWrite=1 if readWrite else 0
</span><span class="cx">             )
</span><span class="cx">         )
</span><del>-        for row in results:
-            delegators.add(row[0].decode(&quot;utf-8&quot;))
</del><ins>+        delegators = set([row[0].decode(&quot;utf-8&quot;) for row in results])
</ins><span class="cx">         returnValue(delegators)
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavdpsserverpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/dps/server.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/dps/server.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/txdav/dps/server.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -29,6 +29,7 @@
</span><span class="cx"> from twisted.protocols import amp
</span><span class="cx"> from twisted.python.constants import Names, NamedConstant
</span><span class="cx"> from twisted.python.usage import Options, UsageError
</span><ins>+from twistedcaldav import memcachepool
</ins><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
</span><span class="cx"> from txdav.dps.commands import (
</span><span class="lines">@@ -796,6 +797,14 @@
</span><span class="cx">             raise
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+        #
+        # Configure Memcached Client Pool
+        #
+        memcachepool.installPools(
+            config.Memcached.Pools,
+            config.Memcached.MaxClients,
+        )
+
</ins><span class="cx">         log.info(&quot;Created directory service&quot;)
</span><span class="cx"> 
</span><span class="cx">         return strPortsService(
</span></span></pre></div>
<a id="CalendarServertrunktxdavwhodelegatespy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/delegates.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/delegates.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/txdav/who/delegates.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -24,6 +24,7 @@
</span><span class="cx">     DeferredList
</span><span class="cx"> 
</span><span class="cx"> from twistedcaldav.config import config
</span><ins>+from twistedcaldav.memcacher import Memcacher
</ins><span class="cx"> 
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from twext.who.idirectory import (
</span><span class="lines">@@ -78,11 +79,11 @@
</span><span class="cx">                 RecordType.readDelegateGroup, RecordType.writeDelegateGroup
</span><span class="cx">             ):  # Members are delegates of this record
</span><span class="cx">                 readWrite = (self.recordType is RecordType.writeDelegateGroup)
</span><del>-                delegateUIDs = yield _delegatesOfUIDs(txn, parentRecord, readWrite, expanded=expanded)
</del><ins>+                delegateUIDs = yield Delegates._delegatesOfUIDs(txn, parentRecord, readWrite, expanded=expanded)
</ins><span class="cx"> 
</span><span class="cx">             else:  # Members have delegated to this record
</span><span class="cx">                 readWrite = (self.recordType is RecordType.writeDelegatorGroup)
</span><del>-                delegateUIDs = yield _delegatedToUIDs(txn, parentRecord, readWrite)
</del><ins>+                delegateUIDs = yield Delegates._delegatedToUIDs(txn, parentRecord, readWrite)
</ins><span class="cx">             returnValue(delegateUIDs)
</span><span class="cx"> 
</span><span class="cx">         delegateUIDs = yield self.service._store.inTransaction(
</span><span class="lines">@@ -130,7 +131,7 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         def _setMembers(txn):
</span><del>-            return setDelegates(txn, delegator, memberRecords, readWrite)
</del><ins>+            return Delegates.setDelegates(txn, delegator, memberRecords, readWrite)
</ins><span class="cx"> 
</span><span class="cx">         yield self.service._store.inTransaction(
</span><span class="cx">             &quot;DirectoryRecord.setMembers&quot;, _setMembers
</span><span class="lines">@@ -227,249 +228,398 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-@inlineCallbacks
-def setDelegates(txn, delegator, delegates, readWrite):
</del><ins>+class CachingDelegates(object):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    Sets the full set of delegates for a delegator.
</del><ins>+    Manages access to the store's delegates API, including caching of results.
+    &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-    We need to take multiple pods into account by re-directing this request
-    to the cross-pod conduit if the delegator is not local to this pod.
</del><ins>+    class DelegatesMemcacher(Memcacher):
</ins><span class="cx"> 
</span><del>-    @param delegator: the delegator's directory record
-    @type delegator: L{IDirectoryRecord}
-    @param delegates: the delegates directory records
-    @type delegates: L{list}} of L{IDirectoryRecord}
-    @param readWrite: if True, read and write access is granted; read-only
-        access otherwise
-    &quot;&quot;&quot;
-    if delegator.thisServer():
-        yield txn.removeDelegates(delegator.uid, readWrite)
-        yield txn.removeDelegateGroups(delegator.uid, readWrite)
</del><ins>+        def __init__(self, namespace):
+            super(CachingDelegates.DelegatesMemcacher, self).__init__(namespace, key_normalization=True)
</ins><span class="cx"> 
</span><del>-        for delegate in delegates:
-            yield addDelegate(txn, delegator, delegate, readWrite)
-    else:
-        yield _podSetDelegates(txn, delegator, delegates, readWrite)
</del><ins>+        def _key(self, keyname, uid, readWrite, expanded):
+            return &quot;{}{}:{}#{}&quot;.format(
+                keyname,
+                &quot;-expanded&quot; if expanded else &quot;&quot;,
+                uid,
+                &quot;write&quot; if readWrite else &quot;read&quot;,
+            )
</ins><span class="cx"> 
</span><ins>+        def _membersKey(self, uid, readWrite, expanded):
+            return self._key(&quot;members&quot;, uid, readWrite, expanded)
</ins><span class="cx"> 
</span><ins>+        def _membershipsKey(self, uid, readWrite):
+            return self._key(&quot;memberships&quot;, uid, readWrite, False)
</ins><span class="cx"> 
</span><del>-@inlineCallbacks
-def addDelegate(txn, delegator, delegate, readWrite):
-    &quot;&quot;&quot;
-    Adds &quot;delegate&quot; as a delegate of &quot;delegator&quot;.  The type of access is
-    specified by the &quot;readWrite&quot; parameter.
</del><ins>+        def setMembers(self, uid, readWrite, members, expanded):
+            return self.set(
+                self._membersKey(uid, readWrite, expanded),
+                &quot;,&quot;.join(members),
+            )
</ins><span class="cx"> 
</span><del>-    @param delegator: the delegator's directory record
-    @type delegator: L{IDirectoryRecord}
-    @param delegate: the delegate's directory record
-    @type delegate: L{IDirectoryRecord}
-    @param readWrite: if True, read and write access is granted; read-only
-        access otherwise
-    &quot;&quot;&quot;
-    if delegate.recordType == BaseRecordType.group:
-        # find the groupID
-        (
-            groupID, _ignore_name, _ignore_membershipHash, _ignore_modified,
-            _ignore_extant
-        ) = yield txn.groupByUID(
-            delegate.uid
-        )
-        yield txn.addDelegateGroup(delegator.uid, groupID, readWrite)
-    else:
-        yield txn.addDelegate(delegator.uid, delegate.uid, readWrite)
</del><ins>+        def setMemberships(self, uid, readWrite, memberships):
+            return self.set(
+                self._membershipsKey(uid, readWrite),
+                &quot;,&quot;.join(memberships),
+            )
</ins><span class="cx"> 
</span><ins>+        @staticmethod
+        def _value_decode(value):
+            if value:
+                return set(value.split(&quot;,&quot;))
+            elif value is None:
+                return None
+            else:
+                return set()
</ins><span class="cx"> 
</span><ins>+        @inlineCallbacks
+        def getMembers(self, uid, readWrite, expanded):
+            value = yield self.get(self._membersKey(uid, readWrite, expanded))
+            returnValue(self._value_decode(value))
</ins><span class="cx"> 
</span><del>-@inlineCallbacks
-def removeDelegate(txn, delegator, delegate, readWrite):
-    &quot;&quot;&quot;
-    Removes &quot;delegate&quot; as a delegate of &quot;delegator&quot;.  The type of access is
-    specified by the &quot;readWrite&quot; parameter.
</del><ins>+        @inlineCallbacks
+        def getMemberships(self, uid, readWrite):
+            value = yield self.get(self._membershipsKey(uid, readWrite))
+            returnValue(self._value_decode(value))
</ins><span class="cx"> 
</span><del>-    @param delegator: the delegator's directory record
-    @type delegator: L{IDirectoryRecord}
-    @param delegate: the delegate's directory record
-    @type delegate: L{IDirectoryRecord}
-    @param readWrite: if True, read and write access is revoked; read-only
-        access otherwise
-    &quot;&quot;&quot;
-    if delegate.recordType == BaseRecordType.group:
-        # find the groupID
-        (
-            groupID, _ignore_name, _ignore_membershipHash, _ignore_modified,
-            _ignore_extant
-        ) = yield txn.groupByUID(
-            delegate.uid
-        )
-        yield txn.removeDelegateGroup(delegator.uid, groupID, readWrite)
-    else:
-        yield txn.removeDelegate(delegator.uid, delegate.uid, readWrite)
</del><ins>+        @inlineCallbacks
+        def deleteMember(self, uid, readWrite):
+            &quot;&quot;&quot;
+            Delete both the regular and expanded keys.
+            &quot;&quot;&quot;
+            yield self.delete(self._membersKey(uid, readWrite, False))
+            yield self.delete(self._membersKey(uid, readWrite, True))
</ins><span class="cx"> 
</span><ins>+        @inlineCallbacks
+        def deleteMembership(self, uid, readWrite):
+            &quot;&quot;&quot;
+            Delete both the regular and expanded keys.
+            &quot;&quot;&quot;
+            yield self.delete(self._membershipsKey(uid, readWrite))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-@inlineCallbacks
-def delegatesOf(txn, delegator, readWrite, expanded=False):
-    &quot;&quot;&quot;
-    Return the records of the delegates of &quot;delegator&quot;.  The type of access
-    is specified by the &quot;readWrite&quot; parameter.
</del><ins>+    def __init__(self):
+        self._memcacher = CachingDelegates.DelegatesMemcacher(&quot;DelegatesDB&quot;)
</ins><span class="cx"> 
</span><del>-    @param delegator: the delegator's directory record
-    @type delegator: L{IDirectoryRecord}
-    @param readWrite: if True, read and write access delegates are returned;
-        read-only access otherwise
-    @return: the set of directory records
-    @rtype: a Deferred which fires a set of L{IDirectoryRecord}
-    &quot;&quot;&quot;
-    delegateUIDs = yield _delegatesOfUIDs(txn, delegator, readWrite, expanded)
</del><span class="cx"> 
</span><del>-    records = []
-    directory = delegator.service
-    for uid in delegateUIDs:
-        if uid != delegator.uid:
-            record = (yield directory.recordWithUID(uid))
-            if record is not None:
-                records.append(record)
-    returnValue(records)
</del><ins>+    @inlineCallbacks
+    def setDelegates(self, txn, delegator, delegates, readWrite):
+        &quot;&quot;&quot;
+        Sets the full set of delegates for a delegator.
</ins><span class="cx"> 
</span><ins>+        We need to take multiple pods into account by re-directing this request
+        to the cross-pod conduit if the delegator is not local to this pod.
</ins><span class="cx"> 
</span><ins>+        @param delegator: the delegator's directory record
+        @type delegator: L{IDirectoryRecord}
+        @param delegates: the delegates directory records
+        @type delegates: L{list}} of L{IDirectoryRecord}
+        @param readWrite: if True, read and write access is granted; read-only
+            access otherwise
+        &quot;&quot;&quot;
+        existingDelegates = yield self.delegatesOf(txn, delegator, readWrite)
</ins><span class="cx"> 
</span><del>-@inlineCallbacks
-def delegatedTo(txn, delegate, readWrite):
-    &quot;&quot;&quot;
-    Return the records of those who have delegated to &quot;delegate&quot;.  The type of
-    access is specified by the &quot;readWrite&quot; parameter.
</del><ins>+        if delegator.thisServer():
+            # Remove some
+            for delegate in set(existingDelegates) - set(delegates):
+                yield self.removeDelegate(txn, delegator, delegate, readWrite)
</ins><span class="cx"> 
</span><del>-    @param delegate: the delegate's directory record
-    @type delegate: L{IDirectoryRecord}
-    @param readWrite: if True, read and write access delegators are returned;
-        read-only access otherwise
-    @return: the set of directory records
-    @rtype: a Deferred which fires a set of L{IDirectoryRecord}
-    &quot;&quot;&quot;
-    delegatorUIDs = yield _delegatedToUIDs(txn, delegate, readWrite)
</del><ins>+            for delegate in set(delegates) - set(existingDelegates):
+                yield self.addDelegate(txn, delegator, delegate, readWrite)
+        else:
+            yield self._podSetDelegates(txn, delegator, delegates, readWrite)
</ins><span class="cx"> 
</span><del>-    records = []
-    directory = delegate.service
-    for uid in delegatorUIDs:
-        if uid != delegate.uid:
-            record = (yield directory.recordWithUID(uid))
-            if record is not None:
-                records.append(record)
-    returnValue(records)
</del><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def addDelegate(self, txn, delegator, delegate, readWrite):
+        &quot;&quot;&quot;
+        Adds &quot;delegate&quot; as a delegate of &quot;delegator&quot;.  The type of access is
+        specified by the &quot;readWrite&quot; parameter.
</ins><span class="cx"> 
</span><ins>+        @param delegator: the delegator's directory record
+        @type delegator: L{IDirectoryRecord}
+        @param delegate: the delegate's directory record
+        @type delegate: L{IDirectoryRecord}
+        @param readWrite: if True, read and write access is granted; read-only
+            access otherwise
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-@inlineCallbacks
-def _delegatesOfUIDs(txn, delegator, readWrite, expanded=False):
-    &quot;&quot;&quot;
-    Return the UIDs of the delegates of &quot;delegator&quot;.  The type of access
-    is specified by the &quot;readWrite&quot; parameter.
</del><ins>+        # Never add the delegator as a delegate
+        if delegator.uid == delegate.uid:
+            returnValue(None)
</ins><span class="cx"> 
</span><del>-    We need to take multiple pods into account by re-directing this request
-    to the cross-pod conduit if the delegator is not local to this pod.
</del><ins>+        existingDelegateUIDs = yield self._delegatesOfUIDs(txn, delegator, readWrite, expanded=True)
</ins><span class="cx"> 
</span><del>-    @param delegator: the delegator's directory record
-    @type delegator: L{IDirectoryRecord}
-    @param readWrite: if True, read and write access delegates are returned;
-        read-only access otherwise
-    @return: the set of directory record uids
-    @rtype: a Deferred which fires a set of L{str}
-    &quot;&quot;&quot;
</del><ins>+        if delegate.recordType == BaseRecordType.group:
+            # find the groupID
+            (
+                groupID, _ignore_name, _ignore_membershipHash, _ignore_modified,
+                _ignore_extant
+            ) = yield txn.groupByUID(
+                delegate.uid
+            )
+            yield txn.addDelegateGroup(delegator.uid, groupID, readWrite)
+        else:
+            yield txn.addDelegate(delegator.uid, delegate.uid, readWrite)
</ins><span class="cx"> 
</span><del>-    log.debug(&quot;_delegatesOfUIDs for: {} and read-write = {} and expanded = {}&quot;.format(delegator.uid, readWrite, expanded,))
-    if delegator.thisServer():
-        delegateUIDs = yield txn.delegates(delegator.uid, readWrite, expanded=expanded)
-    else:
-        delegateUIDs = yield _podDelegates(txn, delegator, readWrite, expanded=expanded)
-    returnValue(delegateUIDs)
</del><ins>+        # Update cache (remove the member cache entry first as we need to recalculate it for
+        # memberships removal)
+        yield self._memcacher.deleteMember(delegator.uid, readWrite)
+        newDelegateUIDs = yield self._delegatesOfUIDs(txn, delegator, readWrite, expanded=True)
+        for uid in set(newDelegateUIDs) - set(existingDelegateUIDs):
+            yield self._memcacher.deleteMembership(uid, readWrite)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def removeDelegate(self, txn, delegator, delegate, readWrite):
+        &quot;&quot;&quot;
+        Removes &quot;delegate&quot; as a delegate of &quot;delegator&quot;.  The type of access is
+        specified by the &quot;readWrite&quot; parameter.
</ins><span class="cx"> 
</span><del>-@inlineCallbacks
-def _delegatedToUIDs(txn, delegate, readWrite, onlyThisServer=False):
-    &quot;&quot;&quot;
-    Return the UIDs of those who have delegated to &quot;delegate&quot;.  The type of
-    access is specified by the &quot;readWrite&quot; parameter.
</del><ins>+        @param delegator: the delegator's directory record
+        @type delegator: L{IDirectoryRecord}
+        @param delegate: the delegate's directory record
+        @type delegate: L{IDirectoryRecord}
+        @param readWrite: if True, read and write access is revoked; read-only
+            access otherwise
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-    We need to take multiple pods into account by re-directing this request
-    to the cross-pod conduit if the delegate is not local to this pod.
</del><ins>+        # Never remove the delegator as a delegate
+        if delegator.uid == delegate.uid:
+            returnValue(None)
</ins><span class="cx"> 
</span><del>-    @param delegate: the delegate's directory record
-    @type delegate: L{IDirectoryRecord}
-    @param readWrite: if True, read and write access delegators are returned;
-        read-only access otherwise
-    @param onlyThisServer: used when doing the query as part of a cross-pod request since that
-        should only returns results for this server
-    @type onlyThisServer: L{bool}
-    @return: the set of directory record uids
-    @rtype: a Deferred which fires a set of L{str}
-    &quot;&quot;&quot;
</del><ins>+        existingDelegateUIDs = yield self._delegatesOfUIDs(txn, delegator, readWrite, expanded=True)
</ins><span class="cx"> 
</span><ins>+        if delegate.recordType == BaseRecordType.group:
+            # find the groupID
+            (
+                groupID, _ignore_name, _ignore_membershipHash, _ignore_modified,
+                _ignore_extant
+            ) = yield txn.groupByUID(
+                delegate.uid
+            )
+            yield txn.removeDelegateGroup(delegator.uid, groupID, readWrite)
+        else:
+            yield txn.removeDelegate(delegator.uid, delegate.uid, readWrite)
</ins><span class="cx"> 
</span><del>-    log.debug(&quot;_delegatedToUIDs for: {} and read-write = {}&quot;.format(delegate.uid, readWrite,))
-    delegatorUIDs = (yield txn.delegators(delegate.uid, readWrite))
-    if not onlyThisServer and config.Servers.Enabled:
-        delegatorUIDs.update((yield _podDelegators(txn, delegate, readWrite)))
-    returnValue(delegatorUIDs)
</del><ins>+        # Update cache (remove the member cache entry first as we need to recalculate it for
+        # memberships removal)
+        yield self._memcacher.deleteMember(delegator.uid, readWrite)
+        newDelegateUIDs = yield self._delegatesOfUIDs(txn, delegator, readWrite, expanded=True)
+        for uid in set(existingDelegateUIDs) - set(newDelegateUIDs):
+            yield self._memcacher.deleteMembership(uid, readWrite)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def groupChanged(self, txn, groupID, addedUIDs, removedUIDs):
+        &quot;&quot;&quot;
+        A group has changed. We need to see which delegators might be using this group
+        and invalidate caches.
</ins><span class="cx"> 
</span><del>-def _podSetDelegates(txn, delegator, delegates, readWrite):
-    &quot;&quot;&quot;
-    Sets the full set of delegates for a delegator.
</del><ins>+        @param groupID: group id of group that changed
+        @type groupID: L{str}
+        @param addedUIDs: set of new member UIDs added to the group
+        @type addedUIDs: L{set} of L{str}
+        @param removedUIDs: set of old member UIDs removed from the group
+        @type removedUIDs: L{set} of L{str}
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-    We need to take multiple pods into account by re-directing this request
-    to the cross-pod conduit if the delegator is not local to this pod.
</del><ins>+        # Remove member cache entry for delegators using the group
+        delegators = set()
+        for readWrite in (True, False):
+            delegators.update((yield txn.delegatorsToGroup(groupID, readWrite)))
</ins><span class="cx"> 
</span><del>-    @param delegator: the delegator's directory record
-    @type delegator: L{IDirectoryRecord}
-    @param delegates: the delegates directory records
-    @type delegates: L{list}} of L{IDirectoryRecord}
-    @param readWrite: if True, read and write access is granted; read-only
-        access otherwise
-    &quot;&quot;&quot;
-    return txn.store().conduit.send_set_delegates(txn, delegator, delegates, readWrite)
</del><ins>+        for delegator in delegators:
+            yield self._memcacher.deleteMember(delegator, True)
+            yield self._memcacher.deleteMember(delegator, False)
</ins><span class="cx"> 
</span><ins>+        # Remove membership cache entries for added/removed delegates
+        for delegate in (addedUIDs | removedUIDs):
+            yield self._memcacher.deleteMembership(delegate, True)
+            yield self._memcacher.deleteMembership(delegate, False)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-def _podDelegates(txn, delegator, readWrite, expanded=False):
-    &quot;&quot;&quot;
-    Do a cross-pod request to get the delegates for this delegator.
</del><ins>+    @inlineCallbacks
+    def delegatesOf(self, txn, delegator, readWrite, expanded=False):
+        &quot;&quot;&quot;
+        Return the records of the delegates of &quot;delegator&quot;.  The type of access
+        is specified by the &quot;readWrite&quot; parameter.
</ins><span class="cx"> 
</span><del>-    @param delegator: the delegator's directory record
-    @type delegator: L{IDirectoryRecord}
-    @param readWrite: if True, read and write access delegates are returned;
-        read-only access otherwise
-    @return: the set of directory record uids
-    @rtype: a Deferred which fires a set of L{str}
-    &quot;&quot;&quot;
</del><ins>+        @param delegator: the delegator's directory record
+        @type delegator: L{IDirectoryRecord}
+        @param readWrite: if True, read and write access delegates are returned;
+            read-only access otherwise
+        @return: the set of directory records
+        @rtype: a Deferred which fires a set of L{IDirectoryRecord}
+        &quot;&quot;&quot;
+        delegateUIDs = yield self._delegatesOfUIDs(txn, delegator, readWrite, expanded)
</ins><span class="cx"> 
</span><del>-    log.debug(&quot;_podDelegates for: {} and read-write = {} and expanded = {}&quot;.format(delegator.uid, readWrite, expanded,))
-    return txn.store().conduit.send_get_delegates(txn, delegator, readWrite, expanded)
</del><ins>+        records = []
+        directory = delegator.service
+        for uid in delegateUIDs:
+            if uid != delegator.uid:
+                record = (yield directory.recordWithUID(uid))
+                if record is not None:
+                    records.append(record)
+        returnValue(records)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def delegatedTo(self, txn, delegate, readWrite):
+        &quot;&quot;&quot;
+        Return the records of those who have delegated to &quot;delegate&quot;.  The type of
+        access is specified by the &quot;readWrite&quot; parameter.
</ins><span class="cx"> 
</span><del>-@inlineCallbacks
-def _podDelegators(txn, delegate, readWrite):
-    &quot;&quot;&quot;
-    Do a cross-pod request to get the delegators for this delegate. We need to iterate over all
-    other pod servers to get results from each one.
</del><ins>+        @param delegate: the delegate's directory record
+        @type delegate: L{IDirectoryRecord}
+        @param readWrite: if True, read and write access delegators are returned;
+            read-only access otherwise
+        @return: the set of directory records
+        @rtype: a Deferred which fires a set of L{IDirectoryRecord}
+        &quot;&quot;&quot;
+        delegatorUIDs = yield self._delegatedToUIDs(txn, delegate, readWrite)
</ins><span class="cx"> 
</span><del>-    @param delegate: the delegate's directory record
-    @type delegate: L{IDirectoryRecord}
-    @param readWrite: if True, read and write access delegates are returned;
-        read-only access otherwise
-    @return: the set of directory record uids
-    @rtype: a Deferred which fires a set of L{str}
-    &quot;&quot;&quot;
</del><ins>+        records = []
+        directory = delegate.service
+        for uid in delegatorUIDs:
+            if uid != delegate.uid:
+                record = (yield directory.recordWithUID(uid))
+                if record is not None:
+                    records.append(record)
+        returnValue(records)
</ins><span class="cx"> 
</span><del>-    log.debug(&quot;_podDelegators for: {} and read-write = {}&quot;.format(delegate.uid, readWrite,))
-    results = yield DeferredList([
-        txn.store().conduit.send_get_delegators(txn, server, delegate, readWrite) for
-        server in txn.directoryService().serversDB.allServersExceptThis()
-    ], consumeErrors=True)
-    delegators = set()
-    for result in results:
-        if result and result[0]:
-            delegators.update(result[1])
-    returnValue(delegators)
</del><ins>+
+    @inlineCallbacks
+    def _delegatesOfUIDs(self, txn, delegator, readWrite, expanded=False):
+        &quot;&quot;&quot;
+        Return the UIDs of the delegates of &quot;delegator&quot;.  The type of access
+        is specified by the &quot;readWrite&quot; parameter.
+
+        We need to take multiple pods into account by re-directing this request
+        to the cross-pod conduit if the delegator is not local to this pod.
+
+        @param delegator: the delegator's directory record
+        @type delegator: L{IDirectoryRecord}
+        @param readWrite: if True, read and write access delegates are returned;
+            read-only access otherwise
+        @return: the set of directory record uids
+        @rtype: a Deferred which fires a set of L{str}
+        &quot;&quot;&quot;
+
+        # Try cache first
+        delegateUIDs = yield self._memcacher.getMembers(delegator.uid, readWrite, expanded)
+        if delegateUIDs is not None:
+            log.debug(&quot;_delegatesOfUIDs cached for: {} and read-write = {} and expanded = {}&quot;.format(delegator.uid, readWrite, expanded,))
+            returnValue(delegateUIDs)
+
+        # Get from the store
+        log.debug(&quot;_delegatesOfUIDs for: {} and read-write = {} and expanded = {}&quot;.format(delegator.uid, readWrite, expanded,))
+        if delegator.thisServer():
+            delegateUIDs = yield txn.delegates(delegator.uid, readWrite, expanded=expanded)
+
+            # Cache result - only need to do this on the host
+            yield self._memcacher.setMembers(delegator.uid, readWrite, delegateUIDs, expanded)
+        else:
+            delegateUIDs = yield self._podDelegates(txn, delegator, readWrite, expanded=expanded)
+
+        returnValue(delegateUIDs)
+
+
+    @inlineCallbacks
+    def _delegatedToUIDs(self, txn, delegate, readWrite, onlyThisServer=False):
+        &quot;&quot;&quot;
+        Return the UIDs of those who have delegated to &quot;delegate&quot;.  The type of
+        access is specified by the &quot;readWrite&quot; parameter.
+
+        We need to take multiple pods into account by re-directing this request
+        to the cross-pod conduit if the delegate is not local to this pod.
+
+        @param delegate: the delegate's directory record
+        @type delegate: L{IDirectoryRecord}
+        @param readWrite: if True, read and write access delegators are returned;
+            read-only access otherwise
+        @param onlyThisServer: used when doing the query as part of a cross-pod request since that
+            should only returns results for this server
+        @type onlyThisServer: L{bool}
+        @return: the set of directory record uids
+        @rtype: a Deferred which fires a set of L{str}
+        &quot;&quot;&quot;
+
+        # Try cache first
+        delegatorUIDs = yield self._memcacher.getMemberships(delegate.uid, readWrite)
+        if delegatorUIDs is not None:
+            log.debug(&quot;_delegatedToUIDs cached for: {} and read-write = {}&quot;.format(delegate.uid, readWrite,))
+            returnValue(delegatorUIDs)
+
+        # Get from the store
+        log.debug(&quot;_delegatedToUIDs for: {} and read-write = {}&quot;.format(delegate.uid, readWrite,))
+        delegatorUIDs = (yield txn.delegators(delegate.uid, readWrite))
+        if not onlyThisServer and config.Servers.Enabled:
+            delegatorUIDs.update((yield self._podDelegators(txn, delegate, readWrite)))
+
+        # Cache result - only need to do this on the host
+        yield self._memcacher.setMemberships(delegate.uid, readWrite, delegatorUIDs)
+
+        returnValue(delegatorUIDs)
+
+
+    def _podSetDelegates(self, txn, delegator, delegates, readWrite):
+        &quot;&quot;&quot;
+        Sets the full set of delegates for a delegator.
+
+        We need to take multiple pods into account by re-directing this request
+        to the cross-pod conduit if the delegator is not local to this pod.
+
+        @param delegator: the delegator's directory record
+        @type delegator: L{IDirectoryRecord}
+        @param delegates: the delegates directory records
+        @type delegates: L{list}} of L{IDirectoryRecord}
+        @param readWrite: if True, read and write access is granted; read-only
+            access otherwise
+        &quot;&quot;&quot;
+        return txn.store().conduit.send_set_delegates(txn, delegator, delegates, readWrite)
+
+
+    def _podDelegates(self, txn, delegator, readWrite, expanded=False):
+        &quot;&quot;&quot;
+        Do a cross-pod request to get the delegates for this delegator.
+
+        @param delegator: the delegator's directory record
+        @type delegator: L{IDirectoryRecord}
+        @param readWrite: if True, read and write access delegates are returned;
+            read-only access otherwise
+        @return: the set of directory record uids
+        @rtype: a Deferred which fires a set of L{str}
+        &quot;&quot;&quot;
+
+        log.debug(&quot;_podDelegates for: {} and read-write = {} and expanded = {}&quot;.format(delegator.uid, readWrite, expanded,))
+        return txn.store().conduit.send_get_delegates(txn, delegator, readWrite, expanded)
+
+
+    @inlineCallbacks
+    def _podDelegators(self, txn, delegate, readWrite):
+        &quot;&quot;&quot;
+        Do a cross-pod request to get the delegators for this delegate. We need to iterate over all
+        other pod servers to get results from each one.
+
+        @param delegate: the delegate's directory record
+        @type delegate: L{IDirectoryRecord}
+        @param readWrite: if True, read and write access delegates are returned;
+            read-only access otherwise
+        @return: the set of directory record uids
+        @rtype: a Deferred which fires a set of L{str}
+        &quot;&quot;&quot;
+
+        log.debug(&quot;_podDelegators for: {} and read-write = {}&quot;.format(delegate.uid, readWrite,))
+        results = yield DeferredList([
+            txn.store().conduit.send_get_delegators(txn, server, delegate, readWrite) for
+            server in txn.directoryService().serversDB.allServersExceptThis()
+        ], consumeErrors=True)
+        delegators = set()
+        for result in results:
+            if result and result[0]:
+                delegators.update(result[1])
+        returnValue(delegators)
+
+Delegates = CachingDelegates()
</ins></span></pre></div>
<a id="CalendarServertrunktxdavwhotesttest_delegatespy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/test/test_delegates.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/test/test_delegates.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/txdav/who/test/test_delegates.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -18,10 +18,8 @@
</span><span class="cx"> Delegates implementation tests
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-from txdav.who.delegates import (
-    addDelegate, removeDelegate, delegatesOf, delegatedTo,
-    RecordType as DelegateRecordType
-)
</del><ins>+from txdav.common.datastore.sql import CommonStoreTransaction
+from txdav.who.delegates import Delegates, RecordType as DelegateRecordType
</ins><span class="cx"> from txdav.who.groups import GroupCacher
</span><span class="cx"> from twext.who.idirectory import RecordType
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks
</span><span class="lines">@@ -46,10 +44,10 @@
</span><span class="cx">         delegate2 = yield self.directory.recordWithUID(u&quot;__cdaboo1__&quot;)
</span><span class="cx"> 
</span><span class="cx">         # Add 1 delegate
</span><del>-        yield addDelegate(txn, delegator, delegate1, True)
-        delegates = (yield delegatesOf(txn, delegator, True))
</del><ins>+        yield Delegates.addDelegate(txn, delegator, delegate1, True)
+        delegates = (yield Delegates.delegatesOf(txn, delegator, True))
</ins><span class="cx">         self.assertEquals([u&quot;__sagen1__&quot;], [d.uid for d in delegates])
</span><del>-        delegators = (yield delegatedTo(txn, delegate1, True))
</del><ins>+        delegators = (yield Delegates.delegatedTo(txn, delegate1, True))
</ins><span class="cx">         self.assertEquals([u&quot;__wsanchez1__&quot;], [d.uid for d in delegators])
</span><span class="cx"> 
</span><span class="cx">         yield txn.commit()  # So delegateService will see the changes
</span><span class="lines">@@ -97,27 +95,27 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         # Add another delegate
</span><del>-        yield addDelegate(txn, delegator, delegate2, True)
-        delegates = (yield delegatesOf(txn, delegator, True))
</del><ins>+        yield Delegates.addDelegate(txn, delegator, delegate2, True)
+        delegates = (yield Delegates.delegatesOf(txn, delegator, True))
</ins><span class="cx">         self.assertEquals(
</span><span class="cx">             set([u&quot;__sagen1__&quot;, u&quot;__cdaboo1__&quot;]),
</span><span class="cx">             set([d.uid for d in delegates])
</span><span class="cx">         )
</span><del>-        delegators = (yield delegatedTo(txn, delegate2, True))
</del><ins>+        delegators = (yield Delegates.delegatedTo(txn, delegate2, True))
</ins><span class="cx">         self.assertEquals([u&quot;__wsanchez1__&quot;], [d.uid for d in delegators])
</span><span class="cx"> 
</span><span class="cx">         # Remove 1 delegate
</span><del>-        yield removeDelegate(txn, delegator, delegate1, True)
-        delegates = (yield delegatesOf(txn, delegator, True))
</del><ins>+        yield Delegates.removeDelegate(txn, delegator, delegate1, True)
+        delegates = (yield Delegates.delegatesOf(txn, delegator, True))
</ins><span class="cx">         self.assertEquals([u&quot;__cdaboo1__&quot;], [d.uid for d in delegates])
</span><del>-        delegators = (yield delegatedTo(txn, delegate1, True))
</del><ins>+        delegators = (yield Delegates.delegatedTo(txn, delegate1, True))
</ins><span class="cx">         self.assertEquals(0, len(delegators))
</span><span class="cx"> 
</span><span class="cx">         # Remove the other delegate
</span><del>-        yield removeDelegate(txn, delegator, delegate2, True)
-        delegates = (yield delegatesOf(txn, delegator, True))
</del><ins>+        yield Delegates.removeDelegate(txn, delegator, delegate2, True)
+        delegates = (yield Delegates.delegatesOf(txn, delegator, True))
</ins><span class="cx">         self.assertEquals(0, len(delegates))
</span><del>-        delegators = (yield delegatedTo(txn, delegate2, True))
</del><ins>+        delegators = (yield Delegates.delegatedTo(txn, delegate2, True))
</ins><span class="cx">         self.assertEquals(0, len(delegators))
</span><span class="cx"> 
</span><span class="cx">         yield txn.commit()  # So delegateService will see the changes
</span><span class="lines">@@ -131,7 +129,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Verify the assignments were made
</span><span class="cx">         txn = self.store.newTransaction(label=&quot;test_directDelegation&quot;)
</span><del>-        delegates = (yield delegatesOf(txn, delegator, True))
</del><ins>+        delegates = (yield Delegates.delegatesOf(txn, delegator, True))
</ins><span class="cx">         self.assertEquals(
</span><span class="cx">             set([u&quot;__sagen1__&quot;, u&quot;__cdaboo1__&quot;]),
</span><span class="cx">             set([d.uid for d in delegates])
</span><span class="lines">@@ -143,7 +141,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Verify the assignments were made
</span><span class="cx">         txn = self.store.newTransaction(label=&quot;test_directDelegation&quot;)
</span><del>-        delegates = (yield delegatesOf(txn, delegator, True))
</del><ins>+        delegates = (yield Delegates.delegatesOf(txn, delegator, True))
</ins><span class="cx">         self.assertEquals(
</span><span class="cx">             set([u&quot;__cdaboo1__&quot;]),
</span><span class="cx">             set([d.uid for d in delegates])
</span><span class="lines">@@ -161,31 +159,31 @@
</span><span class="cx">         group2 = yield self.directory.recordWithUID(u&quot;__sub_group_1__&quot;)
</span><span class="cx"> 
</span><span class="cx">         # Add group delegate
</span><del>-        yield addDelegate(txn, delegator, group1, True)
</del><ins>+        yield Delegates.addDelegate(txn, delegator, group1, True)
</ins><span class="cx">         # Passing expanded=False will return the group
</span><del>-        delegates = (yield delegatesOf(txn, delegator, True, expanded=False))
</del><ins>+        delegates = (yield Delegates.delegatesOf(txn, delegator, True, expanded=False))
</ins><span class="cx">         self.assertEquals(1, len(delegates))
</span><span class="cx">         self.assertEquals(delegates[0].uid, u&quot;__top_group_1__&quot;)
</span><span class="cx">         # Passing expanded=True will return not the group -- it only returns
</span><span class="cx">         # non-groups
</span><del>-        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
</del><ins>+        delegates = (yield Delegates.delegatesOf(txn, delegator, True, expanded=True))
</ins><span class="cx">         self.assertEquals(
</span><span class="cx">             set([u&quot;__sagen1__&quot;, u&quot;__cdaboo1__&quot;, u&quot;__glyph1__&quot;]),
</span><span class="cx">             set([d.uid for d in delegates])
</span><span class="cx">         )
</span><del>-        delegators = (yield delegatedTo(txn, delegate1, True))
</del><ins>+        delegators = (yield Delegates.delegatedTo(txn, delegate1, True))
</ins><span class="cx">         self.assertEquals([u&quot;__wsanchez1__&quot;], [d.uid for d in delegators])
</span><span class="cx"> 
</span><span class="cx">         # Verify we can ask for all delegated-to groups
</span><del>-        yield addDelegate(txn, delegator, group2, True)
</del><ins>+        yield Delegates.addDelegate(txn, delegator, group2, True)
</ins><span class="cx">         groups = (yield txn.allGroupDelegates())
</span><span class="cx">         self.assertEquals(
</span><span class="cx">             set([u'__sub_group_1__', u'__top_group_1__']), set(groups)
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         # Delegate to a user who is already indirectly delegated-to
</span><del>-        yield addDelegate(txn, delegator, delegate1, True)
-        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
</del><ins>+        yield Delegates.addDelegate(txn, delegator, delegate1, True)
+        delegates = (yield Delegates.delegatesOf(txn, delegator, True, expanded=True))
</ins><span class="cx">         self.assertEquals(
</span><span class="cx">             set([u&quot;__sagen1__&quot;, u&quot;__cdaboo1__&quot;, u&quot;__glyph1__&quot;]),
</span><span class="cx">             set([d.uid for d in delegates])
</span><span class="lines">@@ -205,23 +203,23 @@
</span><span class="cx">         _ignore_numAdded, _ignore_numRemoved = (
</span><span class="cx">             yield self.groupCacher.synchronizeMembers(txn, groupID, newSet)
</span><span class="cx">         )
</span><del>-        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
</del><ins>+        delegates = (yield Delegates.delegatesOf(txn, delegator, True, expanded=True))
</ins><span class="cx">         self.assertEquals(
</span><span class="cx">             set([u&quot;__sagen1__&quot;, u&quot;__cdaboo1__&quot;, u&quot;__glyph1__&quot;, u&quot;__dre1__&quot;]),
</span><span class="cx">             set([d.uid for d in delegates])
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         # Remove delegate access from the top group
</span><del>-        yield removeDelegate(txn, delegator, group1, True)
-        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
</del><ins>+        yield Delegates.removeDelegate(txn, delegator, group1, True)
+        delegates = (yield Delegates.delegatesOf(txn, delegator, True, expanded=True))
</ins><span class="cx">         self.assertEquals(
</span><span class="cx">             set([u&quot;__sagen1__&quot;, u&quot;__cdaboo1__&quot;]),
</span><span class="cx">             set([d.uid for d in delegates])
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         # Remove delegate access from the sub group
</span><del>-        yield removeDelegate(txn, delegator, group2, True)
-        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
</del><ins>+        yield Delegates.removeDelegate(txn, delegator, group2, True)
+        delegates = (yield Delegates.delegatesOf(txn, delegator, True, expanded=True))
</ins><span class="cx">         self.assertEquals(
</span><span class="cx">             set([u&quot;__sagen1__&quot;]),
</span><span class="cx">             set([d.uid for d in delegates])
</span><span class="lines">@@ -240,11 +238,11 @@
</span><span class="cx">         delegate1 = yield self.directory.recordWithUID(u&quot;__sagen1__&quot;)
</span><span class="cx"> 
</span><span class="cx">         txn = self.store.newTransaction(label=&quot;test_noDuplication&quot;)
</span><del>-        yield addDelegate(txn, delegator, delegate1, True)
</del><ins>+        yield Delegates.addDelegate(txn, delegator, delegate1, True)
</ins><span class="cx">         yield txn.commit()
</span><span class="cx"> 
</span><span class="cx">         txn = self.store.newTransaction(label=&quot;test_noDuplication&quot;)
</span><del>-        yield addDelegate(txn, delegator, delegate1, True)
</del><ins>+        yield Delegates.addDelegate(txn, delegator, delegate1, True)
</ins><span class="cx">         yield txn.commit()
</span><span class="cx"> 
</span><span class="cx">         txn = self.store.newTransaction(label=&quot;test_noDuplication&quot;)
</span><span class="lines">@@ -262,11 +260,11 @@
</span><span class="cx">         group1 = yield self.directory.recordWithUID(u&quot;__top_group_1__&quot;)
</span><span class="cx"> 
</span><span class="cx">         txn = self.store.newTransaction(label=&quot;test_noDuplication&quot;)
</span><del>-        yield addDelegate(txn, delegator, group1, True)
</del><ins>+        yield Delegates.addDelegate(txn, delegator, group1, True)
</ins><span class="cx">         yield txn.commit()
</span><span class="cx"> 
</span><span class="cx">         txn = self.store.newTransaction(label=&quot;test_noDuplication&quot;)
</span><del>-        yield addDelegate(txn, delegator, group1, True)
</del><ins>+        yield Delegates.addDelegate(txn, delegator, group1, True)
</ins><span class="cx">         yield txn.commit()
</span><span class="cx"> 
</span><span class="cx">         txn = self.store.newTransaction(label=&quot;test_noDuplication&quot;)
</span><span class="lines">@@ -279,3 +277,572 @@
</span><span class="cx">         )
</span><span class="cx">         yield txn.commit()
</span><span class="cx">         self.assertEquals([[&quot;__top_group_1__&quot;]], results)
</span><ins>+
+
+
+class DelegationCachingTest(StoreTestCase):
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(DelegationCachingTest, self).setUp()
+        self.store = self.storeUnderTest()
+        self.groupCacher = GroupCacher(self.directory)
+
+
+    @inlineCallbacks
+    def _memcacherMemberResults(self, delegate, readWrite, expanded, results):
+        delegateUIDs = yield Delegates._memcacher.getMembers(delegate.uid, readWrite, expanded)
+        self.assertEqual(
+            set(delegateUIDs) if delegateUIDs is not None else None,
+            set([delegate.uid for delegate in results]) if results is not None else None,
+            msg=&quot;uid:{}, rw={}, expanded={}&quot;.format(delegate.uid, readWrite, expanded)
+        )
+
+
+    @inlineCallbacks
+    def _memcacherAllMemberResults(self, delegate, results1, results2, results3, results4):
+        for readWrite, expanded, results in (
+            (True, False, results1),
+            (True, True, results2),
+            (False, False, results3),
+            (False, True, results4),
+        ):
+            yield self._memcacherMemberResults(delegate, readWrite, expanded, results)
+
+
+    @inlineCallbacks
+    def _memcacherMembershipResults(self, delegate, readWrite, results):
+        delegatorUIDs = yield Delegates._memcacher.getMemberships(delegate.uid, readWrite)
+        self.assertEqual(
+            set(delegatorUIDs) if delegatorUIDs is not None else None,
+            set([delegator.uid for delegator in results]) if results is not None else None,
+            msg=&quot;uid:{}, rw={}&quot;.format(delegate.uid, readWrite)
+        )
+
+
+    @inlineCallbacks
+    def _memcacherAllMembershipResults(self, delegate, results1, results2):
+        for readWrite, results in (
+            (True, results1),
+            (False, results2),
+        ):
+            yield self._memcacherMembershipResults(delegate, readWrite, results)
+
+
+    @inlineCallbacks
+    def _delegatesOfResults(self, delegator, readWrite, expanded, results):
+        delegates = (yield Delegates.delegatesOf(self.transactionUnderTest(), delegator, readWrite, expanded))
+        self.assertEquals(
+            set([d.uid for d in delegates]),
+            set([delegate.uid for delegate in results]),
+            msg=&quot;uid:{}, rw={}, expanded={}&quot;.format(delegator.uid, readWrite, expanded)
+        )
+
+
+    @inlineCallbacks
+    def _delegatesOfAllResults(self, delegator, results1, results2, results3, results4):
+        for readWrite, expanded, results in (
+            (True, False, results1),
+            (True, True, results2),
+            (False, False, results3),
+            (False, True, results4),
+        ):
+            yield self._delegatesOfResults(delegator, readWrite, expanded, results)
+
+
+    @inlineCallbacks
+    def _delegatedToResults(self, delegate, readWrite, results):
+        delegators = (yield Delegates.delegatedTo(self.transactionUnderTest(), delegate, readWrite))
+        self.assertEquals(
+            set([d.uid for d in delegators]),
+            set([delegator.uid for delegator in results]),
+            msg=&quot;uid:{}, rw={}&quot;.format(delegate.uid, readWrite)
+        )
+
+
+    @inlineCallbacks
+    def _delegatedToAllResults(self, delegator, results1, results2):
+        for readWrite, results in (
+            (True, results1),
+            (False, results2),
+        ):
+            yield self._delegatedToResults(delegator, readWrite, results)
+
+
+    @inlineCallbacks
+    def test_cacheUsed(self):
+
+        yield Delegates._memcacher.flushAll()
+
+        delegator = yield self.directory.recordWithUID(u&quot;__wsanchez1__&quot;)
+        delegate1 = yield self.directory.recordWithUID(u&quot;__sagen1__&quot;)
+
+        # Patch transaction so we can monitor whether cache is being used
+        original_delegates = CommonStoreTransaction.delegates
+        delegates_query = [0]
+        def _delegates(self, delegator, readWrite, expanded=False):
+            delegates_query[0] += 1
+            return original_delegates(self, delegator, readWrite, expanded)
+        self.patch(CommonStoreTransaction, &quot;delegates&quot;, _delegates)
+
+        original_delegators = CommonStoreTransaction.delegators
+        delegators_query = [0]
+        def _delegators(self, delegate, readWrite):
+            delegators_query[0] += 1
+            return original_delegators(self, delegate, readWrite)
+        self.patch(CommonStoreTransaction, &quot;delegators&quot;, _delegators)
+
+        # Not used
+        yield Delegates.delegatesOf(self.transactionUnderTest(), delegator, True, False)
+        self.assertEqual(delegates_query[0], 1)
+
+        # Used
+        yield Delegates.delegatesOf(self.transactionUnderTest(), delegator, True, False)
+        self.assertEqual(delegates_query[0], 1)
+
+        # Not used
+        yield Delegates.delegatesOf(self.transactionUnderTest(), delegator, False, False)
+        self.assertEqual(delegates_query[0], 2)
+
+        # Used
+        yield Delegates.delegatesOf(self.transactionUnderTest(), delegator, False, False)
+        self.assertEqual(delegates_query[0], 2)
+
+        # Not used
+        yield Delegates.delegatedTo(self.transactionUnderTest(), delegate1, True)
+        self.assertEqual(delegators_query[0], 1)
+
+        # Used
+        yield Delegates.delegatedTo(self.transactionUnderTest(), delegate1, True)
+        self.assertEqual(delegators_query[0], 1)
+
+        # Not used
+        yield Delegates.delegatedTo(self.transactionUnderTest(), delegate1, False)
+        self.assertEqual(delegators_query[0], 2)
+
+        # Used
+        yield Delegates.delegatedTo(self.transactionUnderTest(), delegate1, False)
+        self.assertEqual(delegators_query[0], 2)
+
+
+    @inlineCallbacks
+    def test_addRemoveDelegation(self):
+
+        yield Delegates._memcacher.flushAll()
+
+        delegator = yield self.directory.recordWithUID(u&quot;__wsanchez1__&quot;)
+        delegate1 = yield self.directory.recordWithUID(u&quot;__sagen1__&quot;)
+        delegate2 = yield self.directory.recordWithUID(u&quot;__cdaboo1__&quot;)
+
+        # Add delegate
+        yield Delegates.addDelegate(self.transactionUnderTest(), delegator, delegate1, True)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, None, [delegate1], None, None)
+        yield self._memcacherAllMemberResults(delegate1, None, None, None, None)
+        yield self._memcacherAllMemberResults(delegate2, None, None, None, None)
+        yield self._memcacherAllMembershipResults(delegator, None, None)
+        yield self._memcacherAllMembershipResults(delegate1, None, None)
+        yield self._memcacherAllMembershipResults(delegate2, None, None)
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(
+                delegator,
+                [delegate1], [delegate1], [], [],
+            )
+
+            yield self._delegatesOfAllResults(
+                delegate1,
+                [], [], [], [],
+            )
+
+            yield self._delegatesOfAllResults(
+                delegate2,
+                [], [], [], [],
+            )
+
+            yield self._delegatedToAllResults(
+                delegator,
+                [], [],
+            )
+
+            yield self._delegatedToAllResults(
+                delegate1,
+                [delegator], [],
+            )
+
+            yield self._delegatedToAllResults(
+                delegate2,
+                [], [],
+            )
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [delegate1], [delegate1], [], [])
+            yield self._memcacherAllMemberResults(delegate1, [], [], [], [])
+            yield self._memcacherAllMemberResults(delegate2, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegate1, [delegator], [])
+            yield self._memcacherAllMembershipResults(delegate2, [], [])
+
+        # Remove delegate
+        yield Delegates.removeDelegate(self.transactionUnderTest(), delegator, delegate1, True)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, None, [], [], [])
+        yield self._memcacherAllMemberResults(delegate1, [], [], [], [])
+        yield self._memcacherAllMemberResults(delegate2, [], [], [], [])
+        yield self._memcacherAllMembershipResults(delegator, [], [])
+        yield self._memcacherAllMembershipResults(delegate1, None, [])
+        yield self._memcacherAllMembershipResults(delegate2, [], [])
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(
+                delegator,
+                [], [], [], [],
+            )
+
+            yield self._delegatesOfAllResults(
+                delegate1,
+                [], [], [], [],
+            )
+
+            yield self._delegatesOfAllResults(
+                delegate2,
+                [], [], [], [],
+            )
+
+            yield self._delegatedToAllResults(
+                delegator,
+                [], [],
+            )
+
+            yield self._delegatedToAllResults(
+                delegate1,
+                [], [],
+            )
+
+            yield self._delegatedToAllResults(
+                delegate2,
+                [], [],
+            )
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [], [], [], [])
+            yield self._memcacherAllMemberResults(delegate1, [], [], [], [])
+            yield self._memcacherAllMemberResults(delegate2, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegate1, [], [])
+            yield self._memcacherAllMembershipResults(delegate2, [], [])
+
+
+    @inlineCallbacks
+    def test_setDelegation(self):
+
+        yield Delegates._memcacher.flushAll()
+
+        delegator = yield self.directory.recordWithUID(u&quot;__wsanchez1__&quot;)
+        delegates = [
+            (yield self.directory.recordWithUID(u&quot;__sagen1__&quot;)),
+            (yield self.directory.recordWithUID(u&quot;__cdaboo1__&quot;)),
+            (yield self.directory.recordWithUID(u&quot;__dre1__&quot;)),
+        ]
+
+        # Add delegates
+        yield Delegates.setDelegates(self.transactionUnderTest(), delegator, [delegates[0], delegates[1]], True)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, None, [delegates[0], delegates[1]], None, None)
+        yield self._memcacherAllMembershipResults(delegator, None, None)
+        for delegate in delegates:
+            yield self._memcacherAllMemberResults(delegate, None, None, None, None)
+            yield self._memcacherAllMembershipResults(delegate, None, None)
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(delegator, [delegates[0], delegates[1]], [delegates[0], delegates[1]], [], [])
+            for delegate in delegates:
+                yield self._delegatesOfAllResults(delegate, [], [], [], [])
+
+            yield self._delegatedToAllResults(delegator, [], [])
+            yield self._delegatedToAllResults(delegates[0], [delegator], [])
+            yield self._delegatedToAllResults(delegates[1], [delegator], [])
+            yield self._delegatedToAllResults(delegates[2], [], [])
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [delegates[0], delegates[1]], [delegates[0], delegates[1]], [], [])
+            for delegate in delegates:
+                yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegates[0], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[2], [], [])
+
+        # Remove delegate
+        yield Delegates.setDelegates(self.transactionUnderTest(), delegator, [delegates[1], delegates[2]], True)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, None, [delegates[1], delegates[2]], [], [])
+        for delegate in delegates:
+            yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+        yield self._memcacherAllMembershipResults(delegator, [], [])
+        yield self._memcacherAllMembershipResults(delegates[0], None, [])
+        yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+        yield self._memcacherAllMembershipResults(delegates[2], None, [])
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(delegator, [delegates[1], delegates[2]], [delegates[1], delegates[2]], [], [])
+            for delegate in delegates:
+                yield self._delegatesOfAllResults(delegate, [], [], [], [])
+
+            yield self._delegatedToAllResults(delegator, [], [])
+            yield self._delegatedToAllResults(delegates[0], [], [])
+            yield self._delegatedToAllResults(delegates[1], [delegator], [])
+            yield self._delegatedToAllResults(delegates[2], [delegator], [])
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [delegates[1], delegates[2]], [delegates[1], delegates[2]], [], [])
+            for delegate in delegates:
+                yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegates[0], [], [])
+            yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[2], [delegator], [])
+
+        # Add delegate with other mode
+        yield Delegates.setDelegates(self.transactionUnderTest(), delegator, [delegates[0]], False)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, [delegates[1], delegates[2]], [delegates[1], delegates[2]], None, [delegates[0]])
+        for delegate in delegates:
+            yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+        yield self._memcacherAllMembershipResults(delegator, [], [])
+        yield self._memcacherAllMembershipResults(delegates[0], [], None)
+        yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+        yield self._memcacherAllMembershipResults(delegates[2], [delegator], [])
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(delegator, [delegates[1], delegates[2]], [delegates[1], delegates[2]], [delegates[0]], [delegates[0]])
+            for delegate in delegates:
+                yield self._delegatesOfAllResults(delegate, [], [], [], [])
+
+            yield self._delegatedToAllResults(delegator, [], [])
+            yield self._delegatedToAllResults(delegates[0], [], [delegator])
+            yield self._delegatedToAllResults(delegates[1], [delegator], [])
+            yield self._delegatedToAllResults(delegates[2], [delegator], [])
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [delegates[1], delegates[2]], [delegates[1], delegates[2]], [delegates[0]], [delegates[0]])
+            for delegate in delegates:
+                yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegates[0], [], [delegator])
+            yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[2], [delegator], [])
+
+
+    @inlineCallbacks
+    def test_setGroupDelegation(self):
+
+        yield Delegates._memcacher.flushAll()
+
+        delegator = yield self.directory.recordWithUID(u&quot;__wsanchez1__&quot;)
+        delegates = [
+            (yield self.directory.recordWithUID(u&quot;__sagen1__&quot;)),
+            (yield self.directory.recordWithUID(u&quot;__cdaboo1__&quot;)),
+            (yield self.directory.recordWithUID(u&quot;__glyph1__&quot;)),
+            (yield self.directory.recordWithUID(u&quot;__dre1__&quot;)),
+        ]
+        group1 = yield self.directory.recordWithUID(u&quot;__top_group_1__&quot;)
+        group2 = yield self.directory.recordWithUID(u&quot;__sub_group_1__&quot;)
+        yield self.transactionUnderTest().groupByUID(u&quot;__top_group_1__&quot;)
+        yield self.transactionUnderTest().groupByUID(u&quot;__sub_group_1__&quot;)
+        yield self.commit()
+
+        def delegateMatch(*args):
+            return [delegates[i] for i in args]
+
+        # Add group delegate
+        yield Delegates.setDelegates(self.transactionUnderTest(), delegator, [group1], True)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, None, delegateMatch(0, 1, 2), None, None)
+        yield self._memcacherAllMembershipResults(delegator, None, None)
+        for delegate in delegates:
+            yield self._memcacherAllMemberResults(delegate, None, None, None, None)
+            yield self._memcacherAllMembershipResults(delegate, None, None)
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(delegator, [group1], delegateMatch(0, 1, 2), [], [])
+            for delegate in delegates:
+                yield self._delegatesOfAllResults(delegate, [], [], [], [])
+
+            yield self._delegatedToAllResults(delegator, [], [])
+            yield self._delegatedToAllResults(delegates[0], [delegator], [])
+            yield self._delegatedToAllResults(delegates[1], [delegator], [])
+            yield self._delegatedToAllResults(delegates[2], [delegator], [])
+            yield self._delegatedToAllResults(delegates[3], [], [])
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [group1], delegateMatch(0, 1, 2), [], [])
+            for delegate in delegates:
+                yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegates[0], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[2], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[3], [], [])
+
+        # Add individual delegate
+        yield Delegates.setDelegates(self.transactionUnderTest(), delegator, [group1, delegates[3]], True)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, None, delegateMatch(0, 1, 2, 3), [], [])
+        for delegate in delegates:
+            yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+        yield self._memcacherAllMembershipResults(delegator, [], [])
+        yield self._memcacherAllMembershipResults(delegates[0], [delegator], [])
+        yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+        yield self._memcacherAllMembershipResults(delegates[2], [delegator], [])
+        yield self._memcacherAllMembershipResults(delegates[3], None, [])
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(delegator, [group1, delegates[3]], delegateMatch(0, 1, 2, 3), [], [])
+            for delegate in delegates:
+                yield self._delegatesOfAllResults(delegate, [], [], [], [])
+
+            yield self._delegatedToAllResults(delegator, [], [])
+            yield self._delegatedToAllResults(delegates[0], [delegator], [])
+            yield self._delegatedToAllResults(delegates[1], [delegator], [])
+            yield self._delegatedToAllResults(delegates[2], [delegator], [])
+            yield self._delegatedToAllResults(delegates[3], [delegator], [])
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [group1, delegates[3]], delegateMatch(0, 1, 2, 3), [], [])
+            for delegate in delegates:
+                yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegates[0], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[2], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[3], [delegator], [])
+
+        # Switch to sub-group
+        yield Delegates.setDelegates(self.transactionUnderTest(), delegator, [group2, delegates[3]], True)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, None, delegateMatch(0, 1, 3), [], [])
+        for delegate in delegates:
+            yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+        yield self._memcacherAllMembershipResults(delegator, [], [])
+        yield self._memcacherAllMembershipResults(delegates[0], None, [])
+        yield self._memcacherAllMembershipResults(delegates[1], None, [])
+        yield self._memcacherAllMembershipResults(delegates[2], None, [])
+        yield self._memcacherAllMembershipResults(delegates[3], [delegator], [])
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(delegator, [group2, delegates[3]], delegateMatch(0, 1, 3), [], [])
+            for delegate in delegates:
+                yield self._delegatesOfAllResults(delegate, [], [], [], [])
+
+            yield self._delegatedToAllResults(delegator, [], [])
+            yield self._delegatedToAllResults(delegates[0], [delegator], [])
+            yield self._delegatedToAllResults(delegates[1], [delegator], [])
+            yield self._delegatedToAllResults(delegates[2], [], [])
+            yield self._delegatedToAllResults(delegates[3], [delegator], [])
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [group2, delegates[3]], delegateMatch(0, 1, 3), [], [])
+            for delegate in delegates:
+                yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegates[0], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[2], [], [])
+            yield self._memcacherAllMembershipResults(delegates[3], [delegator], [])
+
+        # Add member of existing group
+        yield Delegates.setDelegates(self.transactionUnderTest(), delegator, [group2, delegates[0], delegates[3]], True)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, None, delegateMatch(0, 1, 3), [], [])
+        for delegate in delegates:
+            yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+        yield self._memcacherAllMembershipResults(delegator, [], [])
+        yield self._memcacherAllMembershipResults(delegates[0], [delegator], [])
+        yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+        yield self._memcacherAllMembershipResults(delegates[2], [], [])
+        yield self._memcacherAllMembershipResults(delegates[3], [delegator], [])
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(delegator, [group2, delegates[0], delegates[3]], delegateMatch(0, 1, 3), [], [])
+            for delegate in delegates:
+                yield self._delegatesOfAllResults(delegate, [], [], [], [])
+
+            yield self._delegatedToAllResults(delegator, [], [])
+            yield self._delegatedToAllResults(delegates[0], [delegator], [])
+            yield self._delegatedToAllResults(delegates[1], [delegator], [])
+            yield self._delegatedToAllResults(delegates[2], [], [])
+            yield self._delegatedToAllResults(delegates[3], [delegator], [])
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [group2, delegates[0], delegates[3]], delegateMatch(0, 1, 3), [], [])
+            for delegate in delegates:
+                yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegates[0], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[1], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[2], [], [])
+            yield self._memcacherAllMembershipResults(delegates[3], [delegator], [])
+
+        # Remove group
+        yield Delegates.setDelegates(self.transactionUnderTest(), delegator, [delegates[0], delegates[3]], True)
+        yield self.commit()
+
+        # Some cache entries invalid
+        yield self._memcacherAllMemberResults(delegator, None, delegateMatch(0, 3), [], [])
+        for delegate in delegates:
+            yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+        yield self._memcacherAllMembershipResults(delegator, [], [])
+        yield self._memcacherAllMembershipResults(delegates[0], [delegator], [])
+        yield self._memcacherAllMembershipResults(delegates[1], None, [])
+        yield self._memcacherAllMembershipResults(delegates[2], [], [])
+        yield self._memcacherAllMembershipResults(delegates[3], [delegator], [])
+
+        # Read the delegate information twice - first time should be without cache, second with
+        for _ignore in range(2):
+            yield self._delegatesOfAllResults(delegator, [delegates[0], delegates[3]], delegateMatch(0, 3), [], [])
+            for delegate in delegates:
+                yield self._delegatesOfAllResults(delegate, [], [], [], [])
+
+            yield self._delegatedToAllResults(delegator, [], [])
+            yield self._delegatedToAllResults(delegates[0], [delegator], [])
+            yield self._delegatedToAllResults(delegates[1], [], [])
+            yield self._delegatedToAllResults(delegates[2], [], [])
+            yield self._delegatedToAllResults(delegates[3], [delegator], [])
+
+            # Check cache
+            yield self._memcacherAllMemberResults(delegator, [delegates[0], delegates[3]], delegateMatch(0, 3), [], [])
+            for delegate in delegates:
+                yield self._memcacherAllMemberResults(delegate, [], [], [], [])
+            yield self._memcacherAllMembershipResults(delegator, [], [])
+            yield self._memcacherAllMembershipResults(delegates[0], [delegator], [])
+            yield self._memcacherAllMembershipResults(delegates[1], [], [])
+            yield self._memcacherAllMembershipResults(delegates[2], [], [])
+            yield self._memcacherAllMembershipResults(delegates[3], [delegator], [])
</ins></span></pre></div>
<a id="CalendarServertrunktxdavwhotesttest_groupspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/test/test_groups.py (14102 => 14103)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/test/test_groups.py        2014-10-24 17:34:33 UTC (rev 14102)
+++ CalendarServer/trunk/txdav/who/test/test_groups.py        2014-10-24 21:12:40 UTC (rev 14103)
</span><span class="lines">@@ -277,7 +277,6 @@
</span><span class="cx">             delegates,
</span><span class="cx">             set(
</span><span class="cx">                 [
</span><del>-                    u&quot;__wsanchez1__&quot;,
</del><span class="cx">                     u&quot;__sagen1__&quot;,
</span><span class="cx">                     u&quot;__cdaboo1__&quot;,
</span><span class="cx">                     u&quot;__glyph1__&quot;
</span></span></pre>
</div>
</div>

</body>
</html>