Commit 9ef12f52 authored by Sean Bleier's avatar Sean Bleier

Merge pull request #85 from sebleier/unstable

Unstable
parents eb85ab99 176a0d2d
...@@ -19,4 +19,3 @@ MANIFEST ...@@ -19,4 +19,3 @@ MANIFEST
.venv .venv
redis/ redis/
*/_build/ */_build/
requirements_local.txt
language: python language: python
python: python:
- "2.6"
- "2.7" - "2.7"
- "3.2" - "3.2"
- "3.3" - "3.3"
...@@ -11,6 +10,7 @@ env: ...@@ -11,6 +10,7 @@ env:
- DJANGO_VERSION=1.7 - DJANGO_VERSION=1.7
- DJANGO_VERSION=1.8 - DJANGO_VERSION=1.8
# command to run tests # command to run tests
install: ./install_redis.sh
script: make test DJANGO_VERSION=$DJANGO_VERSION script: make test DJANGO_VERSION=$DJANGO_VERSION
branches: branches:
only: only:
......
SHELL := /bin/bash SHELL := /bin/bash
PACKAGE_NAME=redis_cache
VENV_DIR?=.venv PACKAGE_NAME=redis_cache
VENV_ACTIVATE=$(VENV_DIR)/bin/activate
WITH_VENV=. $(VENV_ACTIVATE);
DJANGO_VERSION?=1.7 DJANGO_VERSION?=1.7
default:
python setup.py check build
$(VENV_ACTIVATE): requirements*.txt
test -f $@ || virtualenv --system-site-packages $(VENV_DIR)
touch $@
.PHONY: install_requirements .PHONY: install_requirements
install_requirements: requirements*.txt install_requirements: requirements*.txt
$(WITH_VENV) pip install --no-deps -r requirements.txt pip install -r requirements.txt
$(WITH_VENV) pip install --no-deps -r requirements-dev.txt pip install -r requirements-dev.txt
$(WITH_VENV) $(test -f requirements-local.txt && pip install -r requirements-local.txt) pip install Django==$(DJANGO_VERSION)
$(WITH_VENV) pip install Django==$(DJANGO_VERSION)
.PHONY: venv
venv: $(VENV_ACTIVATE)
.PHONY: setup
setup: venv
.PHONY: redis_servers
redis_servers:
test -d redis || git clone https://github.com/antirez/redis
git -C redis checkout 2.6
make -C redis
for i in 1 2 3; do \
./redis/src/redis-server \
--pidfile /tmp/redis`echo $$i`.pid \
--requirepass yadayada \
--daemonize yes \
--port `echo 638$$i` ; \
done
for i in 4 5 6; do \
./redis/src/redis-server \
--pidfile /tmp/redis`echo $$i`.pid \
--requirepass yadayada \
--daemonize yes \
--port 0 \
--unixsocket /tmp/redis`echo $$i`.sock \
--unixsocketperm 755 ; \
done
./redis/src/redis-server \
--pidfile /tmp/redis7.pid \
--requirepass yadayada \
--daemonize yes \
--port 6387 ;
for i in 8 9; do \
./redis/src/redis-server \
--pidfile /tmp/redis`echo $$i`.pid \
--requirepass yadayada \
--daemonize yes \
--masterauth yadayada \
--slaveof 127.0.0.1 6387 \
--port `echo 638$$i` ; \
done
.PHONY: clean .PHONY: clean
clean: clean:
...@@ -74,18 +18,11 @@ clean: ...@@ -74,18 +18,11 @@ clean:
rm -rf __pycache__/ rm -rf __pycache__/
rm -f MANIFEST rm -f MANIFEST
rm -f test.db rm -f test.db
find $(PACKAGE_NAME) -type f -name '*.pyc' -delete
.PHONY: teardown
teardown:
rm -rf $(VENV_DIR)/
.PHONY: test .PHONY: test
test: venv install_requirements redis_servers test: install_requirements
$(WITH_VENV) PYTHONPATH=$(PYTHONPATH): django-admin.py test --settings=tests.settings -s PYTHONPATH=$(PYTHONPATH): django-admin.py test --settings=tests.settings -s
for i in 1 2 3 4 5 6 7 8 9; do kill `cat /tmp/redis$$i.pid`; done;
.PHONY: shell .PHONY: shell
shell: venv shell:
$(WITH_VENV) PYTHONPATH=$(PYTHONPATH): django-admin.py shell --settings=tests.settings PYTHONPATH=$(PYTHONPATH): django-admin.py shell --settings=tests.settings
#!/bin/bash
: ${REDIS_VERSION:="2.6"}
test -d redis || git clone https://github.com/antirez/redis
git -C redis checkout $REDIS_VERSION
make -C redis
from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils import importlib from django.utils import importlib
from django.utils.functional import cached_property
from django.utils.importlib import import_module from django.utils.importlib import import_module
from redis_cache.compat import bytes_type, smart_bytes, DEFAULT_TIMEOUT from redis_cache.compat import smart_bytes, DEFAULT_TIMEOUT
try: try:
import cPickle as pickle import cPickle as pickle
...@@ -14,12 +13,14 @@ except ImportError: ...@@ -14,12 +13,14 @@ except ImportError:
try: try:
import redis import redis
except ImportError: except ImportError:
raise InvalidCacheBackendError("Redis cache backend requires the 'redis-py' library") raise InvalidCacheBackendError(
"Redis cache backend requires the 'redis-py' library"
)
from redis.connection import DefaultParser from redis.connection import DefaultParser
from redis_cache.connection import pool from redis_cache.connection import pool
from redis_cache.utils import CacheKey from redis_cache.utils import CacheKey, get_servers, parse_connection_kwargs
from functools import wraps from functools import wraps
...@@ -34,7 +35,6 @@ def get_client(write=False): ...@@ -34,7 +35,6 @@ def get_client(write=False):
version = kwargs.pop('version', None) version = kwargs.pop('version', None)
client = self.get_client(key, write=write) client = self.get_client(key, write=write)
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
return method(self, client, key, *args, **kwargs) return method(self, client, key, *args, **kwargs)
return wrapped return wrapped
...@@ -50,7 +50,7 @@ class BaseRedisCache(BaseCache): ...@@ -50,7 +50,7 @@ class BaseRedisCache(BaseCache):
""" """
super(BaseRedisCache, self).__init__(params) super(BaseRedisCache, self).__init__(params)
self.server = server self.server = server
self.servers = self.get_servers(server) self.servers = get_servers(server)
self.params = params or {} self.params = params or {}
self.options = params.get('OPTIONS', {}) self.options = params.get('OPTIONS', {})
self.clients = {} self.clients = {}
...@@ -71,20 +71,6 @@ class BaseRedisCache(BaseCache): ...@@ -71,20 +71,6 @@ class BaseRedisCache(BaseCache):
def __setstate__(self, state): def __setstate__(self, state):
self.__init__(**state) self.__init__(**state)
def get_servers(self, server):
"""returns a list of servers given the server argument passed in
from Django.
"""
if isinstance(server, bytes_type):
servers = server.split(',')
elif hasattr(server, '__iter__'):
servers = server
else:
raise ImproperlyConfigured(
'"server" must be an iterable or string'
)
return servers
def get_db(self): def get_db(self):
_db = self.params.get('db', self.options.get('DB', 1)) _db = self.params.get('db', self.options.get('DB', 1))
try: try:
...@@ -136,49 +122,29 @@ class BaseRedisCache(BaseCache): ...@@ -136,49 +122,29 @@ class BaseRedisCache(BaseCache):
Get the write server:port of the master cache Get the write server:port of the master cache
""" """
cache = self.options.get('MASTER_CACHE', None) cache = self.options.get('MASTER_CACHE', None)
return self.client_list[0] if cache is None else self.create_client(cache) if cache is None:
return next(iter(self.client_list))
def create_client(self, server): kwargs = parse_connection_kwargs(cache, db=self.db)
kwargs = { return self.clients[(
'db': self.db, kwargs['host'],
'password': self.password, kwargs['port'],
} kwargs['db'],
if '://' in server: kwargs['unix_socket_path'],
client = redis.Redis.from_url( )]
server,
parser_class=self.parser_class,
**kwargs
)
unix_socket_path = client.connection_pool.connection_kwargs.get('path')
kwargs.update(
client.connection_pool.connection_kwargs,
unix_socket_path=unix_socket_path,
)
else:
unix_socket_path = None
if ':' in server:
host, port = server.rsplit(':', 1)
try:
port = int(port)
except (ValueError, TypeError):
raise ImproperlyConfigured("Port value must be an integer")
else:
host, port = None, None
unix_socket_path = server
kwargs.update(
host=host,
port=port,
unix_socket_path=unix_socket_path,
)
client = redis.Redis(**kwargs)
def create_client(self, server):
kwargs = parse_connection_kwargs(
server,
db=self.db,
password=self.password,
)
client = redis.Redis(**kwargs)
kwargs.update( kwargs.update(
parser_class=self.parser_class, parser_class=self.parser_class,
connection_pool_class=self.connection_pool_class, connection_pool_class=self.connection_pool_class,
connection_pool_class_kwargs=self.connection_pool_class_kwargs, connection_pool_class_kwargs=self.connection_pool_class_kwargs,
) )
connection_pool = pool.get_connection_pool(client, **kwargs) connection_pool = pool.get_connection_pool(client, **kwargs)
client.connection_pool = connection_pool client.connection_pool = connection_pool
return client return client
......
from collections import defaultdict from collections import defaultdict
from django.core.exceptions import ImproperlyConfigured
from redis_cache.backends.base import BaseRedisCache from redis_cache.backends.base import BaseRedisCache
from redis_cache.compat import DEFAULT_TIMEOUT
from redis_cache.sharder import HashRing from redis_cache.sharder import HashRing
...@@ -20,9 +17,8 @@ class ShardedRedisCache(BaseRedisCache): ...@@ -20,9 +17,8 @@ class ShardedRedisCache(BaseRedisCache):
self.client_list = self.clients.values() self.client_list = self.clients.values()
def get_client(self, key, write=False): def get_client(self, key, write=False):
node = self.sharder.get_node(unicode(key)) node = self.sharder.get_node(key)
return self.clients[node] return self.clients[node]
def shard(self, keys, write=False, version=None): def shard(self, keys, write=False, version=None):
...@@ -31,7 +27,9 @@ class ShardedRedisCache(BaseRedisCache): ...@@ -31,7 +27,9 @@ class ShardedRedisCache(BaseRedisCache):
""" """
clients = defaultdict(list) clients = defaultdict(list)
for key in keys: for key in keys:
clients[self.get_client(key, write)].append(self.make_key(key, version)) clients[self.get_client(key, write)].append(
self.make_key(key, version)
)
return clients return clients
#################### ####################
...@@ -54,7 +52,7 @@ class ShardedRedisCache(BaseRedisCache): ...@@ -54,7 +52,7 @@ class ShardedRedisCache(BaseRedisCache):
namespace will be deleted. Otherwise, all keys will be deleted. namespace will be deleted. Otherwise, all keys will be deleted.
""" """
if version is None: if version is None:
for client in self.clients.itervalues(): for client in self.clients.values():
self._clear(client) self._clear(client)
else: else:
self.delete_pattern('*', version=version) self.delete_pattern('*', version=version)
...@@ -64,7 +62,13 @@ class ShardedRedisCache(BaseRedisCache): ...@@ -64,7 +62,13 @@ class ShardedRedisCache(BaseRedisCache):
clients = self.shard(keys, version=version) clients = self.shard(keys, version=version)
for client, versioned_keys in clients.items(): for client, versioned_keys in clients.items():
original_keys = [key._original_key for key in versioned_keys] original_keys = [key._original_key for key in versioned_keys]
data.update(self._get_many(client, original_keys, versioned_keys=versioned_keys)) data.update(
self._get_many(
client,
original_keys,
versioned_keys=versioned_keys
)
)
return data return data
def set_many(self, data, timeout=None, version=None): def set_many(self, data, timeout=None, version=None):
...@@ -113,12 +117,12 @@ class ShardedRedisCache(BaseRedisCache): ...@@ -113,12 +117,12 @@ class ShardedRedisCache(BaseRedisCache):
def delete_pattern(self, pattern, version=None): def delete_pattern(self, pattern, version=None):
pattern = self.make_key(pattern, version=version) pattern = self.make_key(pattern, version=version)
for client in self.clients.itervalues(): for client in self.clients.values():
self._delete_pattern(client, pattern) self._delete_pattern(client, pattern)
def reinsert_keys(self): def reinsert_keys(self):
""" """
Reinsert cache entries using the current pickle protocol version. Reinsert cache entries using the current pickle protocol version.
""" """
for client in self.clients.itervalues(): for client in self.clients.values():
self._reinsert_keys(client) self._reinsert_keys(client)
...@@ -5,7 +5,6 @@ except ImportError: ...@@ -5,7 +5,6 @@ except ImportError:
import random import random
from redis_cache.backends.base import BaseRedisCache from redis_cache.backends.base import BaseRedisCache
from redis_cache.compat import bytes_type, DEFAULT_TIMEOUT
class RedisCache(BaseRedisCache): class RedisCache(BaseRedisCache):
...@@ -26,7 +25,7 @@ class RedisCache(BaseRedisCache): ...@@ -26,7 +25,7 @@ class RedisCache(BaseRedisCache):
def get_client(self, key, write=False): def get_client(self, key, write=False):
if write and self.master_client is not None: if write and self.master_client is not None:
return self.master_client return self.master_client
return random.choice(self.client_list) return random.choice(list(self.client_list))
#################### ####################
# Django cache api # # Django cache api #
......
import sys import sys
import django import django
PY3 = (sys.version_info >= (3,)) PY3 = (sys.version_info >= (3,))
try: try:
...@@ -15,8 +16,11 @@ except ImportError: ...@@ -15,8 +16,11 @@ except ImportError:
if PY3: if PY3:
bytes_type = bytes bytes_type = bytes
from urllib.parse import parse_qs, urlparse
else: else:
bytes_type = str bytes_type = str
from urlparse import parse_qs, urlparse
if django.VERSION[:2] >= (1, 6): if django.VERSION[:2] >= (1, 6):
from django.core.cache.backends.base import DEFAULT_TIMEOUT as DJANGO_DEFAULT_TIMEOUT from django.core.cache.backends.base import DEFAULT_TIMEOUT as DJANGO_DEFAULT_TIMEOUT
......
...@@ -14,7 +14,7 @@ class CacheConnectionPool(object): ...@@ -14,7 +14,7 @@ class CacheConnectionPool(object):
return self._clients.get(server, None) return self._clients.get(server, None)
def reset(self): def reset(self):
for pool in self._connection_pools.itervalues(): for pool in self._connection_pools.values():
pool.disconnect() pool.disconnect()
self._clients = {} self._clients = {}
self._connection_pools = {} self._connection_pools = {}
......
from bisect import insort, bisect from bisect import insort, bisect
from hashlib import md5 import hashlib
from math import log from redis_cache.compat import smart_text
import sys
try:
maxint = sys.maxint
except AttributeError:
maxint = sys.maxsize
DIGITS = int(log(maxint) / log(16)) DIGITS = 8
def make_hash(s): def get_slot(s):
return int(md5(s.encode('utf-8')).hexdigest()[:DIGITS], 16) _hash = hashlib.md5(s.encode('utf-8')).hexdigest()
return int(_hash[-DIGITS:], 16)
class Node(object): class Node(object):
def __init__(self, node, i): def __init__(self, node, i):
self._node = node self._node = node
self._i = i self._i = i
self._position = make_hash("%d:%s" % (i, str(self._node))) key = "{0}:{1}".format(
smart_text(i),
smart_text(self._node),
)
self._position = get_slot(key)
def __cmp__(self, other): def __gt__(self, other):
if isinstance(other, int): if isinstance(other, int):
return cmp(self._position, other) return self._position > other
elif isinstance(other, Node): elif isinstance(other, Node):
return cmp(self._position, other._position) return self._position > other._position
raise TypeError('Cannot compare this class with "%s" type' % type(other)) raise TypeError(
'Cannot compare this class with "%s" type' % type(other)
def __eq__(self, other): )
return self._node == other._node
class HashRing(object): class HashRing(object):
...@@ -42,7 +42,7 @@ class HashRing(object): ...@@ -42,7 +42,7 @@ class HashRing(object):
insort(self._nodes, Node(node, i)) insort(self._nodes, Node(node, i))
def add(self, node, weight=1): def add(self, node, weight=1):
for i in xrange(weight * self.replicas): for i in range(weight * self.replicas):
self._add(node, i) self._add(node, i)
def remove(self, node): def remove(self, node):
...@@ -52,5 +52,5 @@ class HashRing(object): ...@@ -52,5 +52,5 @@ class HashRing(object):
del self._nodes[n - i - 1] del self._nodes[n - i - 1]
def get_node(self, key): def get_node(self, key):
i = bisect(self._nodes, make_hash(key)) - 1 i = bisect(self._nodes, get_slot(smart_text(key))) - 1
return self._nodes[i]._node return self._nodes[i]._node
from redis_cache.compat import smart_text, python_2_unicode_compatible import warnings
from django.core.exceptions import ImproperlyConfigured
from redis.connection import SSLConnection
from redis_cache.compat import (
smart_text, python_2_unicode_compatible, parse_qs, urlparse
)
try:
basestring
except NameError:
basestring = str
@python_2_unicode_compatible @python_2_unicode_compatible
...@@ -16,4 +28,141 @@ class CacheKey(object): ...@@ -16,4 +28,141 @@ class CacheKey(object):
def __unicode__(self): def __unicode__(self):
return smart_text(self._versioned_key) return smart_text(self._versioned_key)
def __hash__(self):
return hash(self._versioned_key)
__repr__ = __str__ = __unicode__ __repr__ = __str__ = __unicode__
def get_servers(location):
"""Returns a list of servers given the server argument passed in from
Django.
"""
if isinstance(location, basestring):
servers = location.split(',')
elif hasattr(location, '__iter__'):
servers = location
else:
raise ImproperlyConfigured(
'"server" must be an iterable or string'
)
return servers
def parse_connection_kwargs(server, db=None, **kwargs):
"""
Return a connection pool configured from the given URL.
For example::
redis://[:password]@localhost:6379/0
rediss://[:password]@localhost:6379/0
unix://[:password]@/path/to/socket.sock?db=0
Three URL schemes are supported:
redis:// creates a normal TCP socket connection
rediss:// creates a SSL wrapped TCP socket connection
unix:// creates a Unix Domain Socket connection
There are several ways to specify a database number. The parse function
will return the first specified option:
1. A ``db`` querystring option, e.g. redis://localhost?db=0
2. If using the redis:// scheme, the path argument of the url, e.g.
redis://localhost/0
3. The ``db`` argument to this function.
If none of these options are specified, db=0 is used.
Any additional querystring arguments and keyword arguments will be
passed along to the ConnectionPool class's initializer. In the case
of conflicting arguments, querystring arguments always win.
NOTE: taken from `redis.ConnectionPool.from_url` in redis-py
"""
if '://' in server:
url = server
url_string = url
url = urlparse(url)
qs = ''
# in python2.6, custom URL schemes don't recognize querystring values
# they're left as part of the url.path.
if '?' in url.path and not url.query:
# chop the querystring including the ? off the end of the url
# and reparse it.
qs = url.path.split('?', 1)[1]
url = urlparse(url_string[:-(len(qs) + 1)])
else:
qs = url.query
url_options = {}
for name, value in parse_qs(qs).items():
if value and len(value) > 0:
url_options[name] = value[0]
# We only support redis:// and unix:// schemes.
if url.scheme == 'unix':
url_options.update({
'password': url.password,
'unix_socket_path': url.path,
})
else:
url_options.update({
'host': url.hostname,
'port': int(url.port or 6379),
'password': url.password,
})
# If there's a path argument, use it as the db argument if a
# querystring value wasn't specified
if 'db' not in url_options and url.path:
try:
url_options['db'] = int(url.path.replace('/', ''))
except (AttributeError, ValueError):
pass
if url.scheme == 'rediss':
url_options['connection_class'] = SSLConnection
# last shot at the db value
url_options['db'] = int(url_options.get('db', db or 0))
# update the arguments from the URL values
kwargs.update(url_options)
# backwards compatability
if 'charset' in kwargs:
warnings.warn(DeprecationWarning(
'"charset" is deprecated. Use "encoding" instead'))
kwargs['encoding'] = kwargs.pop('charset')
if 'errors' in kwargs:
warnings.warn(DeprecationWarning(
'"errors" is deprecated. Use "encoding_errors" instead'))
kwargs['encoding_errors'] = kwargs.pop('errors')
else:
unix_socket_path = None
if ':' in server:
host, port = server.rsplit(':', 1)
try:
port = int(port)
except (ValueError, TypeError):
raise ImproperlyConfigured(
"{0} from {1} must be an integer".format(
repr(port),
server
)
)
else:
host, port = None, None
unix_socket_path = server
kwargs.update(
host=host,
port=port,
unix_socket_path=unix_socket_path,
db=db,
)
return kwargs
hiredis==0.2.0 hiredis==0.2.0
django-nose==1.4 django-nose==1.4
nose==1.3.6 nose==1.3.6
unittest2==1.0.1
...@@ -5,7 +5,7 @@ setup( ...@@ -5,7 +5,7 @@ setup(
url="http://github.com/sebleier/django-redis-cache/", url="http://github.com/sebleier/django-redis-cache/",
author="Sean Bleier", author="Sean Bleier",
author_email="sebleier@gmail.com", author_email="sebleier@gmail.com",
version="1.1.1", version="1.2.0",
packages=["redis_cache", "redis_cache.backends"], packages=["redis_cache", "redis_cache.backends"],
description="Redis Cache Backend for Django", description="Redis Cache Backend for Django",
install_requires=['redis>=2.4.5'], install_requires=['redis>=2.4.5'],
......
...@@ -32,3 +32,4 @@ CACHES = { ...@@ -32,3 +32,4 @@ CACHES = {
}, },
} }
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
MIDDLEWARE_CLASSES = tuple()
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from hashlib import sha1 from hashlib import sha1
import os
import subprocess
import sys
import time import time
import unittest
if sys.version_info < (2, 7):
import unittest2 as unittest
else:
import unittest
try: try:
import cPickle as pickle import cPickle as pickle
except ImportError: except ImportError:
import pickle import pickle
from django.core.cache import get_cache from django.core.cache import get_cache
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase from django.test import TestCase
...@@ -19,7 +30,11 @@ import redis ...@@ -19,7 +30,11 @@ import redis
from tests.testapp.models import Poll, expensive_calculation from tests.testapp.models import Poll, expensive_calculation
from redis_cache.cache import RedisCache, pool from redis_cache.cache import RedisCache, pool
from redis_cache.compat import DEFAULT_TIMEOUT from redis_cache.compat import DEFAULT_TIMEOUT, smart_bytes
from redis_cache.utils import get_servers, parse_connection_kwargs
REDIS_PASSWORD = 'yadayada'
LOCATION = "127.0.0.1:6381" LOCATION = "127.0.0.1:6381"
...@@ -35,15 +50,91 @@ class C: ...@@ -35,15 +50,91 @@ class C:
return 24 return 24
def start_redis_servers(servers, db=None, master=None):
"""Creates redis instances using specified locations from the settings.
Returns list of Popen objects
"""
processes = []
devnull = open(os.devnull, 'w')
master_connection_kwargs = master and parse_connection_kwargs(
master,
db=db,
password=REDIS_PASSWORD
)
for i, server in enumerate(servers):
connection_kwargs = parse_connection_kwargs(
server,
db=db,
password=REDIS_PASSWORD, # will be overridden if specified in `server`
)
parameters = dict(
port=connection_kwargs.get('port', 0),
requirepass=connection_kwargs['password'],
)
is_socket = server.startswith('unix://') or server.startswith('/')
if is_socket:
parameters.update(
port=0,
unixsocket='/tmp/redis{0}.sock'.format(i),
unixsocketperm=755,
)
if master and not connection_kwargs == master_connection_kwargs:
parameters.update(
masterauth=master_connection_kwargs['password'],
slaveof="{host} {port}".format(
host=master_connection_kwargs['host'],
port=master_connection_kwargs['port'],
)
)
args = ['./redis/src/redis-server'] + [
"--{parameter} {value}".format(parameter=parameter, value=value)
for parameter, value in parameters.items()
]
p = subprocess.Popen(args, stdout=devnull)
processes.append(p)
return processes
class SetupMixin(object): class SetupMixin(object):
processes = None
@classmethod
def tearDownClass(cls):
for p in cls.processes:
p.kill()
cls.processes = None
# Give redis processes some time to shutdown
# time.sleep(.1)
def setUp(self): def setUp(self):
# use DB 16 for testing and hope there isn't any important data :-> if self.__class__.processes is None:
from django.conf import settings
cache_settings = settings.CACHES['default']
servers = get_servers(cache_settings['LOCATION'])
options = cache_settings.get('OPTIONS', {})
db = options.get('db', 0)
master = options.get('MASTER_CACHE')
self.__class__.processes = start_redis_servers(
servers,
db=db,
master=master
)
# Give redis processes some time to startup
time.sleep(.1)
self.reset_pool() self.reset_pool()
self.cache = self.get_cache() self.cache = self.get_cache()
def tearDown(self): def tearDown(self):
# Sometimes it will be necessary to skip this method because we need to test default # Sometimes it will be necessary to skip this method because we need to
# initialization and that may be using a different port than the test redis server. # test default initialization and that may be using a different port
# than the test redis server.
if hasattr(self, '_skip_tearDown') and self._skip_tearDown: if hasattr(self, '_skip_tearDown') and self._skip_tearDown:
self._skip_tearDown = False self._skip_tearDown = False
return return
...@@ -226,10 +317,10 @@ class BaseRedisTestCase(SetupMixin): ...@@ -226,10 +317,10 @@ class BaseRedisTestCase(SetupMixin):
def test_unicode(self): def test_unicode(self):
# Unicode values can be cached # Unicode values can be cached
stuff = { stuff = {
u'ascii': u'ascii_value', 'ascii': 'ascii_value',
u'unicode_ascii': u'Iñtërnâtiônàlizætiøn1', 'unicode_ascii': 'Iñtërnâtiônàlizætiøn1',
u'Iñtërnâtiônàlizætiøn': u'Iñtërnâtiônàlizætiøn2', 'Iñtërnâtiônàlizætiøn': 'Iñtërnâtiônàlizætiøn2',
u'ascii': {u'x': 1} 'ascii': {'x': 1}
} }
for (key, value) in stuff.items(): for (key, value) in stuff.items():
self.cache.set(key, value) self.cache.set(key, value)
...@@ -373,7 +464,7 @@ class BaseRedisTestCase(SetupMixin): ...@@ -373,7 +464,7 @@ class BaseRedisTestCase(SetupMixin):
def test_reinsert_keys(self): def test_reinsert_keys(self):
self.cache._pickle_version = 0 self.cache._pickle_version = 0
for i in range(2000): for i in range(2000):
s = sha1(str(i)).hexdigest() s = sha1(smart_bytes(i)).hexdigest()
self.cache.set(s, self.cache) self.cache.set(s, self.cache)
self.cache._pickle_version = -1 self.cache._pickle_version = -1
self.cache.reinsert_keys() self.cache.reinsert_keys()
...@@ -414,7 +505,7 @@ class BaseRedisTestCase(SetupMixin): ...@@ -414,7 +505,7 @@ class BaseRedisTestCase(SetupMixin):
self.assertEqual(value, 42) self.assertEqual(value, 42)
def assertMaxConnection(self, cache, max_num): def assertMaxConnection(self, cache, max_num):
for client in cache.clients.itervalues(): for client in cache.clients.values():
self.assertLessEqual(client.connection_pool._created_connections, max_num) self.assertLessEqual(client.connection_pool._created_connections, max_num)
def test_max_connections(self): def test_max_connections(self):
...@@ -425,7 +516,7 @@ class BaseRedisTestCase(SetupMixin): ...@@ -425,7 +516,7 @@ class BaseRedisTestCase(SetupMixin):
pass pass
releases = {} releases = {}
for client in cache.clients.itervalues(): for client in cache.clients.values():
releases[client.connection_pool] = client.connection_pool.release releases[client.connection_pool] = client.connection_pool.release
client.connection_pool.release = noop client.connection_pool.release = noop
self.assertEqual(client.connection_pool.max_connections, 2) self.assertEqual(client.connection_pool.max_connections, 2)
...@@ -441,7 +532,7 @@ class BaseRedisTestCase(SetupMixin): ...@@ -441,7 +532,7 @@ class BaseRedisTestCase(SetupMixin):
self.assertMaxConnection(cache, 2) self.assertMaxConnection(cache, 2)
for client in cache.clients.itervalues(): for client in cache.clients.values():
client.connection_pool.release = releases[client.connection_pool] client.connection_pool.release = releases[client.connection_pool]
client.connection_pool.max_connections = 2 ** 31 client.connection_pool.max_connections = 2 ** 31
......
...@@ -11,11 +11,11 @@ from redis_cache.connection import pool ...@@ -11,11 +11,11 @@ from redis_cache.connection import pool
from tests.testapp.tests.base_tests import SetupMixin from tests.testapp.tests.base_tests import SetupMixin
MASTER_LOCATION = "127.0.0.1:6387" MASTER_LOCATION = "127.0.0.2:6387"
LOCATIONS = [ LOCATIONS = [
'127.0.0.1:6387', '127.0.0.2:6387',
'127.0.0.1:6388', '127.0.0.2:6388',
'127.0.0.1:6389', '127.0.0.2:6389',
] ]
...@@ -43,22 +43,22 @@ class MasterSlaveTestCase(SetupMixin, TestCase): ...@@ -43,22 +43,22 @@ class MasterSlaveTestCase(SetupMixin, TestCase):
client = cache.master_client client = cache.master_client
self.assertEqual( self.assertEqual(
client.connection_pool.connection_identifier, client.connection_pool.connection_identifier,
('127.0.0.1', 6387, 1, None) ('127.0.0.2', 6387, 1, None)
) )
self.assertEqual(len(pool._connection_pools), 3) self.assertEqual(len(pool._connection_pools), 3)
def test_set(self): def test_set(self):
cache = self.get_cache() cache = self.get_cache()
cache.set('a', 'a') cache.set('a', 'a')
time.sleep(.5) time.sleep(.2)
for client in self.cache.clients.itervalues(): for client in self.cache.clients.values():
key = cache.make_key('a') key = cache.make_key('a')
self.assertIsNotNone(client.get(key)) self.assertIsNotNone(client.get(key))
def test_set_many(self): def test_set_many(self):
cache = self.get_cache() cache = self.get_cache()
cache.set_many({'a': 'a', 'b': 'b'}) cache.set_many({'a': 'a', 'b': 'b'})
for client in self.cache.clients.itervalues(): for client in self.cache.clients.values():
self.assertNotIn(None, client.mget([ self.assertNotIn(None, client.mget([
cache.make_key('a'), cache.make_key('a'),
cache.make_key('b'), cache.make_key('b'),
...@@ -68,24 +68,28 @@ class MasterSlaveTestCase(SetupMixin, TestCase): ...@@ -68,24 +68,28 @@ class MasterSlaveTestCase(SetupMixin, TestCase):
cache = self.get_cache() cache = self.get_cache()
cache.set('a', 0) cache.set('a', 0)
cache.incr('a') cache.incr('a')
time.sleep(.5) time.sleep(.2)
key = cache.make_key('a') key = cache.make_key('a')
for client in self.cache.clients.itervalues(): for client in self.cache.clients.values():
self.assertEqual(client.get(key), '1') self.assertEqual(int(client.get(key)), 1)
def test_delete(self): def test_delete(self):
cache = self.get_cache() cache = self.get_cache()
cache.set('a', 'a') cache.set('a', 'a')
time.sleep(.2)
self.assertEqual(cache.get('a'), 'a') self.assertEqual(cache.get('a'), 'a')
cache.delete('a') cache.delete('a')
time.sleep(.2)
key = cache.make_key('a') key = cache.make_key('a')
for client in self.cache.clients.itervalues(): for client in self.cache.clients.values():
self.assertIsNone(client.get(key)) self.assertIsNone(client.get(key))
def test_clear(self): def test_clear(self):
cache = self.get_cache() cache = self.get_cache()
cache.set('a', 'a') cache.set('a', 'a')
time.sleep(.2)
self.assertEqual(cache.get('a'), 'a') self.assertEqual(cache.get('a'), 'a')
cache.clear() cache.clear()
for client in self.cache.clients.itervalues(): time.sleep(.2)
for client in self.cache.clients.values():
self.assertEqual(len(client.keys('*')), 0) self.assertEqual(len(client.keys('*')), 0)
from collections import Counter
from math import sqrt from math import sqrt
from redis_cache.sharder import HashRing
def mean(lst): def mean(lst):
...@@ -14,15 +16,48 @@ def stddev(lst): ...@@ -14,15 +16,48 @@ def stddev(lst):
class MultiServerTests(object): class MultiServerTests(object):
def test_distribution(self):
nodes = [node._position for node in self.cache.sharder._nodes]
nodes.sort()
diffs = [(b - a) for a, b in zip(nodes[:-1], nodes[1:])]
l = 16 ** 8
perfect_dist = l / len(nodes)
random_dist = sum(diffs) / len(diffs)
_max = max([perfect_dist, random_dist])
_min = min([perfect_dist, random_dist])
percentage = (1 - _max / _min) * 100
# Assert they are less than 2 percent of each other
self.assertLess(percentage, 2.0)
def test_make_key_distribution(self):
ring = HashRing()
nodes = set([str(node._node) for node in self.cache.sharder._nodes])
nodes = [
('127.0.0.1', 6379, 15, '/tmp/redis0.sock'),
('127.0.0.1', 6379, 15, '/tmp/redis1.sock'),
('127.0.0.1', 6379, 15, '/tmp/redis2.sock'),
]
for node in nodes:
ring.add(str(node))
n = 50000
counter = Counter(
[ring.get_node(str(i)) for i in range(n)]
)
self.assertLess(
((stddev(counter.values()) / n) * 100.0), 10, counter.values()
)
def test_key_distribution(self): def test_key_distribution(self):
n = 10000 n = 10000
self.cache.set('a', 'a') for i in range(n):
for i in xrange(n):
self.cache.set(i, i) self.cache.set(i, i)
keys = [ keys = [
len(client.keys('*')) len(client.keys('*'))
for client in self.cache.clients.itervalues() for client in self.cache.clients.values()
] ]
self.assertEqual(sum(keys), n)
self.assertLess(((stddev(keys) / n) * 100.0), 10) self.assertLess(((stddev(keys) / n) * 100.0), 10)
def test_removing_nodes(self): def test_removing_nodes(self):
......
# # -*- coding: utf-8 -*- # # -*- coding: utf-8 -*-
from collections import Counter
from tests.testapp.tests.base_tests import BaseRedisTestCase from tests.testapp.tests.base_tests import BaseRedisTestCase
from tests.testapp.tests.multi_server_tests import MultiServerTests from tests.testapp.tests.multi_server_tests import MultiServerTests
...@@ -8,15 +9,12 @@ except ImportError: ...@@ -8,15 +9,12 @@ except ImportError:
from django.test.utils import override_settings from django.test.utils import override_settings
from django.test import TestCase from django.test import TestCase
from redis_cache.cache import ImproperlyConfigured
from redis.connection import UnixDomainSocketConnection
LOCATION = "unix://:yadayada@/tmp/redis0.sock?db=15"
LOCATION = "unix://:yadayada@/tmp/redis4.sock?db=15"
LOCATIONS = [ LOCATIONS = [
"unix://:yadayada@/tmp/redis4.sock?db=15", "unix://:yadayada@/tmp/redis0.sock?db=15",
"unix://:yadayada@/tmp/redis5.sock?db=15", "unix://:yadayada@/tmp/redis1.sock?db=15",
"unix://:yadayada@/tmp/redis6.sock?db=15", "unix://:yadayada@/tmp/redis2.sock?db=15",
] ]
...@@ -87,7 +85,16 @@ class SinglePythonParserTestCase(SocketTestCase): ...@@ -87,7 +85,16 @@ class SinglePythonParserTestCase(SocketTestCase):
} }
) )
class MultipleHiredisTestCase(MultiServerTests, SocketTestCase): class MultipleHiredisTestCase(MultiServerTests, SocketTestCase):
pass
def test_equal_number_of_nodes(self):
counter = Counter(
[node._node[3] for node in self.cache.sharder._nodes]
)
self.assertEqual(counter, {
'/tmp/redis0.sock': 16,
'/tmp/redis1.sock': 16,
'/tmp/redis2.sock': 16,
})
@override_settings( @override_settings(
......
...@@ -24,6 +24,10 @@ class TCPTestCase(BaseRedisTestCase, TestCase): ...@@ -24,6 +24,10 @@ class TCPTestCase(BaseRedisTestCase, TestCase):
def test_default_initialization(self): def test_default_initialization(self):
self.reset_pool() self.reset_pool()
self.cache = self.get_cache() self.cache = self.get_cache()
self.assertIn(
('127.0.0.1', 6381, 15, None),
self.cache.clients,
)
client = self.cache.clients[('127.0.0.1', 6381, 15, None)] client = self.cache.clients[('127.0.0.1', 6381, 15, None)]
connection_class = client.connection_pool.connection_class connection_class = client.connection_pool.connection_class
if connection_class is not UnixDomainSocketConnection: if connection_class is not UnixDomainSocketConnection:
...@@ -119,5 +123,3 @@ class MultipleHiredisTestCase(MultiServerTests, TCPTestCase): ...@@ -119,5 +123,3 @@ class MultipleHiredisTestCase(MultiServerTests, TCPTestCase):
) )
class MultiplePythonParserTestCase(MultiServerTests, TCPTestCase): class MultiplePythonParserTestCase(MultiServerTests, TCPTestCase):
pass pass
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment