Added "locmem" and "file" cache backends. "locmem" is a thread-safe local-memory cache, and "file" is a file-based cache.

This refs #515; much thanks to Eugene Lazutkin!


git-svn-id: http://code.djangoproject.com/svn/django/trunk@686 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2005-09-25 20:01:35 +00:00
parent 26c4356e01
commit 272eab5cd8
2 changed files with 230 additions and 11 deletions

View File

@ -15,17 +15,22 @@ The CACHE_BACKEND setting is a quasi-URI; examples are:
memcached://127.0.0.1:11211/ A memcached backend; the server is running memcached://127.0.0.1:11211/ A memcached backend; the server is running
on localhost port 11211. on localhost port 11211.
pgsql://tablename/ A pgsql backend (the pgsql backend uses sql://tablename/ A SQL backend. If you use this backend,
the same database/username as the rest of you must have django.contrib.cache in
the CMS, so only a table name is needed.) INSTALLED_APPS, and you must have installed
the tables for django.contrib.cache.
file:///var/tmp/django.cache/ A file-based cache at /var/tmp/django.cache file:///var/tmp/django_cache/ A file-based cache stored in the directory
/var/tmp/django_cache/.
simple:/// A simple single-process memory cache; you simple:/// A simple single-process memory cache; you
probably don't want to use this except for probably don't want to use this except for
testing. Note that this cache backend is testing. Note that this cache backend is
NOT threadsafe! NOT threadsafe!
locmem:/// A more sophisticaed local memory cache;
this is multi-process- and thread-safe.
All caches may take arguments; these are given in query-string style. Valid All caches may take arguments; these are given in query-string style. Valid
arguments are: arguments are:
@ -50,13 +55,10 @@ arguments are:
For example: For example:
memcached://127.0.0.1:11211/?timeout=60 memcached://127.0.0.1:11211/?timeout=60
pgsql://tablename/?timeout=120&max_entries=500&cull_percentage=4 sql://tablename/?timeout=120&max_entries=500&cull_percentage=4
Invalid arguments are silently ignored, as are invalid values of known Invalid arguments are silently ignored, as are invalid values of known
arguments. arguments.
So far, only the memcached and simple backend have been implemented; backends
using postgres, and file-system storage are planned.
""" """
############## ##############
@ -181,13 +183,15 @@ class _SimpleCache(_Cache):
def get(self, key, default=None): def get(self, key, default=None):
now = time.time() now = time.time()
exp = self._expire_info.get(key, now) exp = self._expire_info.get(key)
if exp is not None and exp < now: if exp is None:
return default
elif exp < now:
del self._cache[key] del self._cache[key]
del self._expire_info[key] del self._expire_info[key]
return default return default
else: else:
return self._cache.get(key, default) return self._cache[key]
def set(self, key, value, timeout=None): def set(self, key, value, timeout=None):
if len(self._cache) >= self._max_entries: if len(self._cache) >= self._max_entries:
@ -219,6 +223,134 @@ class _SimpleCache(_Cache):
for k in doomed: for k in doomed:
self.delete(k) self.delete(k)
###############################
# Thread-safe in-memory cache #
###############################
try:
import cPickle as pickle
except ImportError:
import pickle
from django.utils.synch import RWLock
class _LocMemCache(_SimpleCache):
"""Thread-safe in-memory cache"""
def __init__(self, host, params):
_SimpleCache.__init__(self, host, params)
self._lock = RWLock()
def get(self, key, default=None):
should_delete = False
self._lock.reader_enters()
try:
now = time.time()
exp = self._expire_info.get(key)
if exp is None:
return default
elif exp < now:
should_delete = True
else:
return self._cache[key]
finally:
self._lock.reader_leaves()
if should_delete:
self._lock.writer_enters()
try:
del self._cache[key]
del self._expire_info[key]
return default
finally:
self._lock.writer_leaves()
def set(self, key, value, timeout=None):
self._lock.writer_enters()
try:
_SimpleCache.set(self, key, value, timeout)
finally:
self._lock.writer_leaves()
def delete(self, key):
self._lock.writer_enters()
try:
_SimpleCache.delete(self, key)
finally:
self._lock.writer_leaves()
####################
# File-based cache #
####################
import os
import urllib
class _FileCache(_SimpleCache):
"""File-based cache"""
def __init__(self, dir, params):
self._dir = dir
if not os.path.exists(self._dir):
try:
os.makedirs(self._dir)
except OSError:
raise EnvironmentError, "Cache directory '%s' does not exist and could not be created'" % self._dir
_SimpleCache.__init__(self, dir, params)
del self._cache
del self._expire_info
def get(self, key, default=None):
fname = self._key_to_file(key)
try:
f = open(fname, 'rb')
exp = pickle.load(f)
now = time.time()
if exp < now:
f.close()
os.remove(fname)
else:
return pickle.load(f)
except (IOError, pickle.PickleError):
pass
return default
def set(self, key, value, timeout=None):
fname = self._key_to_file(key)
if timeout is None:
timeout = self.default_timeout
filelist = os.listdir(self._dir)
if len(filelist) > self._max_entries:
self._cull(filelist)
try:
f = open(fname, 'wb')
now = time.time()
pickle.dump(now + timeout, f, 2)
pickle.dump(value, f, 2)
except (IOError, OSError):
raise
def delete(self, key):
try:
os.remove(self._key_to_file(key))
except (IOError, OSError):
pass
def has_key(self, key):
return os.path.exists(self._key_to_file(key))
def _cull(self, filelist):
if self.cull_frequency == 0:
doomed = filelist
else:
doomed = [k for (i, k) in enumerate(filelist) if i % self._cull_frequency == 0]
for fname in doomed:
try:
os.remove(os.path.join(self._dir, fname))
except (IOError, OSError):
pass
def _key_to_file(self, key):
return os.path.join(self._dir, urllib.quote_plus(key))
########################################## ##########################################
# Read settings and load a cache backend # # Read settings and load a cache backend #
########################################## ##########################################
@ -228,6 +360,8 @@ from cgi import parse_qsl
_BACKENDS = { _BACKENDS = {
'memcached' : _MemcachedCache, 'memcached' : _MemcachedCache,
'simple' : _SimpleCache, 'simple' : _SimpleCache,
'locmem' : _LocMemCache,
'file' : _FileCache,
} }
def get_cache(backend_uri): def get_cache(backend_uri):

85
django/utils/synch.py Normal file
View File

@ -0,0 +1,85 @@
"""
Synchronization primitives:
- reader-writer lock (preference to writers)
(Contributed to Django by eugene@lazutkin.com)
"""
import threading
class RWLock:
"""
Classic implementation of reader-writer lock with preference to writers.
Readers can access a resource simultaneously.
Writers get an exclusive access.
API is self-descriptive:
reader_enters()
reader_leaves()
writer_enters()
writer_leaves()
"""
def __init__(self):
self.mutex = threading.RLock()
self.can_read = threading.Semaphore(0)
self.can_write = threading.Semaphore(0)
self.active_readers = 0
self.active_writers = 0
self.waiting_readers = 0
self.waiting_writers = 0
def reader_enters(self):
self.mutex.acquire()
try:
if self.active_writers == 0 and self.waiting_writers == 0:
self.active_readers += 1
self.can_read.release()
else:
self.waiting_readers += 1
finally:
self.mutex.release()
self.can_read.acquire()
def reader_leaves(self):
self.mutex.acquire()
try:
self.active_readers -= 1
if self.active_readers == 0 and self.waiting_writers != 0:
self.active_writers += 1
self.waiting_writers -= 1
self.can_write.release()
finally:
self.mutex.release()
def writer_enters(self):
self.mutex.acquire()
try:
if self.active_writers == 0 and self.waiting_writers == 0 and self.active_readers == 0:
self.active_writers += 1
self.can_write.release()
else:
self.waiting_writers += 1
finally:
self.mutex.release()
self.can_write.acquire()
def writer_leaves(self):
self.mutex.acquire()
try:
self.active_writers -= 1
if self.waiting_writers != 0:
self.active_writers += 1
self.waiting_writers -= 1
self.can_write.release()
elif self.waiting_readers != 0:
t = self.waiting_readers
self.waiting_readers = 0
self.active_readers += t
while t > 0:
self.can_read.release()
t -= 1
finally:
self.mutex.release()