[CalendarServer-changes] [13822] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Fri Aug 1 17:49:24 PDT 2014


Revision: 13822
          http://trac.calendarserver.org//changeset/13822
Author:   sagen at apple.com
Date:     2014-08-01 17:49:24 -0700 (Fri, 01 Aug 2014)
Log Message:
-----------
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.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/txdav/common/datastore/test/accounts/accounts.xml
    CalendarServer/trunk/txdav/dps/server.py
    CalendarServer/trunk/txdav/dps/test/test_client.py

Added Paths:
-----------
    CalendarServer/trunk/txdav/who/cache.py
    CalendarServer/trunk/txdav/who/test/test_cache.py

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- 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)
@@ -94,9 +94,10 @@
 )
 from txdav.common.datastore.work.inbox_cleanup import InboxCleanupWork
 from txdav.common.datastore.work.revision_cleanup import FindMinValidRevisionWork
-from txdav.who.util import directoryFromConfig
 from txdav.dps.client import DirectoryService as DirectoryProxyClientService
+from txdav.who.cache import CachingDirectoryService
 from txdav.who.groups import GroupCacher
+from txdav.who.util import directoryFromConfig
 
 from twistedcaldav import memcachepool
 from twistedcaldav.config import ConfigurationError
@@ -526,25 +527,24 @@
 
         # Add the directory proxy sidecar first so it at least get spawned
         # prior to the caldavd worker processes:
-        if config.DirectoryProxy.Enabled:
-            log.info("Adding directory proxy service")
+        log.info("Adding directory proxy service")
 
-            dpsArgv = [
-                sys.executable,
-                sys.argv[0],
-            ]
-            if config.UserName:
-                dpsArgv.extend(("-u", config.UserName))
-            if config.GroupName:
-                dpsArgv.extend(("-g", config.GroupName))
-            dpsArgv.extend((
-                "--reactor={}".format(config.Twisted.reactor),
-                "-n", "caldav_directoryproxy",
-                "-f", self.configPath,
-            ))
-            self.monitor.addProcess(
-                "directoryproxy", dpsArgv, env=PARENT_ENVIRONMENT
-            )
+        dpsArgv = [
+            sys.executable,
+            sys.argv[0],
+        ]
+        if config.UserName:
+            dpsArgv.extend(("-u", config.UserName))
+        if config.GroupName:
+            dpsArgv.extend(("-g", config.GroupName))
+        dpsArgv.extend((
+            "--reactor={}".format(config.Twisted.reactor),
+            "-n", "caldav_directoryproxy",
+            "-f", self.configPath,
+        ))
+        self.monitor.addProcess(
+            "directoryproxy", dpsArgv, env=PARENT_ENVIRONMENT
+        )
 
         for slaveNumber in xrange(0, config.MultiProcess.ProcessCount):
             if config.UseMetaFD:
@@ -854,6 +854,11 @@
         directory = DirectoryProxyClientService(config.DirectoryRealmName)
         if config.Servers.Enabled:
             directory.setServersDB(buildServersDB(config.Servers.MaxClients))
+        if config.DirectoryProxy.InProcessCachingSeconds:
+            directory = CachingDirectoryService(
+                directory,
+                expireSeconds=config.DirectoryProxy.InProcessCachingSeconds
+            )
         store = storeFromConfig(config, txnFactory, directory)
         logObserver = AMPCommonAccessLoggingObserver()
         result = self.requestProcessingService(options, store, logObserver)
@@ -1491,18 +1496,19 @@
                 if directory is None:
                     # Create a Directory Proxy "Server" service and hand it to
                     # the store.
-                    # 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.
                     if config.Servers.Enabled:
                         serversDB = buildServersDB(config.Servers.MaxClients)
                     else:
                         serversDB = None
-                    store.setDirectoryService(
-                        directoryFromConfig(
-                            config, store=store, serversDB=serversDB
+                    directorySvc = directoryFromConfig(
+                        config, store=store, serversDB=serversDB
+                    )
+                    if config.DirectoryProxy.InProcessCachingSeconds:
+                        directorySvc = CachingDirectoryService(
+                            directorySvc,
+                            expireSeconds=config.DirectoryProxy.InProcessCachingSeconds
                         )
-                    )
+                    store.setDirectoryService(directorySvc)
 
                 pps = PreProcessingService(
                     createMainService, cp, store, logObserver, storageService

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- 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)
@@ -175,6 +175,14 @@
     <key>MaxAllowedInstances</key>
     <integer>3000</integer>
 
+    <key>DirectoryProxy</key>
+    <dict>
+      <key>InProcessCachingSeconds</key>
+      <integer>10</integer>
+      <key>InSidecarCachingSeconds</key>
+      <integer>30</integer>
+    </dict>
+
     <!--
         Directory service
 
@@ -788,7 +796,7 @@
 		<dict>
 			<key>Enabled</key>
 			<true/>
-			
+
 			<!-- Make these short for testing -->
             <key>RequestDelaySeconds</key>
             <real>0.1</real>

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
===================================================================
--- 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)
@@ -297,6 +297,12 @@
                 test_items += (principalURL, alternateURL)
 
             for address in test_items:
+
+                # For txdav.who.test.test_cache I added two accounts which
+                # share the same email address, but that messes up this test.
+                if "mailto:cache-user" in address:
+                    continue
+
                 principal = (
                     yield provisioningResource
                     .principalForCalendarUserAddress(address)

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- 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)
@@ -898,8 +898,9 @@
     },
 
     "DirectoryProxy": {
-        "Enabled": True,
         "SocketPath": "directory-proxy.sock",
+        "InProcessCachingSeconds": 10,
+        "InSidecarCachingSeconds": 30,
     },
 
     #

Modified: CalendarServer/trunk/txdav/common/datastore/test/accounts/accounts.xml
===================================================================
--- 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)
@@ -393,6 +393,35 @@
     <full-name>Empty Group</full-name>
   </record>
 
+  <!-- Caching Directory test records -->
+
+  <record type="user">
+    <uid>cache-uid-1</uid>
+    <guid>8166C681-2D08-4846-90F7-97023A6EDDC5</guid>
+    <short-name>cache-name-1</short-name>
+    <short-name>cache-alt-name-1</short-name>
+    <password>cache-password-1</password>
+    <full-name>Cache User 1</full-name>
+    <email>cache-user-1 at example.com</email>
+  </record>
+
+  <record type="user">
+    <uid>cache-uid-duplicate-1</uid>
+    <short-name>cache-name-duplicate-1</short-name>
+    <short-name>cache-alt-name-duplicate-1</short-name>
+    <password>cache-password-duplicate-1</password>
+    <full-name>Cache User Duplicate 1</full-name>
+    <email>cache-user-1 at example.com</email>
+  </record>
+
+  <record type="user">
+    <uid>cache-uid-2</uid>
+    <short-name>cache-name-2</short-name>
+    <password>cache-password-2</password>
+    <full-name>Cache User 2</full-name>
+    <email>cache-user-2 at example.com</email>
+  </record>
+
   <!-- Calverify test records -->
 
   <record type="user">

Modified: CalendarServer/trunk/txdav/dps/server.py
===================================================================
--- 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)
@@ -18,6 +18,7 @@
 import datetime
 import uuid
 
+from calendarserver.tap.util import getDBPool, storeFromConfig
 from twext.python.log import Logger
 from twext.who.expression import MatchType, MatchFlags, Operand
 from twisted.application import service
@@ -39,6 +40,7 @@
     WikiAccessForUIDCommand, ContinuationCommand
     # UpdateRecordsCommand, RemoveRecordsCommand
 )
+from txdav.who.cache import CachingDirectoryService
 from txdav.who.util import directoryFromConfig
 from txdav.who.wiki import WikiAccessLevel
 from zope.interface import implementer
@@ -571,11 +573,20 @@
             setproctitle("CalendarServer Directory Proxy Service")
 
         try:
+            pool, txnFactory = getDBPool(config)
+            store = storeFromConfig(config, txnFactory, None)
             directory = directoryFromConfig(config)
+            if config.DirectoryProxy.InSidecarCachingSeconds:
+                directory = CachingDirectoryService(
+                    directory,
+                    expireSeconds=config.DirectoryProxy.InSidecarCachingSeconds
+                )
+            store.setDirectoryService(directory)
         except Exception as e:
             log.error("Failed to create directory service", error=e)
             raise
 
+
         log.info("Created directory service")
 
         return strPortsService(

Modified: CalendarServer/trunk/txdav/dps/test/test_client.py
===================================================================
--- 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)
@@ -414,7 +414,7 @@
         records = (yield self.client.recordsWithRecordType(
             RecordType.user
         ))
-        self.assertEquals(len(records), 240)
+        self.assertEquals(len(records), 243)
 
 
     @inlineCallbacks

Added: CalendarServer/trunk/txdav/who/cache.py
===================================================================
--- CalendarServer/trunk/txdav/who/cache.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/who/cache.py	2014-08-02 00:49:24 UTC (rev 13822)
@@ -0,0 +1,334 @@
+# -*- 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 "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Caching Directory Service
+"""
+
+__all__ = [
+    "CachingDirectoryService",
+]
+
+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):
+    """
+    Constants to use for identifying indexes
+    """
+    uid = ValueConstant("uid")
+    guid = ValueConstant("guid")
+    shortName = ValueConstant("shortName")
+    emailAddress = ValueConstant("emailAddress")
+
+
+ at implementer(IDirectoryService, IStoreDirectoryService)
+class CachingDirectoryService(
+    BaseDirectoryService, CalendarDirectoryServiceMixin
+):
+    """
+    Caching directory service.
+
+    This is a directory service that wraps an L{IDirectoryService} and caches
+    directory records.
+    """
+
+    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):
+        """
+        Clear the cache
+        """
+        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):
+        """
+        Store a record in the cache, within the specified indexes
+
+        @param record: the directory record
+        @param indexTypes: an iterable of L{IndexType}
+        """
+
+        if hasattr(self, "_test_time"):
+            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):
+        """
+        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}
+        """
+
+        self._requestCount += 1
+        if key in self._cache[indexType]:
+
+            if hasattr(self, "_test_time"):
+                now = self._test_time
+            else:
+                now = time.time()
+
+            cachedTime, record = self._cache[indexType].get(key, (0.0, None))
+            if now - self._expireSeconds > cachedTime:
+                log.debug(
+                    "Directory cache miss (expired): {index} {key}",
+                    index=indexType.value,
+                    key=key
+                )
+                # This record has expired
+                del self._cache[indexType][key]
+                return None
+
+            log.debug(
+                "Directory cache hit: {index} {key}",
+                index=indexType.value,
+                key=key
+            )
+            self._hitCount += 1
+            return record
+        else:
+            log.debug(
+                "Directory cache miss: {index} {key}",
+                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 "uid" 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
+        )

Added: CalendarServer/trunk/txdav/who/test/test_cache.py
===================================================================
--- 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)
@@ -0,0 +1,200 @@
+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Caching service tests
+"""
+
+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):
+        """
+        Verify the CachingDirectoryService can pass through method calls to
+        the underlying service.
+        """
+
+        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("8166C681-2D08-4846-90F7-97023A6EDDC5"))
+        self.assertEquals(record.uid, u"cache-uid-1")
+
+        record = yield dir.recordWithShortName(RecordType.user, u"cache-name-2")
+        self.assertEquals(record.uid, u"cache-uid-2")
+
+        records = yield dir.recordsWithEmailAddress(u"cache-user-1 at example.com")
+        self.assertEquals(len(records), 2)
+
+        record = yield dir.recordWithCalendarUserAddress(u"mailto:cache-user-2 at example.com")
+        self.assertEquals(record.uid, u"cache-uid-2")
+
+
+
+    @inlineCallbacks
+    def test_cachingHitsAndMisses(self):
+        """
+        Verify faulted in records are indexed appropriately and can be retrieved
+        from the cache even by other attributes.
+        """
+
+        dir = self.cachingDirectory
+
+        # Caching on UID
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 0)
+        record = yield dir.recordWithUID(u"cache-uid-1")
+        self.assertEquals(record.uid, u"cache-uid-1")
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 1)
+
+        # Repeat the same lookup
+        record = yield dir.recordWithUID(u"cache-uid-1")
+        self.assertEquals(record.uid, u"cache-uid-1")
+        self.assertEquals(dir._hitCount, 1)
+        self.assertEquals(dir._requestCount, 2)
+
+        # Lookup the same record, but by GUID
+        record = yield dir.recordWithGUID(uuid.UUID("8166C681-2D08-4846-90F7-97023A6EDDC5"))
+        self.assertEquals(record.uid, u"cache-uid-1")
+        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"cache-name-1")
+        self.assertEquals(record.uid, u"cache-uid-1")
+        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"cache-alt-name-1")
+        self.assertEquals(record.uid, u"cache-uid-1")
+        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"cache-uid-2")
+        self.assertEquals(record.uid, u"cache-uid-2")
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 1)
+
+        records = yield dir.recordsWithEmailAddress(u"cache-user-2 at example.com")
+        self.assertEquals(len(records), 1)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 2)
+
+        records = yield dir.recordsWithEmailAddress(u"cache-user-2 at example.com")
+        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"cache-uid-2")
+        self.assertEquals(record.uid, u"cache-uid-2")
+        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"cache-user-1 at example.com")
+        self.assertEquals(len(records), 2)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 2)
+
+        records = yield dir.recordsWithEmailAddress(u"cache-user-1 at example.com")
+        self.assertEquals(len(records), 2)
+        self.assertEquals(dir._hitCount, 0)
+        self.assertEquals(dir._requestCount, 3)
+
+
+    @inlineCallbacks
+    def test_cachingExpiration(self):
+        """
+        Verify records expire at the expected time; in these tests, 10 seconds
+        """
+
+        dir = self.cachingDirectory
+
+        dir.setTestTime(1.0)
+
+        record = yield dir.recordWithUID(u"cache-uid-1")
+        self.assertEquals(record.uid, u"cache-uid-1")
+        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"cache-uid-1")
+        self.assertEquals(record.uid, u"cache-uid-1")
+        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"cache-uid-1")
+        self.assertEquals(record.uid, u"cache-uid-1")
+        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"cache-alt-name-1")
+        self.assertEquals(record.uid, u"cache-uid-1")
+        self.assertEquals(dir._hitCount, 1)
+        self.assertEquals(dir._requestCount, 4)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140801/421292f8/attachment-0001.html>


More information about the calendarserver-changes mailing list