source: ftp_stat_cache.py @ 737:7853d8340c0f

Last change on this file since 737:7853d8340c0f was 737:7853d8340c0f, checked in by Stefan Schwarzer <sschwarzer@…>, 12 years ago
Explain the use of the literal slash in `invalidate` better.
File size: 6.9 KB
Line 
1# Copyright (C) 2008, Stefan Schwarzer
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met:
7#
8# - Redistributions of source code must retain the above copyright
9#   notice, this list of conditions and the following disclaimer.
10#
11# - Redistributions in binary form must reproduce the above copyright
12#   notice, this list of conditions and the following disclaimer in the
13#   documentation and/or other materials provided with the distribution.
14#
15# - Neither the name of the above author nor the names of the
16#   contributors to the software may be used to endorse or promote
17#   products derived from this software without specific prior written
18#   permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
24# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32# $Id$
33
34"""
35ftp_stat_cache.py - cache for (l)stat data
36"""
37
38import time
39
40import lrucache
41
42
43class CacheMissError(Exception):
44    """Raised if a path isn't found in the cache."""
45    pass
46
47
48class StatCache(object):
49    """
50    Implement an LRU (least-recently-used) cache.
51
52    `StatCache` objects have an attribute `max_age`. After this
53    duration after _setting_ it a cache entry will expire. For
54    example, if you code
55
56    my_cache = StatCache()
57    my_cache.max_age = 10
58    my_cache["/home"] = ...
59
60    the value my_cache["/home"] can be retrieved for 10 seconds. After
61    that, the entry will be treated as if it had never been in the
62    cache and should be fetched again from the remote host.
63
64    Note that the `__len__` method does no age tests and thus may
65    include some or many already expired entries.
66    """
67    # default number of cache entries
68    _DEFAULT_CACHE_SIZE = 1000
69
70    def __init__(self):
71        # can be reset with method `resize`
72        self._cache = lrucache.LRUCache(self._DEFAULT_CACHE_SIZE)
73        # never expire
74        self.max_age = None
75        self.enable()
76
77    def enable(self):
78        """Enable storage of stat results."""
79        # `enable` is called by `__init__`, so it's not set outside `__init__`
80        # pylint: disable-msg=W0201
81        self._enabled = True
82
83    def disable(self):
84        """
85        Disable the cache. Further storage attempts with `__setitem__`
86        won't have any visible effect.
87
88        Disabling the cache only effects new storage attempts. Values
89        stored before calling `disable` can still be retrieved unless
90        disturbed by a `resize` command or normal cache expiration.
91        """
92        self._enabled = False
93
94    def resize(self, new_size):
95        """
96        Set number of cache entries to the integer `new_size`.
97        If the new size is greater than the current cache size,
98        relatively long-unused elements will be removed.
99        """
100        self._cache.size = new_size
101
102    def _age(self, path):
103        """
104        Return the age of a cache entry for `path` in seconds. If
105        the path isn't in the cache, raise a `CacheMissError`.
106        """
107        try:
108            return time.time() - self._cache.mtime(path)
109        except lrucache.CacheKeyError:
110            raise CacheMissError("no entry for path %s in cache" % path)
111
112    def clear(self):
113        """Clear (invalidate) all cache entries."""
114        old_size = self._cache.size
115        try:
116            # implicitly clear the cache by setting the size to zero
117            self.resize(0)
118        finally:
119            self.resize(old_size)
120
121    def invalidate(self, path):
122        """
123        Invalidate the cache entry for the absolute `path` if present.
124        After that, the stat result data for `path` can no longer be
125        retrieved, as if it had never been stored.
126
127        If no stat result for `path` is in the cache, do _not_
128        raise an exception.
129        """
130        #XXX to be 100 % sure, this should be `host.sep`, but I don't
131        #  want to introduce a reference to the `FTPHost` object for
132        #  only that purpose
133        assert path.startswith("/"), "%s must be an absolute path" % path
134        try:
135            del self._cache[path]
136        # don't complain about lazy except clause
137        # pylint: disable-msg=W0704
138        except lrucache.CacheKeyError:
139            # ignore errors
140            pass
141
142    def __getitem__(self, path):
143        """
144        Return the stat entry for the `path`. If there's no stored
145        stat entry or the cache is disabled, raise `CacheMissError`.
146        """
147        if not self._enabled:
148            raise CacheMissError("cache is disabled")
149        # possibly raise a `CacheMissError` in `_age`
150        if (self.max_age is not None) and (self._age(path) > self.max_age):
151            self.invalidate(path)
152            raise CacheMissError("entry for path %s has expired" % path)
153        else:
154            #XXX I don't know if this may raise a `CacheMissError` in
155            #  case of race conditions; I'll prefer robust code
156            try:
157                return self._cache[path]
158            except lrucache.CacheKeyError:
159                raise CacheMissError("entry for path %s not found" % path)
160
161    def __setitem__(self, path, stat_result):
162        """
163        Put the stat data for `path` into the cache, unless it's
164        disabled.
165        """
166        if not self._enabled:
167            return
168        self._cache[path] = stat_result
169
170    def __contains__(self, path):
171        """
172        Support for the `in` operator. Return a true value, if data
173        for `path` is in the cache, else return a false value.
174        """
175        try:
176            # implicitly do an age test which may raise `CacheMissError`;
177            #  deliberately ignore the return value `stat_result`
178            # pylint: disable-msg=W0612
179            stat_result = self[path]
180            return True
181        except CacheMissError:
182            return False
183
184    #
185    # the following methods are only intended for debugging!
186    #
187    def __len__(self):
188        """
189        Return the number of entries in the cache. Note that this
190        may include some (or many) expired entries.
191        """
192        return len(self._cache)
193
194    def __str__(self):
195        """Return a string representation of the cache contents."""
196        lines = []
197        for key in sorted(self._cache):
198            lines.append("%s: %s" % (key, self[key]))
199        return "\n".join(lines)
200
Note: See TracBrowser for help on using the repository browser.