root/branches/add_stat_caching/ftpsync-0.1/simplecache.py

Revision 545, 6.0 kB (checked in by schwa, 2 years ago)
Code contributed by Martin Wilck. (Thank you!) I might use some of
this code to add caching of stat results to ftputil.
  • Property svn:eol-style set to native
Line 
1 import threading
2 import loggingclass
3 import time
4
5 class CacheEntry:
6     """
7     A class representing a cache entry.
8     x = CacheEntry(<some object>)
9     """
10
11     def __init__(self, val):
12         self.val = val
13         self.stamp = time.time()
14
15     def expired(self, period):
16         """
17         Boolean: returns true if the entry is older than period (in sec).
18         """
19         return time.time() - self.stamp > period
20
21     def __cmp__(self, other):
22         """
23         CacheEntry objects can be compared by age.
24         """
25         return cmp(self.stamp, other.stamp)
26
27
28 class Cache(loggingclass.LoggingClass):
29     """
30     A simple cache implementation
31
32     Usage: c = Cache(expire=<expire>, size=<size>, entryclass=CacheEntry)
33     <expire>:   expiration time of entries in sec (default: 60)
34     <size>:     max number of cache entries (default: 1000)
35     <entryclass>: A class for the cache entries (default: CacheEntry)
36
37     NOTE: EXPIRED ENTRIES WILL BE DELETED.
38     Do not use this class (exclusively) to store valuable data.
39
40     Entries are assigned and retrieved through indexing:
41         cache[x] = val
42         try:
43            val = cache[x]
44         except KeyError:
45            print x, ": not in cache"
46         cache.invalidate(x)
47
48     doctest example:
49
50 >>> from time import sleep
51 >>> exp=2.0
52 >>> cache = Cache(size=10, expire=exp)
53 >>> for x in range(0, 10):
54 ...     cache[x] = x*x
55 >>> print cache.contents()
56 [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25), (6, 36), (7, 49), (8, 64), (9, 81)]
57 >>> for x in range(11, 20):
58 ...     cache[x] = x*x
59 >>>
60 >>> # size exceeded - old elements will be deleted
61 >>> print cache.contents()
62 [(11, 121), (12, 144), (13, 169), (14, 196), (15, 225), (16, 256), (17, 289), (18, 324), (19, 361)]
63 >>> sleep(exp/2.)
64 >>> for x in range(0, 5):
65 ...     cache[x] = x*x
66 >>> print cache.contents()
67 [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (16, 256), (17, 289), (18, 324), (19, 361)]
68 >>> sleep(exp/2.+0.1)
69 >>>
70 >>> # elements 11 .. 20 will be expired
71 >>> print cache.contents()
72 [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16)]
73     """
74
75     default_expire = 60
76     default_size = 1000
77     _to_shrink = 100      # No. of entries to delete in a _shrink() call
78
79     def __init__(self, expire = default_expire, size = default_size,
80                  entryclass = CacheEntry):
81         self.__cache = {}
82         self.__size = size
83         self.expire = expire
84         self.__lock = threading.Lock()
85         self._entryclass = entryclass
86
87     def _lock(self):
88         self.__lock.acquire()
89
90     def _unlock(self):
91         self.__lock.release()
92
93     def _invalidate(self, key):
94         if self.__cache.has_key(key):
95             del self.__cache[key]
96             return True
97         return False
98        
99     def len(self):
100         """
101         Return current number of cache entries.
102         """
103         return len(self.__cache)
104
105     def invalidate(self, key):
106         """
107         Invalidate (delete) cache entry indexed by key
108         """
109         self._lock()
110         try:
111             if self._invalidate(key):
112                 self.logger.debug("element %s invalidated" % key)
113         finally:
114             self._unlock()
115
116     def invalidate_all(self):
117         """
118         Clear cache completetly
119         """
120         self._lock()
121         try:
122             self.__cache.clear()
123         finally:
124             self._unlock()
125         self.logger.info("cache cleared")
126
127     def invalidate_some(self, func):
128         """
129         Invalidate all entries for which func(key,val) returns True
130         """
131         self._lock()
132         try:
133             for x in self.__cache.keys():
134                 if func(x, self.__cache[x].val):
135                     self._invalidate(x)
136                     self.logger.debug("element %s invalidated" % x)
137         finally:
138             self._unlock()
139        
140     def _expired(self, entry):
141         return self.expire != 0 and entry.expired(self.expire)
142
143     def __getitem__(self, key):
144         """
145         Implements x = cache[key].
146         Will raise KeyError if the cache entry is expired.
147         """
148         ret = None
149         self._lock()
150         try:
151             entry = self.__cache[key]
152             if self._expired(entry):
153                 self._gc()
154                 raise KeyError
155             else:
156                 ret = entry.val
157         finally:
158             self._unlock()
159
160         return ret
161
162     def __setitem__(self, key, val):
163
164         """
165         Implements cache[key] = y.
166         """
167         self._lock()
168         try:
169             self._invalidate(key)
170             if self.len() >= self.__size:
171                 self._shrink()
172             self.__cache[key] = self._entryclass(val)
173         finally:
174             self._unlock()
175
176     def _shrink(self):
177         # Must be called with lock held
178         target = self.__size - min(self.__size/2, self._to_shrink)
179         self.logger.debug("_shrink: trying to reduce size from %d to %d"
180                           % (self.len(), target))
181
182         self._gc()
183
184         n = self.len() - target
185         if n <= 0:
186             return
187
188         # sort entries by age and invalidate the n oldest
189         keys = self.__cache.keys()
190         keys.sort(lambda a, b, c=self.__cache: cmp(c[a], c[b]))
191
192         self.logger.info("_shrink: invalidating %d entries" % n)
193         for key in keys[:n]:
194             self._invalidate(key)
195
196     def _gc(self):
197         # Must be called with lock held
198         expired = []
199         for key in self.__cache.keys():
200             if self._expired(self.__cache[key]):
201                 expired.append(key)
202         for key in expired:
203             self._invalidate(key)
204         self.logger.info("collecting garbage: %d items invalidated"
205                          % len(expired))
206
207     def contents(self):
208         """
209         returns a list of (key, val) tuples for all non-expired entries
210         """
211         self._lock()
212         try:
213             self._gc()
214             ret = [(x, self.__cache[x].val) for x in self.__cache.keys()]
215         finally:
216             self._unlock()
217         return ret
218
219
220 def _test():
221     import doctest, simplecache
222     doctest.testmod(simplecache)
223
224 if __name__ == "__main__":
225     _test()
Note: See TracBrowser for help on using the browser.