| 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, |
|---|
| | 22 | BACKENDS = { |
|---|
| | 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', |
|---|