Django

Code

Changeset 2378

Show
Ignore:
Timestamp:
02/24/06 00:07:01 (3 years ago)
Author:
adrian
Message:

Refactored cache from django/core/cache.py into django/core/cache package, with each backend getting a separate module. This keeps things cleaner and uses less memory, because the backend module is only loaded if it's needed.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/core/cache/__init__.py

    r1922 r2378  
    22Caching framework. 
    33 
    4 This module defines set of cache backends that all conform to a simple API. 
     4This package defines set of cache backends that all conform to a simple API. 
    55In a nutshell, a cache is a set of values -- which can be any object that 
    66may be pickled -- identified by string keys.  For the complete API, see 
    7 the abstract Cache object, below
     7the abstract BaseCache class in django.core.cache.backends.base
    88 
    9 Client code should not access a cache backend directly; instead 
    10 it should use the get_cache() function.  This function will look at 
    11 settings.CACHE_BACKEND and use that to create and load a cache object. 
     9Client code should not access a cache backend directly; instead it should 
     10either use the "cache" variable made available here, or it should use the 
     11get_cache() function made available here. get_cache() takes a backend URI 
     12(e.g. "memcached://127.0.0.1:11211/") and returns an instance of a backend 
     13cache class. 
    1214 
    13 The CACHE_BACKEND setting is a quasi-URI; examples are: 
    14  
    15     memcached://127.0.0.1:11211/    A memcached backend; the server is running 
    16                                     on localhost port 11211.  You can use 
    17                                     multiple memcached servers by separating 
    18                                     them with semicolons. 
    19  
    20     db://tablename/                 A database backend in a table named 
    21                                     "tablename". This table should be created 
    22                                     with "django-admin createcachetable". 
    23  
    24     file:///var/tmp/django_cache/   A file-based cache stored in the directory 
    25                                     /var/tmp/django_cache/. 
    26  
    27     simple:///                      A simple single-process memory cache; you 
    28                                     probably don't want to use this except for 
    29                                     testing. Note that this cache backend is 
    30                                     NOT threadsafe! 
    31  
    32     locmem:///                      A more sophisticated local memory cache; 
    33                                     this is multi-process- and thread-safe. 
    34  
    35     dummy:///                       Doesn't actually cache. For use in test 
    36                                     environments. 
    37  
    38 All caches may take arguments; these are given in query-string style.  Valid 
    39 arguments are: 
    40  
    41     timeout 
    42         Default timeout, in seconds, to use for the cache.  Defaults 
    43         to 5 minutes (300 seconds). 
    44  
    45     max_entries 
    46         For the simple, file, and database backends, the maximum number of 
    47         entries allowed in the cache before it is cleaned.  Defaults to 
    48         300. 
    49  
    50     cull_percentage 
    51         The percentage of entries that are culled when max_entries is reached. 
    52         The actual percentage is 1/cull_percentage, so set cull_percentage=3 to 
    53         cull 1/3 of the entries when max_entries is reached. 
    54  
    55         A value of 0 for cull_percentage means that the entire cache will be 
    56         dumped when max_entries is reached.  This makes culling *much* faster 
    57         at the expense of more cache misses. 
    58  
    59 For example: 
    60  
    61     memcached://127.0.0.1:11211/?timeout=60 
    62     db://tablename/?timeout=120&max_entries=500&cull_percentage=4 
    63  
    64 Invalid arguments are silently ignored, as are invalid values of known 
    65 arguments. 
     15See docs/cache.txt for information on the public API. 
    6616""" 
    6717 
    68 ############## 
    69 # Exceptions # 
    70 ############## 
     18from cgi import parse_qsl 
     19from django.conf import settings 
     20from django.core.cache.backends.base import InvalidCacheBackendError 
    7121 
    72 class InvalidCacheBackendError(Exception): 
    73     pass 
    74  
    75 ################################ 
    76 # Abstract base implementation # 
    77 ################################ 
    78  
    79 class _Cache: 
    80     def __init__(self, params): 
    81         timeout = params.get('timeout', 300) 
    82         try: 
    83             timeout = int(timeout) 
    84         except (ValueError, TypeError): 
    85             timeout = 300 
    86         self.default_timeout = timeout 
    87  
    88     def get(self, key, default=None): 
    89         ''' 
    90         Fetch a given key from the cache.  If the key does not exist, return 
    91         default, which itself defaults to None. 
    92         ''' 
    93         raise NotImplementedError 
    94  
    95     def set(self, key, value, timeout=None): 
    96         ''' 
    97         Set a value in the cache.  If timeout is given, that timeout will be 
    98         used for the key; otherwise the default cache timeout will be used. 
    99         ''' 
    100         raise NotImplementedError 
    101  
    102     def delete(self, key): 
    103         ''' 
    104         Delete a key from the cache, failing silently. 
    105         ''' 
    106         raise NotImplementedError 
    107  
    108     def get_many(self, keys): 
    109         ''' 
    110         Fetch a bunch of keys from the cache.  For certain backends (memcached, 
    111         pgsql) this can be *much* faster when fetching multiple values. 
    112  
    113         Returns a dict mapping each key in keys to its value.  If the given 
    114         key is missing, it will be missing from the response dict. 
    115         ''' 
    116         d = {} 
    117         for k in keys: 
    118             val = self.get(k) 
    119             if val is not None: 
    120                 d[k] = val 
    121         return d 
    122  
    123     def has_key(self, key): 
    124         ''' 
    125         Returns True if the key is in the cache and has not expired. 
    126         ''' 
    127         return self.get(key) is not None 
    128  
    129 ########################### 
    130 # memcached cache backend # 
    131 ########################### 
    132  
    133 try: 
    134     import memcache 
    135 except ImportError: 
    136     _MemcachedCache = None 
    137 else: 
    138     class _MemcachedCache(_Cache): 
    139         "Memcached cache backend." 
    140         def __init__(self, server, params): 
    141             _Cache.__init__(self, params) 
    142             self._cache = memcache.Client(server.split(';')) 
    143  
    144         def get(self, key, default=None): 
    145             val = self._cache.get(key) 
    146             if val is None: 
    147                 return default 
    148             else: 
    149                 return val 
    150  
    151         def set(self, key, value, timeout=0): 
    152             self._cache.set(key, value, timeout) 
    153  
    154         def delete(self, key): 
    155             self._cache.delete(key) 
    156  
    157         def get_many(self, keys): 
    158             return self._cache.get_multi(keys) 
    159  
    160 ################################## 
    161 # Single-process in-memory cache # 
    162 ################################## 
    163  
    164 import time 
    165  
    166 class _SimpleCache(_Cache): 
    167     "Simple single-process in-memory cache." 
    168     def __init__(self, host, params): 
    169         _Cache.__init__(self, params) 
    170         self._cache = {} 
    171         self._expire_info = {} 
    172  
    173         max_entries = params.get('max_entries', 300) 
    174         try: 
    175             self._max_entries = int(max_entries) 
    176         except (ValueError, TypeError): 
    177             self._max_entries = 300 
    178  
    179         cull_frequency = params.get('cull_frequency', 3) 
    180         try: 
    181             self._cull_frequency = int(cull_frequency) 
    182         except (ValueError, TypeError): 
    183             self._cull_frequency = 3 
    184  
    185     def get(self, key, default=None): 
    186         now = time.time() 
    187         exp = self._expire_info.get(key) 
    188         if exp is None: 
    189             return default 
    190         elif exp < now: 
    191             del self._cache[key] 
    192             del self._expire_info[key] 
    193             return default 
    194         else: 
    195             return self._cache[key] 
    196  
    197     def set(self, key, value, timeout=None): 
    198         if len(self._cache) >= self._max_entries: 
    199             self._cull() 
    200         if timeout is None: 
    201             timeout = self.default_timeout 
    202         self._cache[key] = value 
    203         self._expire_info[key] = time.time() + timeout 
    204  
    205     def delete(self, key): 
    206         try: 
    207             del self._cache[key] 
    208         except KeyError: 
    209             pass 
    210         try: 
    211             del self._expire_info[key] 
    212         except KeyError: 
    213             pass 
    214  
    215     def has_key(self, key): 
    216         return self._cache.has_key(key) 
    217  
    218     def _cull(self): 
    219         if self._cull_frequency == 0: 
    220             self._cache.clear() 
    221             self._expire_info.clear() 
    222         else: 
    223             doomed = [k for (i, k) in enumerate(self._cache) if i % self._cull_frequency == 0] 
    224             for k in doomed: 
    225                 self.delete(k) 
    226  
    227 ############################### 
    228 # Thread-safe in-memory cache # 
    229 ############################### 
    230  
    231 try: 
    232     import cPickle as pickle 
    233 except ImportError: 
    234     import pickle 
    235 import copy 
    236 from django.utils.synch import RWLock 
    237  
    238 class _LocMemCache(_SimpleCache): 
    239     "Thread-safe in-memory cache." 
    240     def __init__(self, host, params): 
    241         _SimpleCache.__init__(self, host, params) 
    242         self._lock = RWLock() 
    243  
    244     def get(self, key, default=None): 
    245         should_delete = False 
    246         self._lock.reader_enters() 
    247         try: 
    248             now = time.time() 
    249             exp = self._expire_info.get(key) 
    250             if exp is None: 
    251                 return default 
    252             elif exp < now: 
    253                 should_delete = True 
    254             else: 
    255                 return copy.deepcopy(self._cache[key]) 
    256         finally: 
    257             self._lock.reader_leaves() 
    258         if should_delete: 
    259             self._lock.writer_enters() 
    260             try: 
    261                 del self._cache[key] 
    262                 del self._expire_info[key] 
    263                 return default 
    264             finally: 
    265                 self._lock.writer_leaves() 
    266  
    267     def set(self, key, value, timeout=None): 
    268         self._lock.writer_enters() 
    269         try: 
    270             _SimpleCache.set(self, key, value, timeout) 
    271         finally: 
    272             self._lock.writer_leaves() 
    273  
    274     def delete(self, key): 
    275         self._lock.writer_enters() 
    276         try: 
    277             _SimpleCache.delete(self, key) 
    278         finally: 
    279             self._lock.writer_leaves() 
    280  
    281 ############### 
    282 # Dummy cache # 
    283 ############### 
    284  
    285 class _DummyCache(_Cache): 
    286     def __init__(self, *args, **kwargs): 
    287         pass 
    288  
    289     def get(self, *args, **kwargs): 
    290         pass 
    291  
    292     def set(self, *args, **kwargs): 
    293         pass 
    294  
    295     def delete(self, *args, **kwargs): 
    296         pass 
    297  
    298     def get_many(self, *args, **kwargs): 
    299         pass 
    300  
    301     def has_key(self, *args, **kwargs): 
    302         return False 
    303  
    304 #################### 
    305 # File-based cache # 
    306 #################### 
    307  
    308 import os 
    309 import urllib 
    310  
    311 class _FileCache(_SimpleCache): 
    312     "File-based cache." 
    313     def __init__(self, dir, params): 
    314         self._dir = dir 
    315         if not os.path.exists(self._dir): 
    316             self._createdir() 
    317         _SimpleCache.__init__(self, dir, params) 
    318         del self._cache 
    319         del self._expire_info 
    320  
    321     def get(self, key, default=None): 
    322         fname = self._key_to_file(key) 
    323         try: 
    324             f = open(fname, 'rb') 
    325             exp = pickle.load(f) 
    326             now = time.time() 
    327             if exp < now: 
    328                 f.close() 
    329                 os.remove(fname) 
    330             else: 
    331                 return pickle.load(f) 
    332         except (IOError, OSError, EOFError, pickle.PickleError): 
    333             pass 
    334         return default 
    335  
    336     def set(self, key, value, timeout=None): 
    337         fname = self._key_to_file(key) 
    338         if timeout is None: 
    339             timeout = self.default_timeout 
    340         try: 
    341             filelist = os.listdir(self._dir) 
    342         except (IOError, OSError): 
    343             self._createdir() 
    344             filelist = [] 
    345         if len(filelist) > self._max_entries: 
    346             self._cull(filelist) 
    347         try: 
    348             f = open(fname, 'wb') 
    349             now = time.time() 
    350             pickle.dump(now + timeout, f, 2) 
    351             pickle.dump(value, f, 2) 
    352         except (IOError, OSError): 
    353             pass 
    354  
    355     def delete(self, key): 
    356         try: 
    357             os.remove(self._key_to_file(key)) 
    358         except (IOError, OSError): 
    359             pass 
    360  
    361     def has_key(self, key): 
    362         return os.path.exists(self._key_to_file(key)) 
    363  
    364     def _cull(self, filelist): 
    365         if self._cull_frequency == 0: 
    366             doomed = filelist 
    367         else: 
    368             doomed = [k for (i, k) in enumerate(filelist) if i % self._cull_frequency == 0] 
    369         for fname in doomed: 
    370             try: 
    371                 os.remove(os.path.join(self._dir, fname)) 
    372             except (IOError, OSError): 
    373                 pass 
    374  
    375     def _createdir(self): 
    376         try: 
    377             os.makedirs(self._dir) 
    378         except OSError: 
    379             raise EnvironmentError, "Cache directory '%s' does not exist and could not be created'" % self._dir 
    380  
    381     def _key_to_file(self, key): 
    382         return os.path.join(self._dir, urllib.quote_plus(key)) 
    383  
    384 ############# 
    385 # SQL cache # 
    386 ############# 
    387  
    388 import base64 
    389 from django.core.db import db, DatabaseError 
    390 from datetime import datetime 
    391  
    392 class _DBCache(_Cache): 
    393     "SQL cache backend." 
    394     def __init__(self, table, params): 
    395         _Cache.__init__(self, params) 
    396         self._table = table 
    397         max_entries = params.get('max_entries', 300) 
    398         try: 
    399             self._max_entries = int(max_entries) 
    400         except (ValueError, TypeError): 
    401             self._max_entries = 300 
    402         cull_frequency = params.get('cull_frequency', 3) 
    403         try: 
    404             self._cull_frequency = int(cull_frequency) 
    405         except (ValueError, TypeError): 
    406             self._cull_frequency = 3 
    407  
    408     def get(self, key, default=None): 
    409         cursor = db.cursor() 
    410         cursor.execute("SELECT cache_key, value, expires FROM %s WHERE cache_key = %%s" % self._table, [key]) 
    411         row = cursor.fetchone() 
    412         if row is None: 
    413             return default 
    414         now = datetime.now() 
    415         if row[2] < now: 
    416             cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) 
    417             db.commit() 
    418             return default 
    419         return pickle.loads(base64.decodestring(row[1])) 
    420  
    421     def set(self, key, value, timeout=None): 
    422         if timeout is None: 
    423             timeout = self.default_timeout 
    424         cursor = db.cursor() 
    425         cursor.execute("SELECT COUNT(*) FROM %s" % self._table) 
    426         num = cursor.fetchone()[0] 
    427         now = datetime.now().replace(microsecond=0) 
    428         exp = datetime.fromtimestamp(time.time() + timeout).replace(microsecond=0) 
    429         if num > self._max_entries: 
    430             self._cull(cursor, now) 
    431         encoded = base64.encodestring(pickle.dumps(value, 2)).strip() 
    432         cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s" % self._table, [key]) 
    433         try: 
    434             if cursor.fetchone(): 
    435                 cursor.execute("UPDATE %s SET value = %%s, expires = %%s WHERE cache_key = %%s" % self._table, [encoded, str(exp), key]) 
    436             else: 
    437                 cursor.execute("INSERT INTO %s (cache_key, value, expires) VALUES (%%s, %%s, %%s)" % self._table, [key, encoded, str(exp)]) 
    438         except DatabaseError: 
    439             # To be threadsafe, updates/inserts are allowed to fail silently 
    440             pass 
    441         else: 
    442             db.commit() 
    443  
    444     def delete(self, key): 
    445         cursor = db.cursor() 
    446         cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) 
    447         db.commit() 
    448  
    449     def has_key(self, key): 
    450         cursor = db.cursor() 
    451         cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s" % self._table, [key]) 
    452         return cursor.fetchone() is not None 
    453  
    454     def _cull(self, cursor, now): 
    455         if self._cull_frequency == 0: 
    456             cursor.execute("DELETE FROM %s" % self._table) 
    457         else: 
    458             cursor.execute("DELETE FROM %s WHERE expires < %%s" % self._table, [str(now)]) 
    459             cursor.execute("SELECT COUNT(*) FROM %s" % self._table) 
    460             num = cursor.fetchone()[0] 
    461             if num > self._max_entries: 
    462                 cursor.execute("SELECT cache_key FROM %s ORDER BY cache_key LIMIT 1 OFFSET %%s" % self._table, [num / self._cull_frequency]) 
    463                 cursor.execute("DELETE FROM %s WHERE cache_key < %%s" % self._table, [cursor.fetchone()[0]]) 
    464  
    465 ########################################## 
    466 # Read settings and load a cache backend # 
    467 ########################################## 
    468  
    469 from cgi import parse_qsl 
    470  
    471 _BACKENDS = { 
    472     'memcached': _MemcachedCache, 
    473     'simple': _SimpleCache, 
    474     'locmem': _LocMemCache, 
    475     'file': _FileCache, 
    476     'db': _DBCache, 
    477     'dummy': _DummyCache, 
     22BACKENDS = { 
     23    # name for use in settings file --> name of module in "backends" directory 
     24    'memcached': 'memcached', 
     25    'simple': 'simple', 
     26    'locmem': 'locmem', 
     27    'file': 'filebased', 
     28    'db': 'db', 
     29    'dummy': 'dummy', 
    47830} 
    47931 
    48032def get_cache(backend_uri): 
    48133    if backend_uri.find(':') == -1: 
    482         raise InvalidCacheBackendError("Backend URI must start with scheme://") 
     34        raise InvalidCacheBackendError, "Backend URI must start with scheme://" 
    48335    scheme, rest = backend_uri.split(':', 1) 
    48436    if not rest.startswith('//'): 
    485         raise InvalidCacheBackendError("Backend URI must start with scheme://") 
    486     if scheme not in _BACKENDS.keys()
    487         raise InvalidCacheBackendError("%r is not a valid cache backend" % scheme) 
     37        raise InvalidCacheBackendError, "Backend URI must start with scheme://" 
     38    if scheme not in BACKENDS
     39        raise InvalidCacheBackendError, "%r is not a valid cache backend" % scheme 
    48840 
    48941    host = rest[2:] 
     
    49749        host = host[:-1] 
    49850 
    499     return _BACKENDS[scheme](host, params) 
     51    cache_class = getattr(__import__('django.core.cache.backends.%s' % BACKENDS[scheme], '', '', ['']), 'CacheClass') 
     52    return cache_class(host, params) 
    50053 
    501 from django.conf.settings import CACHE_BACKEND 
    502 cache = get_cache(CACHE_BACKEND) 
     54cache = get_cache(settings.CACHE_BACKEND)