0001"""
0002This implements the instance caching in SQLObject. Caching is
0003relatively aggressive. All objects are retained so long as they are
0004in memory, by keeping weak references to objects. We also keep other
0005objects in a cache that doesn't allow them to be garbage collected
0006(unless caching is turned off).
0007"""
0008
0009import threading
0010from weakref import ref
0011
0012
0013class CacheFactory(object):
0014
0015 """
0016 CacheFactory caches object creation. Each object should be
0017 referenced by a single hashable ID (note tuples of hashable
0018 values are also hashable).
0019
0020 """
0021
0022 def __init__(self, cullFrequency=100, cullFraction=2,
0023 cache=True):
0024 """
0025 Every cullFrequency times that an item is retrieved from
0026 this cache, the cull method is called.
0027
0028 The cull method then expires an arbitrary fraction of
0029 the cached objects. The idea is at no time will the cache
0030 be entirely emptied, placing a potentially high load at that
0031 moment, but everything object will have its time to go
0032 eventually. The fraction is given as an integer, and one
0033 in that many objects are expired (i.e., the default is 1/2
0034 of objects are expired).
0035
0036 By setting cache to False, items won't be cached.
0037
0038 However, in all cases a weak reference is kept to created
0039 objects, and if the object hasn't been garbage collected
0040 it will be returned.
0041 """
0042
0043 self.cullFrequency = cullFrequency
0044 self.cullCount = 0
0045 self.cullOffset = 0
0046 self.cullFraction = cullFraction
0047 self.doCache = cache
0048
0049 if self.doCache:
0050 self.cache = {}
0051 self.expiredCache = {}
0052 self.lock = threading.Lock()
0053
0054 def tryGet(self, id):
0055 """
0056 This returns None, or the object in cache.
0057 """
0058 value = self.expiredCache.get(id)
0059 if value:
0060
0061 return value()
0062 if not self.doCache:
0063 return None
0064 return self.cache.get(id)
0065
0066 def get(self, id):
0067 """
0068 This method can cause deadlocks! tryGet is safer
0069
0070 This returns the object found in cache, or None. If None,
0071 then the cache will remain locked! This is so that the
0072 calling function can create the object in a threadsafe manner
0073 before releasing the lock. You should use this like (note
0074 that ``cache`` is actually a CacheSet object in this
0075 example)::
0076
0077 obj = cache.get(some_id, my_class)
0078 if obj is None:
0079 try:
0080 obj = create_object(some_id)
0081 cache.put(some_id, my_class, obj)
0082 finally:
0083 cache.finishPut(cls)
0084
0085 This method checks both the main cache (which retains
0086 references) and the 'expired' cache, which retains only weak
0087 references.
0088 """
0089
0090 if self.doCache:
0091 if self.cullCount > self.cullFrequency:
0092
0093
0094
0095
0096 self.cullCount = 0
0097 self.cull()
0098 else:
0099 self.cullCount = self.cullCount + 1
0100
0101 try:
0102 return self.cache[id]
0103 except KeyError:
0104 pass
0105 self.lock.acquire()
0106 try:
0107 val = self.cache[id]
0108 except KeyError:
0109 pass
0110 else:
0111 self.lock.release()
0112 return val
0113 try:
0114 val = self.expiredCache[id]()
0115 except KeyError:
0116 return None
0117 else:
0118 del self.expiredCache[id]
0119 if val is None:
0120 return None
0121 self.cache[id] = val
0122 self.lock.release()
0123 return val
0124
0125 else:
0126 try:
0127 val = self.expiredCache[id]()
0128 if val is not None:
0129 return val
0130 except KeyError:
0131 pass
0132 self.lock.acquire()
0133 try:
0134 val = self.expiredCache[id]()
0135 except KeyError:
0136 return None
0137 else:
0138 if val is None:
0139 del self.expiredCache[id]
0140 return None
0141 self.lock.release()
0142 return val
0143
0144 def put(self, id, obj):
0145 """
0146 Puts an object into the cache. Should only be called after
0147 .get(), so that duplicate objects don't end up in the cache.
0148 """
0149 if self.doCache:
0150 self.cache[id] = obj
0151 else:
0152 self.expiredCache[id] = ref(obj)
0153
0154 def finishPut(self):
0155 """
0156 Releases the lock that is retained when .get() is called and
0157 returns None.
0158 """
0159 self.lock.release()
0160
0161 def created(self, id, obj):
0162 """
0163 Inserts and object into the cache. Should be used when no one
0164 else knows about the object yet, so there cannot be any object
0165 already in the cache. After a database INSERT is an example
0166 of this situation.
0167 """
0168 if self.doCache:
0169 if self.cullCount > self.cullFrequency:
0170
0171
0172
0173
0174 self.cullCount = 0
0175 self.cull()
0176 else:
0177 self.cullCount = self.cullCount + 1
0178 self.cache[id] = obj
0179 else:
0180 self.expiredCache[id] = ref(obj)
0181
0182 def cull(self):
0183 """Runs through the cache and expires objects
0184
0185 E.g., if ``cullFraction`` is 3, then every third object is moved to
0186 the 'expired' (aka weakref) cache.
0187
0188 """
0189 self.lock.acquire()
0190 try:
0191
0192 keys = list(self.expiredCache.keys())
0193 for key in keys:
0194 if self.expiredCache[key]() is None:
0195 self.expiredCache.pop(key, None)
0196
0197 keys = list(self.cache.keys())
0198 for i in range(self.cullOffset, len(keys), self.cullFraction):
0199 id = keys[i]
0200
0201 obj = ref(self.cache[id])
0202 del self.cache[id]
0203
0204
0205
0206 if obj() is not None:
0207 self.expiredCache[id] = obj
0208
0209
0210
0211 self.cullOffset = (self.cullOffset + 1) % self.cullFraction
0212 finally:
0213 self.lock.release()
0214
0215 def clear(self):
0216 """
0217 Removes everything from the cache. Warning! This can cause
0218 duplicate objects in memory.
0219 """
0220 if self.doCache:
0221 self.cache.clear()
0222 self.expiredCache.clear()
0223
0224 def expire(self, id):
0225 """
0226 Expires a single object. Typically called after a delete.
0227 Doesn't even keep a weakref. (@@: bad name?)
0228 """
0229 if not self.doCache:
0230 return
0231 self.lock.acquire()
0232 try:
0233 if id in self.cache:
0234 del self.cache[id]
0235 if id in self.expiredCache:
0236 del self.expiredCache[id]
0237 finally:
0238 self.lock.release()
0239
0240 def expireAll(self):
0241 """
0242 Expires all objects, moving them all into the expired/weakref
0243 cache.
0244 """
0245 if not self.doCache:
0246 return
0247 self.lock.acquire()
0248 try:
0249 for key, value in self.cache.items():
0250 self.expiredCache[key] = ref(value)
0251 self.cache = {}
0252 finally:
0253 self.lock.release()
0254
0255 def allIDs(self):
0256 """
0257 Returns the IDs of all objects in the cache.
0258 """
0259 if self.doCache:
0260 all = list(self.cache.keys())
0261 else:
0262 all = []
0263 for id, value in self.expiredCache.items():
0264 if value():
0265 all.append(id)
0266 return all
0267
0268 def getAll(self):
0269 """
0270 Return all the objects in the cache.
0271 """
0272 if self.doCache:
0273 all = list(self.cache.values())
0274 else:
0275 all = []
0276 for value in self.expiredCache.values():
0277 if value():
0278 all.append(value())
0279 return all
0280
0281
0282class CacheSet(object):
0283
0284 """
0285 A CacheSet is used to collect and maintain a series of caches. In
0286 SQLObject, there is one CacheSet per connection, and one Cache
0287 in the CacheSet for each class, since IDs are not unique across
0288 classes. It contains methods similar to Cache, but that take
0289 a ``cls`` argument.
0290 """
0291
0292 def __init__(self, *args, **kw):
0293 self.caches = {}
0294 self.args = args
0295 self.kw = kw
0296
0297 def get(self, id, cls):
0298 try:
0299 return self.caches[cls.__name__].get(id)
0300 except KeyError:
0301 self.caches[cls.__name__] = CacheFactory(*self.args, **self.kw)
0302 return self.caches[cls.__name__].get(id)
0303
0304 def put(self, id, cls, obj):
0305 self.caches[cls.__name__].put(id, obj)
0306
0307 def finishPut(self, cls):
0308 self.caches[cls.__name__].finishPut()
0309
0310 def created(self, id, cls, obj):
0311 try:
0312 self.caches[cls.__name__].created(id, obj)
0313 except KeyError:
0314 self.caches[cls.__name__] = CacheFactory(*self.args, **self.kw)
0315 self.caches[cls.__name__].created(id, obj)
0316
0317 def expire(self, id, cls):
0318 try:
0319 self.caches[cls.__name__].expire(id)
0320 except KeyError:
0321 pass
0322
0323 def clear(self, cls=None):
0324 if cls is None:
0325 for cache in self.caches.values():
0326 cache.clear()
0327 elif cls.__name__ in self.caches:
0328 self.caches[cls.__name__].clear()
0329
0330 def tryGet(self, id, cls):
0331 return self.tryGetByName(id, cls.__name__)
0332
0333 def tryGetByName(self, id, clsname):
0334 try:
0335 return self.caches[clsname].tryGet(id)
0336 except KeyError:
0337 return None
0338
0339 def allIDs(self, cls):
0340 try:
0341 self.caches[cls.__name__].allIDs()
0342 except KeyError:
0343 return []
0344
0345 def allSubCaches(self):
0346 return self.caches.values()
0347
0348 def allSubCachesByClassNames(self):
0349 return self.caches
0350
0351 def weakrefAll(self, cls=None):
0352 """
0353 Move all objects in the cls (or if not given, then in all
0354 classes) to the weakref dictionary, where they can be
0355 collected.
0356 """
0357 if cls is None:
0358 for cache in self.caches.values():
0359 cache.expireAll()
0360 elif cls.__name__ in self.caches:
0361 self.caches[cls.__name__].expireAll()
0362
0363 def getAll(self, cls=None):
0364 """
0365 Returns all instances in the cache for the given class or all
0366 classes.
0367 """
0368 if cls is None:
0369 results = []
0370 for cache in self.caches.values():
0371 results.extend(cache.getAll())
0372 return results
0373 elif cls.__name__ in self.caches:
0374 return self.caches[cls.__name__].getAll()
0375 else:
0376 return []