diff --git a/docs/advanced_configuration.rst b/docs/advanced_configuration.rst index 3b0547ca..538335f2 100644 --- a/docs/advanced_configuration.rst +++ b/docs/advanced_configuration.rst @@ -135,7 +135,52 @@ read/write server and secondary servers as read-only. } } +Additionally if you set ``MASTER_CACHE_ONLY_FOR_WRITE`` to True the primary +server will be write-only. +**For example:** + +Two data centers - DC1 and DC2. Redis master is in DC1, slave in DC2. With +MASTER_CACHE_ONLY_FOR_WRITE option it's possible to configure application +instances to write to the master and read from the closer data center. + +DC1 configuration: + +.. code:: python + + CACHES = { + 'default': { + 'LOCATION': [ + '10.10.0.1:6379', # Master, DC1 + ], + 'OPTIONS': { + 'PASSWORD': 'yadayada', + 'MASTER_CACHE': '10.10.0.1:6379', + ... + }, + ... + } + } + +DC2 configuration: + +.. code:: python + + CACHES = { + 'default': { + 'LOCATION': [ + '10.10.0.1:6379', # Master, DC1 + '10.20.0.1:6379', # Slave, DC2 + ], + 'OPTIONS': { + 'PASSWORD': 'yadayada', + 'MASTER_CACHE': '10.10.0.1:6379', + 'MASTER_CACHE_ONLY_FOR_WRITE': True, + ... + }, + ... + } + } Pluggable Parser Classes diff --git a/install_redis.sh b/install_redis.sh index 52dac6c1..04934110 100755 --- a/install_redis.sh +++ b/install_redis.sh @@ -1,6 +1,6 @@ #!/bin/bash -: ${REDIS_VERSION:="2.6"} +: ${REDIS_VERSION:="2.8"} test -d redis || git clone https://github.com/antirez/redis git -C redis checkout $REDIS_VERSION diff --git a/redis_cache/backends/single.py b/redis_cache/backends/single.py index f13aec74..6b17b336 100644 --- a/redis_cache/backends/single.py +++ b/redis_cache/backends/single.py @@ -23,10 +23,17 @@ def __init__(self, server, params): self.client_list = self.clients.values() self.master_client = self.get_master_client() + self.ro_slaves_list = [] + if self.options.get('MASTER_CACHE_ONLY_FOR_WRITE', False): + for identifier, client in self.clients.items(): + if identifier != self.master_client.connection_pool.connection_identifier: # noqa: E501 + self.ro_slaves_list.append(client) def get_client(self, key, write=False): if write and self.master_client is not None: return self.master_client + if self.ro_slaves_list: + return random.choice(self.ro_slaves_list) return random.choice(list(self.client_list)) #################### diff --git a/tests/testapp/tests/master_slave_tests.py b/tests/testapp/tests/master_slave_tests.py index acba0a9e..b49eef77 100644 --- a/tests/testapp/tests/master_slave_tests.py +++ b/tests/testapp/tests/master_slave_tests.py @@ -16,24 +16,7 @@ ] -@override_settings(CACHES={ - 'default': { - 'BACKEND': 'redis_cache.RedisCache', - 'LOCATION': LOCATIONS, - 'OPTIONS': { - 'DB': 1, - 'PASSWORD': 'yadayada', - 'PARSER_CLASS': 'redis.connection.HiredisParser', - 'PICKLE_VERSION': -1, - 'MASTER_CACHE': MASTER_LOCATION, - }, - }, -}) -class MasterSlaveTestCase(SetupMixin, TestCase): - - def setUp(self): - super(MasterSlaveTestCase, self).setUp() - pool.reset() +class BaseMasterSlaveTestMixin(SetupMixin): def test_master_client(self): # Reset the caches at the beginning of the test. @@ -93,3 +76,59 @@ def test_clear(self): time.sleep(.2) for client in self.cache.clients.values(): self.assertEqual(len(client.keys('*')), 0) + + +@override_settings(CACHES={ + 'default': { + 'BACKEND': 'redis_cache.RedisCache', + 'LOCATION': LOCATIONS, + 'OPTIONS': { + 'DB': 1, + 'PASSWORD': 'yadayada', + 'PARSER_CLASS': 'redis.connection.HiredisParser', + 'PICKLE_VERSION': -1, + 'MASTER_CACHE': MASTER_LOCATION, + }, + }, +}) +class MasterSlaveTestCase(BaseMasterSlaveTestMixin, TestCase): + + def setUp(self): + super(MasterSlaveTestCase, self).setUp() + pool.reset() + + +@override_settings(CACHES={ + 'default': { + 'BACKEND': 'redis_cache.RedisCache', + 'LOCATION': LOCATIONS, + 'OPTIONS': { + 'DB': 1, + 'PASSWORD': 'yadayada', + 'PARSER_CLASS': 'redis.connection.HiredisParser', + 'PICKLE_VERSION': -1, + 'MASTER_CACHE': MASTER_LOCATION, + 'MASTER_CACHE_ONLY_FOR_WRITE': True, + }, + }, +}) +class MasterCacheOnlyForWriteTestCase(BaseMasterSlaveTestMixin, TestCase): + + def setUp(self): + super(MasterCacheOnlyForWriteTestCase, self).setUp() + pool.reset() + + def test_master_client_only_for_write(self): + caches._caches.caches = {} + + cache = self.get_cache() + master_client = cache.master_client + slaves = [] + for slave in cache.ro_slaves_list: + slaves.append( + slave.connection_pool.connection_identifier + ) + self.assertTrue( + master_client.connection_pool.connection_identifier not in slaves + ) + self.assertEqual(len(slaves), 2)