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
0011from time import time as now
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            # it's actually a weakref:
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                # Two threads could hit the cull in a row, but
0093                # that's not so bad.  At least by setting cullCount
0094                # back to zero right away we avoid this.  The cull
0095                # method has a lock, so it's threadsafe.
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                # Two threads could hit the cull in a row, but
0171                # that's not so bad.  At least by setting cullCount
0172                # back to zero right away we avoid this.  The cull
0173                # method has a lock, so it's threadsafe.
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        """
0184        Runs through the cache and expires objects.  E.g., if
0185        ``cullFraction`` is 3, then every third object is moved to
0186        the 'expired' (aka weakref) cache.
0187        """
0188        self.lock.acquire()
0189        try:
0190            keys = self.cache.keys()
0191            for i in xrange(self.cullOffset, len(keys), self.cullFraction):
0192                id = keys[i]
0193                self.expiredCache[id] = ref(self.cache[id])
0194                del self.cache[id]
0195            # This offset tries to balance out which objects we
0196            # expire, so no object will just hang out in the cache
0197            # forever.
0198            self.cullOffset = (self.cullOffset + 1) % self.cullFraction
0199        finally:
0200            self.lock.release()
0201
0202    def clear(self):
0203        """
0204        Removes everything from the cache.  Warning!  This can cause
0205        duplicate objects in memory.
0206        """
0207        if self.doCache:
0208            self.cache.clear()
0209        self.expiredCache.clear()
0210
0211    def expire(self, id):
0212        """
0213        Expires a single object.  Typically called after a delete.
0214        Doesn't even keep a weakref.  (@@: bad name?)
0215        """
0216        if not self.doCache:
0217            return
0218        self.lock.acquire()
0219        try:
0220            if self.cache.has_key(id):
0221                del self.cache[id]
0222            if self.expiredCache.has_key(id):
0223                del self.expiredCache[id]
0224        finally:
0225            self.lock.release()
0226
0227    def expireAll(self):
0228        """
0229        Expires all objects, moving them all into the expired/weakref
0230        cache.
0231        """
0232        if not self.doCache:
0233            return
0234        self.lock.acquire()
0235        try:
0236            for key, value in self.cache.items():
0237                self.expiredCache[key] = ref(value)
0238            self.cache = {}
0239        finally:
0240            self.lock.release()
0241
0242    def allIDs(self):
0243        """
0244        Returns the IDs of all objects in the cache.
0245        """
0246        if self.doCache:
0247            all = self.cache.keys()
0248        else:
0249            all = []
0250        for id, value in self.expiredCache.items():
0251            if value():
0252                all.append(id)
0253        return all
0254
0255    def getAll(self):
0256        """
0257        Return all the objects in the cache.
0258        """
0259        if self.doCache:
0260            all = self.cache.values()
0261        else:
0262            all = []
0263        for value in self.expiredCache.values():
0264            if value():
0265                all.append(value())
0266        return all
0267
0268class CacheSet(object):
0269
0270    """
0271    A CacheSet is used to collect and maintain a series of caches.  In
0272    SQLObject, there is one CacheSet per connection, and one Cache
0273    in the CacheSet for each class, since IDs are not unique across
0274    classes.  It contains methods similar to Cache, but that take
0275    a ``cls`` argument.
0276    """
0277
0278    def __init__(self, *args, **kw):
0279        self.caches = {}
0280        self.args = args
0281        self.kw = kw
0282
0283    def get(self, id, cls):
0284        try:
0285            return self.caches[cls.__name__].get(id)
0286        except KeyError:
0287            self.caches[cls.__name__] = CacheFactory(*self.args, **self.kw)
0288            return self.caches[cls.__name__].get(id)
0289
0290    def put(self, id, cls, obj):
0291        self.caches[cls.__name__].put(id, obj)
0292
0293    def finishPut(self, cls):
0294        self.caches[cls.__name__].finishPut()
0295
0296    def created(self, id, cls, obj):
0297        try:
0298            self.caches[cls.__name__].created(id, obj)
0299        except KeyError:
0300            self.caches[cls.__name__] = CacheFactory(*self.args, **self.kw)
0301            self.caches[cls.__name__].created(id, obj)
0302
0303    def expire(self, id, cls):
0304        try:
0305            self.caches[cls.__name__].expire(id)
0306        except KeyError:
0307            pass
0308
0309    def clear(self, cls=None):
0310        if cls is None:
0311            for cache in self.caches.values():
0312                cache.clear()
0313        elif self.caches.has_key(cls.__name__):
0314            self.caches[cls.__name__].clear()
0315
0316    def tryGet(self, id, cls):
0317        return self.tryGetByName(id, cls.__name__)
0318
0319    def tryGetByName(self, id, clsname):
0320        try:
0321            return self.caches[clsname].tryGet(id)
0322        except KeyError:
0323            return None
0324
0325    def allIDs(self, cls):
0326        try:
0327            self.caches[cls.__name__].allIDs()
0328        except KeyError:
0329            return []
0330
0331    def allSubCaches(self):
0332        return self.caches.values()
0333
0334    def allSubCachesByClassNames(self):
0335        return self.caches
0336
0337    def weakrefAll(self, cls=None):
0338        """
0339        Move all objects in the cls (or if not given, then in all
0340        classes) to the weakref dictionary, where they can be
0341        collected.
0342        """
0343        if cls is None:
0344            for cache in self.caches.values():
0345                cache.expireAll()
0346        elif self.caches.has_key(cls.__name__):
0347            self.caches[cls.__name__].expireAll()
0348
0349    def getAll(self, cls=None):
0350        """
0351        Returns all instances in the cache for the given class or all
0352        classes.
0353        """
0354        if cls is None:
0355            results = []
0356            for cache in self.caches.values():
0357                results.extend(cache.getAll())
0358            return results
0359        elif cls.__name__ in self.caches:
0360            return self.caches[cls.__name__].getAll()
0361        else:
0362            return []