Skip to content

Commit

Permalink
Added support for unix domain sockets and custom parsers.
Browse files Browse the repository at this point in the history
Added the ability to use tcp sockets or unix domain sockets. Also added the
ability to specify a parser to use when processing responses from the redis
server.  To do this I had to bring back the custom connection pool, since
you need to instantiate a custom connection pool to be able to specify a
parser class.
  • Loading branch information
sebleier committed Aug 17, 2011
1 parent 62c575b commit 9de00c2
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 44 deletions.
42 changes: 38 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,34 @@ Redis Django Cache Backend

A simple Redis cache backend for Django

Changes in 0.9.0
================

Redis cache now allows you to use either a TCP connection or Unix domain
socket to connect to you redis server. Using a TCP connection useful for
when you have your redis server separate from your app server and/or within
a distributed environment. Unix domain sockets are useful if you have your
redis server and application running on the same machine and want the fastest
possible connection.

You can now specify (optionally) what parser class you want redis-py to use
when parsing messages from the redis server. redis-py will pick the best
parser for you implicitly, but using the ``PARSER_CLASS`` setting gives you
control and the option to roll your own parser class if you are so bold.

Notes
-----

This cache backend requires the `redis-py`_ Python client library for communicating with the Redis server.
This cache backend requires the `redis-py`_ Python client library for
communicating with the Redis server.

Redis writes to disk asynchronously so there is a slight chance
Redis writes to disk asynchronously so there is a slight chance
of losing some data, but for most purposes this is acceptable.

Usage
-----

1. Run ``python setup.py install`` to install,
1. Run ``python setup.py install`` to install,
or place ``redis_cache`` on your Python path.

2. Modify your Django settings to use ``redis_cache`` :
Expand All @@ -26,13 +42,31 @@ On Django < 1.3::

On Django >= 1.3::


# When using TCP connections
CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': '<host>:<port>',
'OPTIONS': { # optional
'OPTIONS': {
'DB': 1,
'PASSWORD': 'yadayada',
'PARSER_CLASS': 'redis.connection.HiredisParser'
},
},
}

# When using unix domain sockets
# Note: ``LOCATION`` needs to be the same as the ``unixsocket`` setting
# in your redis.conf
CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': '/path/to/socket/file',
'OPTIONS': {
'DB': 1,
'PASSWORD': 'yadayada',
'PARSER_CLASS': 'redis.connection.HiredisParser'
},
},
}
Expand Down
109 changes: 94 additions & 15 deletions redis_cache/cache.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError
from django.core.exceptions import ImproperlyConfigured
from django.utils import importlib
from django.utils.encoding import smart_unicode, smart_str
from django.utils.datastructures import SortedDict

Expand All @@ -13,7 +14,8 @@
except ImportError:
raise InvalidCacheBackendError(
"Redis cache backend requires the 'redis-py' library")

from redis.connection import UnixDomainSocketConnection, Connection
from redis.connection import DefaultParser

class CacheKey(object):
"""
Expand All @@ -35,6 +37,34 @@ def __unicode__(self):
return smart_str(self._key)


class CacheConnectionPool(object):
_connection_pool = None

def get_connection_pool(self, host='127.0.0.1', port=6379, db=1,
password=None, parser_class=None,
unix_socket_path=None):
if self._connection_pool is None:
connection_class = (
unix_socket_path and UnixDomainSocketConnection or Connection
)
kwargs = {
'db': db,
'password': password,
'connection_class': connection_class,
'parser_class': parser_class,
}
if unix_socket_path is None:
kwargs.update({
'host': host,
'port': port,
})
else:
kwargs['path'] = unix_socket_path
self._connection_pool = redis.ConnectionPool(**kwargs)
return self._connection_pool
pool = CacheConnectionPool()


class CacheClass(BaseCache):
def __init__(self, server, params):
"""
Expand All @@ -44,27 +74,76 @@ def __init__(self, server, params):

def _init(self, server, params):
super(CacheClass, self).__init__(params)
self._initargs = { 'server': server, 'params': params }
options = params.get('OPTIONS', {})
password = params.get('password', options.get('PASSWORD', None))
db = params.get('db', options.get('DB', 1))
try:
db = int(db)
except (ValueError, TypeError):
raise ImproperlyConfigured("db value must be an integer")
if ':' in server:
host, port = server.split(':')
self._server = server
self._params = params

unix_socket_path = None
if ':' in self.server:
host, port = self.server.split(':')
try:
port = int(port)
except (ValueError, TypeError):
raise ImproperlyConfigured("port value must be an integer")
else:
host = server or 'localhost'
port = 6379
self._client = redis.Redis(host=host, port=port, db=db, password=password)
host, port = None, None
unix_socket_path = self.server

kwargs = {
'db': self.db,
'password': self.password,
'host': host,
'port': port,
'unix_socket_path': unix_socket_path,
}
connection_pool = pool.get_connection_pool(
parser_class=self.parser_class,
**kwargs
)
self._client = redis.Redis(
connection_pool=connection_pool,
**kwargs
)

@property
def server(self):
return self._server or "127.0.0.1:6379"

@property
def params(self):
return self._params or {}

@property
def options(self):
return self.params.get('OPTIONS', {})

@property
def db(self):
_db = self.params.get('db', self.options.get('DB', 1))
try:
_db = int(_db)
except (ValueError, TypeError):
raise ImproperlyConfigured("db value must be an integer")
return _db

@property
def password(self):
return self.params.get('password', self.options.get('PASSWORD', None))

@property
def parser_class(self):
cls = self.options.get('PARSER_CLASS', None)
if cls is None:
return DefaultParser
mod_path, cls_name = cls.rsplit('.', 1)
try:
mod = importlib.import_module(mod_path)
parser_class = getattr(mod, cls_name)
except (AttributeError, ImportError):
raise ImproperlyConfigured("Could not find parser class '%s'" % parser_class)
return parser_class

def __getstate__(self):
return self._initargs
return {'params': self._params, 'server': self._server}

def __setstate__(self, state):
self._init(**state)
Expand Down
44 changes: 24 additions & 20 deletions runtests.py → sockettests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,32 @@
from os.path import dirname, abspath
from django.conf import settings

if not settings.configured:
settings.configure(
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
}
},
INSTALLED_APPS = [
'tests.testapp',
],
CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': '127.0.0.1:6379',
'OPTIONS': {
'DB': 15,
'PASSWORD': 'yadayada',
},

cache_settings = {
'DATABASES': {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
}
},
'INSTALLED_APPS': [
'tests.testapp',
],
'ROOT_URLCONF': 'tests.urls',
'CACHES': {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': '/tmp/redis.sock',
'OPTIONS': {
'DB': 15,
'PASSWORD': 'yadayada',
'PARSER_CLASS': 'redis.connection.HiredisParser'
},
},
ROOT_URLCONF = 'tests.urls',
)
},
}

if not settings.configured:
settings.configure(**cache_settings)

from django.test.simple import DjangoTestSuiteRunner

Expand Down
46 changes: 46 additions & 0 deletions tcptests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python
import sys
from os.path import dirname, abspath
from django.conf import settings


cache_settings = {
'DATABASES': {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
}
},
'INSTALLED_APPS': [
'tests.testapp',
],
'ROOT_URLCONF': 'tests.urls',
'CACHES': {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': '127.0.0.1:6379',
'OPTIONS': {
'DB': 15,
'PASSWORD': 'yadayada',
'PARSER_CLASS': 'redis.connection.HiredisParser'
},
},
},
}


if not settings.configured:
settings.configure(**cache_settings)

from django.test.simple import DjangoTestSuiteRunner

def runtests(*test_args):
if not test_args:
test_args = ['testapp']
parent = dirname(abspath(__file__))
sys.path.insert(0, parent)
runner = DjangoTestSuiteRunner(verbosity=1, interactive=True, failfast=False)
failures = runner.run_tests(test_args)
sys.exit(failures)

if __name__ == '__main__':
runtests(*sys.argv[1:])
2 changes: 1 addition & 1 deletion tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
},
}

ROOT_URLCONF = 'tests.urls'
ROOT_URLCONF = 'tests.urls'
14 changes: 10 additions & 4 deletions tests/testapp/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.test import TestCase
from models import Poll, expensive_calculation
from redis_cache.cache import RedisCache, ImproperlyConfigured
from redis.connection import UnixDomainSocketConnection

# functions/classes for complex data type tests
def f():
Expand Down Expand Up @@ -51,10 +52,15 @@ def test_bad_port_initialization(self):

def test_default_initialization(self):
self.reset_pool()
self.cache = self.get_cache('redis_cache.cache://127.0.0.1')
self.assertEqual(self.cache._client.connection_pool.connection_kwargs['host'], '127.0.0.1')
self.assertEqual(self.cache._client.connection_pool.connection_kwargs['db'], 1)
self.assertEqual(self.cache._client.connection_pool.connection_kwargs['port'], 6379)
if VERSION[0] == 1 and VERSION[1] < 3:
self.cache = self.get_cache('redis_cache.cache://')
elif VERSION[0] == 1 and VERSION[1] >= 3:
self.cache = self.get_cache('redis_cache.cache.CacheClass')
connection_class = self.cache._client.connection_pool.connection_class
if connection_class is not UnixDomainSocketConnection:
self.assertEqual(self.cache._client.connection_pool.connection_kwargs['host'], '127.0.0.1')
self.assertEqual(self.cache._client.connection_pool.connection_kwargs['port'], 6379)
self.assertEqual(self.cache._client.connection_pool.connection_kwargs['db'], 15)

def test_simple(self):
# Simple cache set/get works
Expand Down

0 comments on commit 9de00c2

Please sign in to comment.