<!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>[13822] 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/13822">13822</a></dd>
<dt>Author</dt> <dd>sagen@apple.com</dd>
<dt>Date</dt> <dd>2014-08-01 17:49:24 -0700 (Fri, 01 Aug 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Adds directory caching, both in-process and in the directory proxy sidecar.  The number of seconds records are cached for in each place are configurable.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertapcaldavpy">CalendarServer/trunk/calendarserver/tap/caldav.py</a></li>
<li><a href="#CalendarServertrunkconfcaldavdtestplist">CalendarServer/trunk/conf/caldavd-test.plist</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="#CalendarServertrunktxdavcommondatastoretestaccountsaccountsxml">CalendarServer/trunk/txdav/common/datastore/test/accounts/accounts.xml</a></li>
<li><a href="#CalendarServertrunktxdavdpsserverpy">CalendarServer/trunk/txdav/dps/server.py</a></li>
<li><a href="#CalendarServertrunktxdavdpstesttest_clientpy">CalendarServer/trunk/txdav/dps/test/test_client.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServertrunktxdavwhocachepy">CalendarServer/trunk/txdav/who/cache.py</a></li>
<li><a href="#CalendarServertrunktxdavwhotesttest_cachepy">CalendarServer/trunk/txdav/who/test/test_cache.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertapcaldavpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tap/caldav.py (13821 => 13822)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tap/caldav.py        2014-08-01 21:06:17 UTC (rev 13821)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py        2014-08-02 00:49:24 UTC (rev 13822)
</span><span class="lines">@@ -94,9 +94,10 @@
</span><span class="cx"> )
</span><span class="cx"> from txdav.common.datastore.work.inbox_cleanup import InboxCleanupWork
</span><span class="cx"> from txdav.common.datastore.work.revision_cleanup import FindMinValidRevisionWork
</span><del>-from txdav.who.util import directoryFromConfig
</del><span class="cx"> from txdav.dps.client import DirectoryService as DirectoryProxyClientService
</span><ins>+from txdav.who.cache import CachingDirectoryService
</ins><span class="cx"> from txdav.who.groups import GroupCacher
</span><ins>+from txdav.who.util import directoryFromConfig
</ins><span class="cx"> 
</span><span class="cx"> from twistedcaldav import memcachepool
</span><span class="cx"> from twistedcaldav.config import ConfigurationError
</span><span class="lines">@@ -526,25 +527,24 @@
</span><span class="cx"> 
</span><span class="cx">         # Add the directory proxy sidecar first so it at least get spawned
</span><span class="cx">         # prior to the caldavd worker processes:
</span><del>-        if config.DirectoryProxy.Enabled:
-            log.info(&quot;Adding directory proxy service&quot;)
</del><ins>+        log.info(&quot;Adding directory proxy service&quot;)
</ins><span class="cx"> 
</span><del>-            dpsArgv = [
-                sys.executable,
-                sys.argv[0],
-            ]
-            if config.UserName:
-                dpsArgv.extend((&quot;-u&quot;, config.UserName))
-            if config.GroupName:
-                dpsArgv.extend((&quot;-g&quot;, config.GroupName))
-            dpsArgv.extend((
-                &quot;--reactor={}&quot;.format(config.Twisted.reactor),
-                &quot;-n&quot;, &quot;caldav_directoryproxy&quot;,
-                &quot;-f&quot;, self.configPath,
-            ))
-            self.monitor.addProcess(
-                &quot;directoryproxy&quot;, dpsArgv, env=PARENT_ENVIRONMENT
-            )
</del><ins>+        dpsArgv = [
+            sys.executable,
+            sys.argv[0],
+        ]
+        if config.UserName:
+            dpsArgv.extend((&quot;-u&quot;, config.UserName))
+        if config.GroupName:
+            dpsArgv.extend((&quot;-g&quot;, config.GroupName))
+        dpsArgv.extend((
+            &quot;--reactor={}&quot;.format(config.Twisted.reactor),
+            &quot;-n&quot;, &quot;caldav_directoryproxy&quot;,
+            &quot;-f&quot;, self.configPath,
+        ))
+        self.monitor.addProcess(
+            &quot;directoryproxy&quot;, dpsArgv, env=PARENT_ENVIRONMENT
+        )
</ins><span class="cx"> 
</span><span class="cx">         for slaveNumber in xrange(0, config.MultiProcess.ProcessCount):
</span><span class="cx">             if config.UseMetaFD:
</span><span class="lines">@@ -854,6 +854,11 @@
</span><span class="cx">         directory = DirectoryProxyClientService(config.DirectoryRealmName)
</span><span class="cx">         if config.Servers.Enabled:
</span><span class="cx">             directory.setServersDB(buildServersDB(config.Servers.MaxClients))
</span><ins>+        if config.DirectoryProxy.InProcessCachingSeconds:
+            directory = CachingDirectoryService(
+                directory,
+                expireSeconds=config.DirectoryProxy.InProcessCachingSeconds
+            )
</ins><span class="cx">         store = storeFromConfig(config, txnFactory, directory)
</span><span class="cx">         logObserver = AMPCommonAccessLoggingObserver()
</span><span class="cx">         result = self.requestProcessingService(options, store, logObserver)
</span><span class="lines">@@ -1491,18 +1496,19 @@
</span><span class="cx">                 if directory is None:
</span><span class="cx">                     # Create a Directory Proxy &quot;Server&quot; service and hand it to
</span><span class="cx">                     # the store.
</span><del>-                    # FIXME: right now the store passed *to* the directory is the
-                    # calendar/contacts data store, but for a multi-server deployment
-                    # it will need its own separate store.
</del><span class="cx">                     if config.Servers.Enabled:
</span><span class="cx">                         serversDB = buildServersDB(config.Servers.MaxClients)
</span><span class="cx">                     else:
</span><span class="cx">                         serversDB = None
</span><del>-                    store.setDirectoryService(
-                        directoryFromConfig(
-                            config, store=store, serversDB=serversDB
</del><ins>+                    directorySvc = directoryFromConfig(
+                        config, store=store, serversDB=serversDB
+                    )
+                    if config.DirectoryProxy.InProcessCachingSeconds:
+                        directorySvc = CachingDirectoryService(
+                            directorySvc,
+                            expireSeconds=config.DirectoryProxy.InProcessCachingSeconds
</ins><span class="cx">                         )
</span><del>-                    )
</del><ins>+                    store.setDirectoryService(directorySvc)
</ins><span class="cx"> 
</span><span class="cx">                 pps = PreProcessingService(
</span><span class="cx">                     createMainService, cp, store, logObserver, storageService
</span></span></pre></div>
<a id="CalendarServertrunkconfcaldavdtestplist"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/conf/caldavd-test.plist (13821 => 13822)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/conf/caldavd-test.plist        2014-08-01 21:06:17 UTC (rev 13821)
+++ CalendarServer/trunk/conf/caldavd-test.plist        2014-08-02 00:49:24 UTC (rev 13822)
</span><span class="lines">@@ -175,6 +175,14 @@
</span><span class="cx">     &lt;key&gt;MaxAllowedInstances&lt;/key&gt;
</span><span class="cx">     &lt;integer&gt;3000&lt;/integer&gt;
</span><span class="cx"> 
</span><ins>+    &lt;key&gt;DirectoryProxy&lt;/key&gt;
+    &lt;dict&gt;
+      &lt;key&gt;InProcessCachingSeconds&lt;/key&gt;
+      &lt;integer&gt;10&lt;/integer&gt;
+      &lt;key&gt;InSidecarCachingSeconds&lt;/key&gt;
+      &lt;integer&gt;30&lt;/integer&gt;
+    &lt;/dict&gt;
+
</ins><span class="cx">     &lt;!--
</span><span class="cx">         Directory service
</span><span class="cx"> 
</span><span class="lines">@@ -788,7 +796,7 @@
</span><span class="cx">                 &lt;dict&gt;
</span><span class="cx">                         &lt;key&gt;Enabled&lt;/key&gt;
</span><span class="cx">                         &lt;true/&gt;
</span><del>-                        
</del><ins>+
</ins><span class="cx">                         &lt;!-- Make these short for testing --&gt;
</span><span class="cx">             &lt;key&gt;RequestDelaySeconds&lt;/key&gt;
</span><span class="cx">             &lt;real&gt;0.1&lt;/real&gt;
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavdirectorytesttest_principalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py (13821 => 13822)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py        2014-08-01 21:06:17 UTC (rev 13821)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py        2014-08-02 00:49:24 UTC (rev 13822)
</span><span class="lines">@@ -297,6 +297,12 @@
</span><span class="cx">                 test_items += (principalURL, alternateURL)
</span><span class="cx"> 
</span><span class="cx">             for address in test_items:
</span><ins>+
+                # For txdav.who.test.test_cache I added two accounts which
+                # share the same email address, but that messes up this test.
+                if &quot;mailto:cache-user&quot; in address:
+                    continue
+
</ins><span class="cx">                 principal = (
</span><span class="cx">                     yield provisioningResource
</span><span class="cx">                     .principalForCalendarUserAddress(address)
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavstdconfigpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py (13821 => 13822)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-08-01 21:06:17 UTC (rev 13821)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-08-02 00:49:24 UTC (rev 13822)
</span><span class="lines">@@ -898,8 +898,9 @@
</span><span class="cx">     },
</span><span class="cx"> 
</span><span class="cx">     &quot;DirectoryProxy&quot;: {
</span><del>-        &quot;Enabled&quot;: True,
</del><span class="cx">         &quot;SocketPath&quot;: &quot;directory-proxy.sock&quot;,
</span><ins>+        &quot;InProcessCachingSeconds&quot;: 10,
+        &quot;InSidecarCachingSeconds&quot;: 30,
</ins><span class="cx">     },
</span><span class="cx"> 
</span><span class="cx">     #
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoretestaccountsaccountsxml"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/test/accounts/accounts.xml (13821 => 13822)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/test/accounts/accounts.xml        2014-08-01 21:06:17 UTC (rev 13821)
+++ CalendarServer/trunk/txdav/common/datastore/test/accounts/accounts.xml        2014-08-02 00:49:24 UTC (rev 13822)
</span><span class="lines">@@ -393,6 +393,35 @@
</span><span class="cx">     &lt;full-name&gt;Empty Group&lt;/full-name&gt;
</span><span class="cx">   &lt;/record&gt;
</span><span class="cx"> 
</span><ins>+  &lt;!-- Caching Directory test records --&gt;
+
+  &lt;record type=&quot;user&quot;&gt;
+    &lt;uid&gt;cache-uid-1&lt;/uid&gt;
+    &lt;guid&gt;8166C681-2D08-4846-90F7-97023A6EDDC5&lt;/guid&gt;
+    &lt;short-name&gt;cache-name-1&lt;/short-name&gt;
+    &lt;short-name&gt;cache-alt-name-1&lt;/short-name&gt;
+    &lt;password&gt;cache-password-1&lt;/password&gt;
+    &lt;full-name&gt;Cache User 1&lt;/full-name&gt;
+    &lt;email&gt;cache-user-1@example.com&lt;/email&gt;
+  &lt;/record&gt;
+
+  &lt;record type=&quot;user&quot;&gt;
+    &lt;uid&gt;cache-uid-duplicate-1&lt;/uid&gt;
+    &lt;short-name&gt;cache-name-duplicate-1&lt;/short-name&gt;
+    &lt;short-name&gt;cache-alt-name-duplicate-1&lt;/short-name&gt;
+    &lt;password&gt;cache-password-duplicate-1&lt;/password&gt;
+    &lt;full-name&gt;Cache User Duplicate 1&lt;/full-name&gt;
+    &lt;email&gt;cache-user-1@example.com&lt;/email&gt;
+  &lt;/record&gt;
+
+  &lt;record type=&quot;user&quot;&gt;
+    &lt;uid&gt;cache-uid-2&lt;/uid&gt;
+    &lt;short-name&gt;cache-name-2&lt;/short-name&gt;
+    &lt;password&gt;cache-password-2&lt;/password&gt;
+    &lt;full-name&gt;Cache User 2&lt;/full-name&gt;
+    &lt;email&gt;cache-user-2@example.com&lt;/email&gt;
+  &lt;/record&gt;
+
</ins><span class="cx">   &lt;!-- Calverify test records --&gt;
</span><span class="cx"> 
</span><span class="cx">   &lt;record type=&quot;user&quot;&gt;
</span></span></pre></div>
<a id="CalendarServertrunktxdavdpsserverpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/dps/server.py (13821 => 13822)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/dps/server.py        2014-08-01 21:06:17 UTC (rev 13821)
+++ CalendarServer/trunk/txdav/dps/server.py        2014-08-02 00:49:24 UTC (rev 13822)
</span><span class="lines">@@ -18,6 +18,7 @@
</span><span class="cx"> import datetime
</span><span class="cx"> import uuid
</span><span class="cx"> 
</span><ins>+from calendarserver.tap.util import getDBPool, storeFromConfig
</ins><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from twext.who.expression import MatchType, MatchFlags, Operand
</span><span class="cx"> from twisted.application import service
</span><span class="lines">@@ -39,6 +40,7 @@
</span><span class="cx">     WikiAccessForUIDCommand, ContinuationCommand
</span><span class="cx">     # UpdateRecordsCommand, RemoveRecordsCommand
</span><span class="cx"> )
</span><ins>+from txdav.who.cache import CachingDirectoryService
</ins><span class="cx"> from txdav.who.util import directoryFromConfig
</span><span class="cx"> from txdav.who.wiki import WikiAccessLevel
</span><span class="cx"> from zope.interface import implementer
</span><span class="lines">@@ -571,11 +573,20 @@
</span><span class="cx">             setproctitle(&quot;CalendarServer Directory Proxy Service&quot;)
</span><span class="cx"> 
</span><span class="cx">         try:
</span><ins>+            pool, txnFactory = getDBPool(config)
+            store = storeFromConfig(config, txnFactory, None)
</ins><span class="cx">             directory = directoryFromConfig(config)
</span><ins>+            if config.DirectoryProxy.InSidecarCachingSeconds:
+                directory = CachingDirectoryService(
+                    directory,
+                    expireSeconds=config.DirectoryProxy.InSidecarCachingSeconds
+                )
+            store.setDirectoryService(directory)
</ins><span class="cx">         except Exception as e:
</span><span class="cx">             log.error(&quot;Failed to create directory service&quot;, error=e)
</span><span class="cx">             raise
</span><span class="cx"> 
</span><ins>+
</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="CalendarServertrunktxdavdpstesttest_clientpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/dps/test/test_client.py (13821 => 13822)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/dps/test/test_client.py        2014-08-01 21:06:17 UTC (rev 13821)
+++ CalendarServer/trunk/txdav/dps/test/test_client.py        2014-08-02 00:49:24 UTC (rev 13822)
</span><span class="lines">@@ -414,7 +414,7 @@
</span><span class="cx">         records = (yield self.client.recordsWithRecordType(
</span><span class="cx">             RecordType.user
</span><span class="cx">         ))
</span><del>-        self.assertEquals(len(records), 240)
</del><ins>+        self.assertEquals(len(records), 243)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span></span></pre></div>
<a id="CalendarServertrunktxdavwhocachepy"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/txdav/who/cache.py (0 => 13822)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/cache.py                                (rev 0)
+++ CalendarServer/trunk/txdav/who/cache.py        2014-08-02 00:49:24 UTC (rev 13822)
</span><span class="lines">@@ -0,0 +1,334 @@
</span><ins>+# -*- test-case-name: txdav.who.test.test_cache -*-
+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+&quot;&quot;&quot;
+Caching Directory Service
+&quot;&quot;&quot;
+
+__all__ = [
+    &quot;CachingDirectoryService&quot;,
+]
+
+import time
+
+from zope.interface import implementer
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twext.python.log import Logger
+from twext.who.directory import DirectoryService as BaseDirectoryService
+from twext.who.idirectory import (
+    IDirectoryService, FieldName as BaseFieldName
+)
+from twext.who.util import ConstantsContainer
+
+from txdav.common.idirectoryservice import IStoreDirectoryService
+from txdav.who.directory import (
+    CalendarDirectoryServiceMixin,
+)
+from txdav.who.idirectory import (
+    FieldName
+)
+from twisted.python.constants import Values, ValueConstant
+
+log = Logger()
+
+
+class IndexType(Values):
+    &quot;&quot;&quot;
+    Constants to use for identifying indexes
+    &quot;&quot;&quot;
+    uid = ValueConstant(&quot;uid&quot;)
+    guid = ValueConstant(&quot;guid&quot;)
+    shortName = ValueConstant(&quot;shortName&quot;)
+    emailAddress = ValueConstant(&quot;emailAddress&quot;)
+
+
+@implementer(IDirectoryService, IStoreDirectoryService)
+class CachingDirectoryService(
+    BaseDirectoryService, CalendarDirectoryServiceMixin
+):
+    &quot;&quot;&quot;
+    Caching directory service.
+
+    This is a directory service that wraps an L{IDirectoryService} and caches
+    directory records.
+    &quot;&quot;&quot;
+
+    fieldName = ConstantsContainer((
+        BaseFieldName,
+        FieldName,
+    ))
+
+
+
+    def __init__(self, directory, expireSeconds=30):
+        BaseDirectoryService.__init__(self, directory.realmName)
+        self._directory = directory
+        self._expireSeconds = expireSeconds
+        self.resetCache()
+
+
+    def resetCache(self):
+        &quot;&quot;&quot;
+        Clear the cache
+        &quot;&quot;&quot;
+        self._cache = {
+            IndexType.uid: {},
+            IndexType.guid: {},
+            IndexType.shortName: {},  # key is (recordType.name, shortName)
+            IndexType.emailAddress: {},
+        }
+        self._hitCount = 0
+        self._requestCount = 0
+
+
+    def setTestTime(self, timestamp):
+        self._test_time = timestamp
+
+
+    def cacheRecord(self, record, indexTypes):
+        &quot;&quot;&quot;
+        Store a record in the cache, within the specified indexes
+
+        @param record: the directory record
+        @param indexTypes: an iterable of L{IndexType}
+        &quot;&quot;&quot;
+
+        if hasattr(self, &quot;_test_time&quot;):
+            timestamp = self._test_time
+        else:
+            timestamp = time.time()
+
+        if IndexType.uid in indexTypes:
+            self._cache[IndexType.uid][record.uid] = (timestamp, record)
+
+        if IndexType.guid in indexTypes:
+            try:
+                self._cache[IndexType.guid][record.guid] = (timestamp, record)
+            except AttributeError:
+                pass
+        if IndexType.shortName in indexTypes:
+            try:
+                typeName = record.recordType.name
+                for name in record.shortNames:
+                    self._cache[IndexType.shortName][(typeName, name)] = (timestamp, record)
+            except AttributeError:
+                pass
+        if IndexType.emailAddress in indexTypes:
+            try:
+                for emailAddress in record.emailAddresses:
+                    self._cache[IndexType.emailAddress][emailAddress] = (timestamp, record)
+            except AttributeError:
+                pass
+
+
+    def lookupRecord(self, indexType, key):
+        &quot;&quot;&quot;
+        Looks for a record in the specified index, under the specified key
+
+        @param index: an index type
+        @type indexType: L{IndexType}
+
+        @param key: the key to look up in the specified index
+        @type key: any valid type that can be used as a dictionary key
+
+        @return: the cached directory record, or None
+        @rtype: L{DirectoryRecord}
+        &quot;&quot;&quot;
+
+        self._requestCount += 1
+        if key in self._cache[indexType]:
+
+            if hasattr(self, &quot;_test_time&quot;):
+                now = self._test_time
+            else:
+                now = time.time()
+
+            cachedTime, record = self._cache[indexType].get(key, (0.0, None))
+            if now - self._expireSeconds &gt; cachedTime:
+                log.debug(
+                    &quot;Directory cache miss (expired): {index} {key}&quot;,
+                    index=indexType.value,
+                    key=key
+                )
+                # This record has expired
+                del self._cache[indexType][key]
+                return None
+
+            log.debug(
+                &quot;Directory cache hit: {index} {key}&quot;,
+                index=indexType.value,
+                key=key
+            )
+            self._hitCount += 1
+            return record
+        else:
+            log.debug(
+                &quot;Directory cache miss: {index} {key}&quot;,
+                index=indexType.value,
+                key=key
+            )
+        return None
+
+
+    # Cached methods:
+
+    @inlineCallbacks
+    def recordWithUID(self, uid):
+
+        # First check our cache
+        record = self.lookupRecord(IndexType.uid, uid)
+        if record is None:
+            record = yield self._directory.recordWithUID(uid)
+            if record is not None:
+                # Note we do not index on email address; see below.
+                self.cacheRecord(
+                    record,
+                    (IndexType.uid, IndexType.guid, IndexType.shortName)
+                )
+
+        returnValue(record)
+
+
+    @inlineCallbacks
+    def recordWithGUID(self, guid):
+
+        # First check our cache
+        record = self.lookupRecord(IndexType.guid, guid)
+        if record is None:
+            record = yield self._directory.recordWithGUID(guid)
+            if record is not None:
+                # Note we do not index on email address; see below.
+                self.cacheRecord(
+                    record,
+                    (IndexType.uid, IndexType.guid, IndexType.shortName)
+                )
+
+        returnValue(record)
+
+
+    @inlineCallbacks
+    def recordWithShortName(self, recordType, shortName):
+
+        # First check our cache
+        record = self.lookupRecord(
+            IndexType.shortName,
+            (recordType.name, shortName)
+        )
+        if record is None:
+            record = yield self._directory.recordWithShortName(
+                recordType, shortName
+            )
+            if record is not None:
+                # Note we do not index on email address; see below.
+                self.cacheRecord(
+                    record,
+                    (IndexType.uid, IndexType.guid, IndexType.shortName)
+                )
+
+        returnValue(record)
+
+
+    @inlineCallbacks
+    def recordsWithEmailAddress(self, emailAddress):
+
+        # First check our cache
+        record = self.lookupRecord(IndexType.emailAddress, emailAddress)
+        if record is None:
+            records = yield self._directory.recordsWithEmailAddress(emailAddress)
+            if len(records) == 1:
+                # Only cache if there was a single match (which is the most
+                # common scenario).  Caching multiple records for the exact
+                # same key/value complicates the data structures.
+                # Also, this is the only situation where we do index a cached
+                # record on email address.  Otherwise, say we had faulted in
+                # on &quot;uid&quot; and then indexed that record on its email address,
+                # the next lookup by email address would only get that record,
+                # but there might be others in the directory service with that
+                # same email address.
+                self.cacheRecord(
+                    records[0],
+                    (
+                        IndexType.uid, IndexType.guid,
+                        IndexType.shortName, IndexType.emailAddress
+                    )
+                )
+        else:
+            records = [record]
+
+        returnValue(records)
+
+
+    # Uncached methods:
+
+    @property
+    def recordType(self):
+        # Defer to the directory service we're caching
+        return self._directory.recordType
+
+
+    def recordTypes(self):
+        # Defer to the directory service we're caching
+        return self._directory.recordTypes()
+
+
+    def recordsFromExpression(self, expression, recordTypes=None):
+        # Defer to the directory service we're caching
+        return self._directory.recordsFromExpression(
+            expression, recordTypes=recordTypes
+        )
+
+
+    def recordsWithFieldValue(self, fieldName, value):
+        # Defer to the directory service we're caching
+        return self._directory.recordsWithFieldValue(
+            fieldName, value
+        )
+
+
+    def updateRecords(self, records, create=False):
+        # Defer to the directory service we're caching
+        return self._directory.updateRecords(records, create=create)
+
+
+    def removeRecords(self, uids):
+        # Defer to the directory service we're caching
+        return self._directory.removeRecords(uids)
+
+
+    def recordsWithRecordType(self, recordType):
+        # Defer to the directory service we're caching
+        return self._directory.recordsWithRecordType(recordType)
+
+
+    def recordsMatchingTokens(self, *args, **kwds):
+        return CalendarDirectoryServiceMixin.recordsMatchingTokens(
+            self, *args, **kwds
+        )
+
+
+    def recordsMatchingFields(self, *args, **kwds):
+        return CalendarDirectoryServiceMixin.recordsMatchingFields(
+            self, *args, **kwds
+        )
+
+
+    def recordWithCalendarUserAddress(self, *args, **kwds):
+        # This will get cached by the underlying recordWith... call
+        return CalendarDirectoryServiceMixin.recordWithCalendarUserAddress(
+            self, *args, **kwds
+        )
</ins></span></pre></div>
<a id="CalendarServertrunktxdavwhotesttest_cachepy"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/txdav/who/test/test_cache.py (0 => 13822)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/test/test_cache.py                                (rev 0)
+++ CalendarServer/trunk/txdav/who/test/test_cache.py        2014-08-02 00:49:24 UTC (rev 13822)
</span><span class="lines">@@ -0,0 +1,200 @@
</span><ins>+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+&quot;&quot;&quot;
+Caching service tests
+&quot;&quot;&quot;
+
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.test.util import StoreTestCase
+from txdav.who.cache import CachingDirectoryService
+from twext.who.idirectory import (
+    RecordType
+)
+from txdav.who.idirectory import (
+    RecordType as CalRecordType
+)
+import uuid
+
+
+class CacheTest(StoreTestCase):
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(CacheTest, self).setUp()
+
+        self.cachingDirectory = CachingDirectoryService(
+            self.directory,
+            expireSeconds=10
+        )
+        self.storeUnderTest().setDirectoryService(self.cachingDirectory)
+
+
+
+    @inlineCallbacks
+    def test_cachingPassThrough(self):
+        &quot;&quot;&quot;
+        Verify the CachingDirectoryService can pass through method calls to
+        the underlying service.
+        &quot;&quot;&quot;
+
+        dir = self.cachingDirectory
+
+        self.assertTrue(RecordType.user in dir.recordTypes())
+        self.assertTrue(RecordType.group in dir.recordTypes())
+        self.assertTrue(CalRecordType.location in dir.recordTypes())
+        self.assertTrue(CalRecordType.resource in dir.recordTypes())
+
+        records = yield dir.recordsWithRecordType(RecordType.user)
+        self.assertEquals(len(records), 243)
+
+        record = yield dir.recordWithGUID(uuid.UUID(&quot;8166C681-2D08-4846-90F7-97023A6EDDC5&quot;))
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+
+        record = yield dir.recordWithShortName(RecordType.user, u&quot;cache-name-2&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-2&quot;)
+
+        records = yield dir.recordsWithEmailAddress(u&quot;cache-user-1@example.com&quot;)
+        self.assertEquals(len(records), 2)
+
+        record = yield dir.recordWithCalendarUserAddress(u&quot;mailto:cache-user-2@example.com&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-2&quot;)
+
+
+
+    @inlineCallbacks
+    def test_cachingHitsAndMisses(self):
+        &quot;&quot;&quot;
+        Verify faulted in records are indexed appropriately and can be retrieved
+        from the cache even by other attributes.
+        &quot;&quot;&quot;
+
+        dir = self.cachingDirectory
+
+        # Caching on UID
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 0)
+        record = yield dir.recordWithUID(u&quot;cache-uid-1&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 1)
+
+        # Repeat the same lookup
+        record = yield dir.recordWithUID(u&quot;cache-uid-1&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+        self.assertEquals(dir._hitCount, 1)
+        self.assertEquals(dir._requestCount, 2)
+
+        # Lookup the same record, but by GUID
+        record = yield dir.recordWithGUID(uuid.UUID(&quot;8166C681-2D08-4846-90F7-97023A6EDDC5&quot;))
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+        self.assertEquals(dir._hitCount, 2)
+        self.assertEquals(dir._requestCount, 3)
+
+        # Lookup by the shortName for that same record, and it should be a hit
+        record = yield dir.recordWithShortName(RecordType.user, u&quot;cache-name-1&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+        self.assertEquals(dir._hitCount, 3)
+        self.assertEquals(dir._requestCount, 4)
+
+        # Now lookup by a different shortName for that same record, and it
+        # should also be a hit
+        record = yield dir.recordWithShortName(RecordType.user, u&quot;cache-alt-name-1&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+        self.assertEquals(dir._hitCount, 4)
+        self.assertEquals(dir._requestCount, 5)
+
+
+        dir.resetCache()
+
+        # Look up another record which has a unique email address, first by uid
+        # and then by email address and verify this is a cache miss because we
+        # intentionally don't index on email address when faulting in by another
+        # attribute
+        record = yield dir.recordWithUID(u&quot;cache-uid-2&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-2&quot;)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 1)
+
+        records = yield dir.recordsWithEmailAddress(u&quot;cache-user-2@example.com&quot;)
+        self.assertEquals(len(records), 1)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 2)
+
+        records = yield dir.recordsWithEmailAddress(u&quot;cache-user-2@example.com&quot;)
+        self.assertEquals(len(records), 1)
+        self.assertEquals(dir._hitCount, 1)
+        self.assertEquals(dir._requestCount, 3)
+
+        dir.resetCache()
+
+        # Look up a record which has the same email address as another record.
+        record = yield dir.recordWithUID(u&quot;cache-uid-2&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-2&quot;)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 1)
+
+        # Now lookup by the email address for that record, and it should
+        # be a miss;  Note, because there are two records with this email
+        # address, when we repeat this call it will still be a miss because
+        # for simplicity we're only going to cache records when there is a
+        # single result.
+        records = yield dir.recordsWithEmailAddress(u&quot;cache-user-1@example.com&quot;)
+        self.assertEquals(len(records), 2)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 2)
+
+        records = yield dir.recordsWithEmailAddress(u&quot;cache-user-1@example.com&quot;)
+        self.assertEquals(len(records), 2)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 3)
+
+
+    @inlineCallbacks
+    def test_cachingExpiration(self):
+        &quot;&quot;&quot;
+        Verify records expire at the expected time; in these tests, 10 seconds
+        &quot;&quot;&quot;
+
+        dir = self.cachingDirectory
+
+        dir.setTestTime(1.0)
+
+        record = yield dir.recordWithUID(u&quot;cache-uid-1&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 1)
+
+        # 1 second later, the record is still cached
+        dir.setTestTime(2.0)
+        record = yield dir.recordWithUID(u&quot;cache-uid-1&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+        self.assertEquals(dir._hitCount, 1)
+        self.assertEquals(dir._requestCount, 2)
+
+        # 10 seconds later, the record is no longer cached
+        dir.setTestTime(12.0)
+        record = yield dir.recordWithUID(u&quot;cache-uid-1&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+        self.assertEquals(dir._hitCount, 1)
+        self.assertEquals(dir._requestCount, 3)
+
+        # Wait another 11 seconds, verify it's not cached by other attributes
+        dir.setTestTime(23.0)
+        record = yield dir.recordWithShortName(RecordType.user, u&quot;cache-alt-name-1&quot;)
+        self.assertEquals(record.uid, u&quot;cache-uid-1&quot;)
+        self.assertEquals(dir._hitCount, 1)
+        self.assertEquals(dir._requestCount, 4)
</ins></span></pre>
</div>
</div>

</body>
</html>