Skip to content
This repository was archived by the owner on Dec 8, 2017. It is now read-only.

Commit e0d60de

Browse files
committed
initial commit
1 parent ec3e26a commit e0d60de

File tree

11 files changed

+310
-5
lines changed

11 files changed

+310
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ nosetests.xml
3434
.mr.developer.cfg
3535
.project
3636
.pydevproject
37+
.idea

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2014 gusdan
3+
Copyright (c) 2014 Danil Gusev
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy of
66
this software and associated documentation files (the "Software"), to deal in

README.md

Lines changed: 0 additions & 4 deletions
This file was deleted.

README.rst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
Amazon ElastiCache backend for Django
2+
=====================================
3+
4+
Simple Django cache backend for Amazon ElastiCache (memcached based). It uses
5+
`pylibmc <http://github.com/lericson/pylibmc>`_ and setup connection to each
6+
node in cluster using
7+
`Auto Discovery <http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html>`_
8+
function.
9+
10+
11+
Requirements
12+
------------
13+
14+
* pylibmc
15+
* Django 1.3+.
16+
17+
It was written and tested on Python 2.7.
18+
19+
Installation
20+
------------
21+
22+
Get it from `pypi <http://pypi.python.org/pypi/django-elasticache>`_::
23+
24+
pip install django-pylibmc
25+
26+
or `github <http://github.com/gusdan/django-elasticache>`_::
27+
28+
pip install -e git://github.com/gusdan/django-elasticache.git#egg=django-elasticache
29+
30+
31+
Usage
32+
-----
33+
34+
Your cache backend should look something like this::
35+
36+
CACHES = {
37+
'default': {
38+
'BACKEND': 'django_elasticache.memcached.ElasctiCache',
39+
'LOCATION': 'cache-c.drtgf.cfg.use1.cache.amazonaws.com:11211',
40+
}
41+
}
42+
43+
By the first call to cache it connect to cluster (using LOCATION),
44+
get list of all nodes and setup pylibmc client using full
45+
list of nodes. As result your cache will work with all nodes and
46+
automatically detect new nodes in cluster. List of nodes are stored in class-level
47+
cached, so any changes in cluster take affect only after restart working process.
48+
But if you're using gunicorn or mod_wsgi you usually have max_request settings which
49+
restart process after some count of processed requests.
50+
51+
Django-elascticache changes default pylibmc params to increase performance.
52+
53+
54+
Testing
55+
-------
56+
57+
Run the tests like this::
58+
59+
nosetest

django_elasticache/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
VERSION = (0, 0, 1)
2+
__version__ = '.'.join(map(str, VERSION))

django_elasticache/cluster_utils.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
utils for discovery cluster
3+
"""
4+
from distutils.version import StrictVersion
5+
from telnetlib import Telnet
6+
7+
8+
class WrongProtocolData(ValueError):
9+
"""
10+
Exception for raising when we get something unexpected
11+
in telnet protocol
12+
"""
13+
def __init__(self, cmd, response):
14+
super(WrongProtocolData, self).__init__(
15+
'Unexpected response {} for command {}'.format(response, cmd))
16+
17+
18+
def get_cluster_info(host, port):
19+
"""
20+
return dict with info about nodes in cluster and current version
21+
{
22+
'nodes': [
23+
'IP:port',
24+
'IP:port',
25+
],
26+
'version': '1.4.4'
27+
}
28+
"""
29+
client = Telnet(host, port)
30+
client.write('version\n')
31+
res = client.read_until('\r\n').strip()
32+
version_list = res.split(' ')
33+
if len(version_list) != 2 or version_list[0] != 'VERSION':
34+
raise WrongProtocolData('version', res)
35+
version = version_list[1]
36+
if StrictVersion(version) >= StrictVersion('1.4.14'):
37+
cmd = 'config get cluster\n'
38+
else:
39+
cmd = 'get AmazonElastiCache:cluster\n'
40+
client.write(cmd)
41+
res = client.read_until('\r\n').strip()
42+
try:
43+
version = int(res)
44+
except ValueError:
45+
raise WrongProtocolData(cmd, res)
46+
res = client.read_until('\r\n').strip()
47+
client.close()
48+
nodes = []
49+
try:
50+
for node in res.split(' '):
51+
host, ip, port = node.split('|')
52+
nodes.append('{}:{}'.format(ip or host, port))
53+
except ValueError:
54+
raise WrongProtocolData(cmd, res)
55+
return {
56+
'version': version,
57+
'nodes': nodes
58+
}

django_elasticache/memcached.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Backend for django cache
3+
"""
4+
from django.core.cache import InvalidCacheBackendError
5+
from django.core.cache.backends.memcached import PyLibMCCache
6+
from django.utils.functional import cached_property
7+
from .cluster_utils import get_cluster_info
8+
9+
10+
class ElastiCache(PyLibMCCache):
11+
"""
12+
backend for Amazon ElastiCache (memcached) with auto discovery mode
13+
it used pylibmc in binary mode
14+
"""
15+
def __init__(self, server, params):
16+
self.update_params(params)
17+
super(ElastiCache, self).__init__(server, params)
18+
if len(self._servers) > 1:
19+
raise InvalidCacheBackendError(
20+
'ElastiCache should be configured with only one server '
21+
'(Configuration Endpoint)')
22+
if len(self._servers[0].split(':')) != 2:
23+
raise InvalidCacheBackendError(
24+
'Server configuration should be in format IP:port')
25+
26+
def update_params(self, params):
27+
"""
28+
update connection params to maximize performance
29+
"""
30+
if not params.get('BINARY', True):
31+
raise Warning('To increase performance please use ElastiCache'
32+
' in binary mode')
33+
else:
34+
params['BINARY'] = True # patch params, set binary mode
35+
if not 'OPTIONS' in params:
36+
# set special 'behaviors' pylibmc attributes
37+
params['OPTIONS'] = {
38+
'tcp_nodelay': True,
39+
'ketama': True
40+
}
41+
42+
@cached_property
43+
def get_cluster_nodes(self):
44+
"""
45+
return list with all nodes in cluster
46+
"""
47+
server, port = self._servers[0].split(':')
48+
return get_cluster_info(server, port)['nodes']
49+
50+
@property
51+
def _cache(self):
52+
# PylibMC uses cache options as the 'behaviors' attribute.
53+
# It also needs to use threadlocals, because some versions of
54+
# PylibMC don't play well with the GIL.
55+
client = getattr(self._local, 'client', None)
56+
if client:
57+
return client
58+
59+
client = self._lib.Client(self.get_cluster_nodes)
60+
if self._options:
61+
client.behaviors = self._options
62+
63+
self._local.client = client
64+
65+
return client

setup.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from setuptools import setup
2+
3+
import django_elasticache
4+
5+
6+
setup(
7+
name='django-elasticache',
8+
version=django_elasticache.__version__,
9+
description='Django cache backend for Amazon ElastiCache memcached',
10+
long_description=open('README.rst').read(),
11+
author='Danil Gusev',
12+
author_email='[email protected]',
13+
url='http://github.com/gusdan/django-elasticache',
14+
license='MIT',
15+
keywords='elasticache amazon cache pylibmc memcached aws',
16+
packages=['django_elasticache'],
17+
install_requires=['pylibmc', 'Django>=1.3'],
18+
classifiers=[
19+
'Development Status :: 4 - Beta',
20+
'Environment :: Web Environment',
21+
'Environment :: Web Environment :: Mozilla',
22+
'Framework :: Django',
23+
'Intended Audience :: Developers',
24+
'License :: OSI Approved :: BSD License',
25+
'Operating System :: OS Independent',
26+
'Programming Language :: Python',
27+
'Topic :: Software Development :: Libraries :: Python Modules',
28+
],
29+
)

tests/__init__.py

Whitespace-only changes.

tests/test_backend.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from django.conf import global_settings
2+
from mock import patch, Mock
3+
from nose.tools import eq_, raises
4+
5+
6+
@patch('django.conf.settings', global_settings)
7+
def test_patch_params():
8+
from django_elasticache.memcached import ElastiCache
9+
params = {}
10+
ElastiCache('qew:12', params)
11+
eq_(params['BINARY'], True)
12+
eq_(params['OPTIONS']['tcp_nodelay'], True)
13+
eq_(params['OPTIONS']['ketama'], True)
14+
15+
16+
@raises(Exception)
17+
@patch('django.conf.settings', global_settings)
18+
def test_wrong_params():
19+
from django_elasticache.memcached import ElastiCache
20+
ElastiCache('qew', {})
21+
22+
23+
@raises(Warning)
24+
@patch('django.conf.settings', global_settings)
25+
def test_wrong_params_warning():
26+
from django_elasticache.memcached import ElastiCache
27+
ElastiCache('qew', {'BINARY': False})
28+
29+
30+
@patch('django.conf.settings', global_settings)
31+
@patch('django_elasticache.memcached.get_cluster_info')
32+
def test_split_servers(get_cluster_info):
33+
from django_elasticache.memcached import ElastiCache
34+
backend = ElastiCache('h:0', {})
35+
servers = ['h1:p', 'h2:p']
36+
get_cluster_info.return_value = {
37+
'nodes': servers
38+
}
39+
backend._lib.Client = Mock()
40+
assert backend._cache
41+
get_cluster_info.assert_called_once_with('h', '0')
42+
backend._lib.Client.assert_called_once_with(servers)

tests/test_protocol.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from mock import patch, call
2+
from django_elasticache.cluster_utils import (
3+
get_cluster_info, WrongProtocolData)
4+
from nose.tools import eq_, raises
5+
6+
TEST_PROTOCOL_1 = [
7+
'VERSION 1.4.14',
8+
'1',
9+
'hostname|ip-address|port hostname||port'
10+
]
11+
12+
TEST_PROTOCOL_2 = [
13+
'VERSION 1.4.13',
14+
'1',
15+
'hostname|ip-address|port hostname||port'
16+
]
17+
18+
19+
@patch('django_elasticache.cluster_utils.Telnet')
20+
def test_happy_path(Telnet):
21+
client = Telnet.return_value
22+
client.read_until.side_effect = TEST_PROTOCOL_1
23+
info = get_cluster_info('', 0)
24+
eq_(info['version'], 1)
25+
eq_(info['nodes'], ['ip-address:port', 'hostname:port'])
26+
27+
28+
@raises(WrongProtocolData)
29+
@patch('django_elasticache.cluster_utils.Telnet')
30+
def test_bad_protocol(Telnet):
31+
get_cluster_info('', 0)
32+
33+
34+
@patch('django_elasticache.cluster_utils.Telnet')
35+
def test_last_versions(Telnet):
36+
client = Telnet.return_value
37+
client.read_until.side_effect = TEST_PROTOCOL_1
38+
get_cluster_info('', 0)
39+
client.write.assert_has_calls([
40+
call('version\n'),
41+
call('config get cluster\n'),
42+
])
43+
44+
45+
@patch('django_elasticache.cluster_utils.Telnet')
46+
def test_prev_versions(Telnet):
47+
client = Telnet.return_value
48+
client.read_until.side_effect = TEST_PROTOCOL_2
49+
get_cluster_info('', 0)
50+
client.write.assert_has_calls([
51+
call('version\n'),
52+
call('get AmazonElastiCache:cluster\n'),
53+
])

0 commit comments

Comments
 (0)