root/tags/release2_2/ftp_stat_cache.py

Revision 636, 6.5 kB (checked in by schwa, 2 years ago)
Merged in the rest of the changes from the branch add_stat_caching:

svn merge -r580:632 svn://ftputil.sschwarzer.net/ftputil/branches/add_stat_caching .

I don't know for sure why those changes weren't displayed by
`svn log`. It seems to be good to run an `svn up` on both the trunk
and the branch before the actual merge, so the results given by
`svn info` are up to date.
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 # Copyright (C) 2006, 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 """
35 ftp_stat_cache.py - cache for (l)stat data
36 """
37
38 import time
39
40 import lrucache
41
42
43 class CacheMissError(Exception):
44     pass
45
46
47 class StatCache(object):
48     """
49     Implement an LRU (least-recently-used) cache.
50
51     `StatCache` objects have an attribute `max_age`. After this
52     duration after _setting_ it a cache entry will expire. For
53     example, if you code
54
55     my_cache = StatCache()
56     my_cache.max_age = 10
57     my_cache["/home"] = ...
58
59     the value my_cache["/home"] can be retrieved for 10 seconds. After
60     that, the entry will be treated as if it had never been in the
61     cache and should be fetched again from the remote host.
62
63     Note that the `__len__` method does no age tests and thus may
64     include some or many already expired entries.
65     """
66     # default number of cache entries
67     _DEFAULT_CACHE_SIZE = 1000
68
69     def __init__(self):
70         # can be reset with method `resize`
71         self._cache = lrucache.LRUCache(self._DEFAULT_CACHE_SIZE)
72         # never expire
73         self.max_age = None
74         self.enable()
75
76     def enable(self):
77         """Enable storage of stat results."""
78         self._enabled = True
79
80     def disable(self):
81         """
82         Disable the cache. Further storage attempts with `__setitem__`
83         won't have any visible effect.
84
85         Disabling the cache only effects new storage attempts. Values
86         stored before calling `disable` can still be retrieved unless
87         disturbed by a `resize` command or normal cache expiration.
88         """
89         self._enabled = False
90
91     def resize(self, new_size):
92         """
93         Set number of cache entries to the integer `new_size`.
94         If the new size is greater than the current cache size,
95         relatively long-unused elements will be removed.
96         """
97         self._cache.size = new_size
98
99     def _age(self, path):
100         """
101         Return the age of a cache entry for `path` in seconds. If
102         the path isn't in the cache, raise a `CacheMissError`.
103         """
104         try:
105             return time.time() - self._cache.mtime(path)
106         except lrucache.CacheKeyError:
107             raise CacheMissError("no entry for path %s in cache" % path)
108
109     def clear(self):
110         """Clear (invalidate) all cache entries."""
111         old_size = self._cache.size
112         try:
113             # implicitly clear the cache by setting the size to zero
114             self.resize(0)
115         finally:
116             self.resize(old_size)
117
118     def invalidate(self, path):
119         """
120         Invalidate the cache entry for the absolute `path` if present.
121         After that, the stat result data for `path` can no longer be
122         retrieved, as if it had never been stored.
123
124         If no stat result for `path` is in the cache, do _not_
125         raise an exception.
126         """
127         #XXX to be 100 % sure, this should be `host.sep`
128         assert path.startswith("/"), "%s must be an absolute path" % path
129         try:
130             del self._cache[path]
131         except lrucache.CacheKeyError:
132             pass
133
134     def __getitem__(self, path):
135         """
136         Return the stat entry for the `path`. If there's no stored
137         stat entry or the cache is disabled, raise `CacheMissError`.
138         """
139         if not self._enabled:
140             raise CacheMissError("cache is disabled")
141         # possibly raise a `CacheMissError` in `_age`
142         if (self.max_age is not None) and (self._age(path) > self.max_age):
143             self.invalidate(path)
144             raise CacheMissError("entry for path %s has expired" % path)
145         else:
146             #XXX I don't know if this may raise a `CacheMissError` in
147             #  case of race conditions; I'll prefer robust code
148             try:
149                 return self._cache[path]
150             except lrucache.CacheKeyError:
151                 raise CacheMissError("entry for path %s not found" % path)
152
153     def __setitem__(self, path, stat_result):
154         """
155         Put the stat data for `path` into the cache, unless it's
156         disabled.
157         """
158         if not self._enabled:
159             return
160         self._cache[path] = stat_result
161
162     def __contains__(self, path):
163         """
164         Support for the `in` operator. Return a true value, if data
165         for `path` is in the cache, else return a false value.
166         """
167         try:
168             # implicitly do an age test which may raise `CacheMissError`;
169             #  deliberately ignore the return value `stat_result`
170             stat_result = self[path]
171             return True
172         except CacheMissError:
173             return False
174
175     #
176     # the following methods are only intended for debugging!
177     #
178     def __len__(self):
179         """
180         Return the number of entries in the cache. Note that this
181         may include some (or many) expired entries.
182         """
183         return len(self._cache)
184
185     def __str__(self):
186         """Return a string representation of the cache contents."""
187         lines = []
188         for key in sorted(self._cache):
189             lines.append("%s: %s" % (key, self[key]))
190         return "\n".join(lines)
191
Note: See TracBrowser for help on using the browser.