forked from devhub/django-newcache
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5c1fae7
Showing
4 changed files
with
237 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
*.pyc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
django-newcache | ||
=============== | ||
|
||
Newcache is an improved memcached cache backend for Django. It provides two | ||
major advantages over Django's built-in cache backend: | ||
|
||
* It supports pylibmc. | ||
* It allows for a function to be run on each key before it's sent to memcached. | ||
|
||
It also has some pretty nice defaults. By default, the function that's run on | ||
each key is one that hashes, versions, and flavors the key. More on that | ||
later. | ||
|
||
|
||
How to Install | ||
-------------- | ||
|
||
The simplest way is to just set it as your cache backend in your settings.py, | ||
like so:: | ||
|
||
CACHE_BACKEND = 'newcache://127.0.0.1:11211/?binary=true' | ||
|
||
Note that we've passed an additional argument, binary, to the backend. This | ||
is because pylibmc supports using binary mode to talk to memcached. This is a | ||
completely optional parameter, and can be omitted safely to use the old text | ||
mode. It is ignored when using python-memcached. | ||
|
||
|
||
Default Behavior | ||
---------------- | ||
|
||
Earlier we said that by default it hashes, versions, and flavors each key. What | ||
does this mean? Let's go through each item in detail. | ||
|
||
Keys in memcached come with many restrictions, both on their length and on | ||
their contents. Practically speaking, this means that you can't put spaces | ||
in your keys, and they can't be very long. One simple solution to this is to | ||
create an md5 hash of whatever key you want, and use the hash as your key | ||
instead. That is what we do in newcache. It not only allows for long keys, | ||
but it also lets us put spaces or other characters in our key as well. | ||
|
||
Sometimes it's necessary to clear the entire cache. We can do this using | ||
memcached's flushing mechanisms, but sometimes a cache is shared by many things | ||
instead of just one web app. It's a shame to have everything lose its | ||
fresh cache just because one web app needed to clear its cache. For this, we | ||
introduce a simple technique called versioning. A version number is added to | ||
each cache key, and when this version is incremented, all the old cache keys | ||
will become invalid because they have an incorrect version. | ||
|
||
This is exposed as a new setting, CACHE_VERSION, and it defaults to 1. | ||
|
||
Finally, we found that as we split our site out into development, staging, and | ||
production, we didn't want them to share the same cache. But we also didn't | ||
want to spin up a new memcached instance for each one. So we came up with the | ||
idea of flavoring the cache. The concept is simple--add a FLAVOR setting and | ||
make it something like 'dev', 'prod', or 'test'. With newcache, this flavor | ||
string will be added to each key, ensuring that there are no collisions. | ||
|
||
Concretely, this is what happens:: | ||
|
||
# CACHE_VERSION = 2 | ||
# FLAVOR = 'staging' | ||
cache.get('games') | ||
# ... would actually call ... | ||
cache.get('staging-2-9cfa7aefcc61936b70aaec6729329eda') | ||
|
||
|
||
Changing the Default | ||
-------------------- | ||
|
||
All of the above is simply the default, you may provide your own callable | ||
function to be run on each key, by supplying the CACHE_KEY_FUNC setting. It | ||
must take in any instance of basestring and output a str. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
"Modified memcached cache backend" | ||
|
||
import hashlib | ||
import time | ||
|
||
from threading import local | ||
|
||
from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError | ||
from django.utils import importlib | ||
from django.utils.encoding import smart_str | ||
from django.conf import settings | ||
|
||
try: | ||
import pylibmc as memcache | ||
NotFoundError = memcache.NotFound | ||
using_pylibmc = True | ||
except ImportError: | ||
using_pylibmc = False | ||
try: | ||
import memcache | ||
NotFoundError = ValueError | ||
except ImportError: | ||
raise InvalidCacheBackendError('Memcached cache backend requires ' + | ||
'either the "pylibmc" or "memcache" library') | ||
|
||
# Flavor is used amongst multiple apps to differentiate the "flavor" of the | ||
# environment. Examples of flavors are 'prod', 'staging', 'dev', and 'test'. | ||
FLAVOR = getattr(settings, 'FLAVOR', '') | ||
|
||
CACHE_VERSION = str(getattr(settings, 'CACHE_VERSION', 1)) | ||
CACHE_BEHAVIORS = getattr(settings, 'CACHE_BEHAVIORS', {'hash': 'crc'}) | ||
CACHE_KEY_FUNC = getattr(settings, 'CACHE_KEY_FUNC', | ||
'newcache.default_key_func') | ||
|
||
def default_key_func(key): | ||
""" | ||
Returns a hashed, versioned, flavored version of the string that was input. | ||
""" | ||
hashed = hashlib.md5(smart_str(key)).hexdigest() | ||
return ''.join((settings.FLAVOR, '-', CACHE_VERSION, '-', hashed)) | ||
|
||
get_key = importlib.import_module(CACHE_KEY_FUNC) | ||
|
||
class CacheClass(BaseCache): | ||
|
||
def __init__(self, server, params): | ||
super(CacheClass, self).__init__(params) | ||
self._servers = server.split(';') | ||
self._use_binary = bool(params.get('binary')) | ||
self._local = local() | ||
|
||
@property | ||
def _cache(self): | ||
""" | ||
Implements transparent thread-safe access to a memcached client. | ||
""" | ||
client = getattr(self._local, 'client', None) | ||
if client: | ||
return client | ||
|
||
# Use binary mode if it's both supported and requested | ||
if using_pylibmc and self._use_binary: | ||
client = memcache.Client(self._servers, binary=True) | ||
else: | ||
client = memcache.Client(self._servers) | ||
|
||
# If we're using pylibmc, set the behaviors according to settings | ||
if using_pylibmc: | ||
client.behaviors = CACHE_BEHAVIORS | ||
|
||
self._local.client = client | ||
return client | ||
|
||
def _get_memcache_timeout(self, timeout): | ||
""" | ||
Memcached deals with long (> 30 days) timeouts in a special | ||
way. Call this function to obtain a safe value for your timeout. | ||
""" | ||
timeout = timeout or self.default_timeout | ||
if timeout > 2592000: # 60*60*24*30, 30 days | ||
# See http://code.google.com/p/memcached/wiki/FAQ | ||
# "You can set expire times up to 30 days in the future. After that | ||
# memcached interprets it as a date, and will expire the item after | ||
# said date. This is a simple (but obscure) mechanic." | ||
# | ||
# This means that we have to switch to absolute timestamps. | ||
timeout += int(time.time()) | ||
return timeout | ||
|
||
def add(self, key, value, timeout=None): | ||
return self._cache.add(get_key(key), value, | ||
self._get_memcache_timeout(timeout)) | ||
|
||
def get(self, key, default=None): | ||
val = self._cache.get(get_key(key)) | ||
if val is None: | ||
return default | ||
return val | ||
|
||
def set(self, key, value, timeout=None): | ||
self._cache.set(get_key(key), value, | ||
self._get_memcache_timeout(timeout)) | ||
|
||
def delete(self, key): | ||
self._cache.delete(get_key(key)) | ||
|
||
def get_many(self, keys): | ||
return self._cache.get_multi(map(get_key, keys)) | ||
|
||
def close(self, **kwargs): | ||
self._cache.disconnect_all() | ||
|
||
def incr(self, key, delta=1): | ||
try: | ||
return self._cache.incr(get_key(key), delta) | ||
except NotFoundError: | ||
raise ValueError("Key '%s' not found" % (key,)) | ||
|
||
def decr(self, key, delta=1): | ||
try: | ||
return self._cache.decr(get_key(key), delta) | ||
except NotFoundError: | ||
raise ValueError("Key '%s' not found" % (key,)) | ||
|
||
def set_many(self, data, timeout=0): | ||
safe_data = dict(((get_key(k), v) for k, v in data.iteritems())) | ||
self._cache.set_multi(safe_data, self._get_memcache_timeout(timeout)) | ||
|
||
def delete_many(self, keys): | ||
self._cache.delete_multi(map(get_key, keys)) | ||
|
||
def clear(self): | ||
self._cache.flush_all() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import os | ||
|
||
from setuptools import setup, find_packages | ||
|
||
VERSION = '0.1' | ||
|
||
setup( | ||
name='django-newcache', | ||
version=VERSION, | ||
description='Improved memcached cache backend for Django', | ||
long_description=file( | ||
os.path.join(os.path.dirname(__file__), 'README.txt') | ||
).read(), | ||
author='Eric Florenzano', | ||
author_email='floguy@gmail.com', | ||
license='BSD', | ||
url='http://github.com/ericflo/django-newcache', | ||
classifiers=[ | ||
'Development Status :: 4 - Beta', | ||
'Intended Audience :: Developers', | ||
'Programming Language :: Python', | ||
'Topic :: Software Development :: Libraries :: Python Modules', | ||
'Framework :: Django', | ||
'Environment :: Web Environment', | ||
], | ||
zip_safe=False, | ||
packages=find_packages(), | ||
include_package_data=True, | ||
install_requires=['setuptools'], | ||
) |