Code

Ticket #13795: cache_key_prefix_13357.diff

File cache_key_prefix_13357.diff, 16.4 KB (added by bruth, 4 years ago)
Line 
1Index: django/conf/global_settings.py
2===================================================================
3--- django/conf/global_settings.py      (revision 13357)
4+++ django/conf/global_settings.py      (working copy)
5@@ -427,6 +427,7 @@
6 # The cache backend to use.  See the docstring in django.core.cache for the
7 # possible values.
8 CACHE_BACKEND = 'locmem://'
9+CACHE_KEY_PREFIX = ''
10 CACHE_MIDDLEWARE_KEY_PREFIX = ''
11 CACHE_MIDDLEWARE_SECONDS = 600
12 
13Index: django/core/cache/__init__.py
14===================================================================
15--- django/core/cache/__init__.py       (revision 13357)
16+++ django/core/cache/__init__.py       (working copy)
17@@ -56,16 +56,16 @@
18 
19     return scheme, host, params
20 
21-def get_cache(backend_uri):
22+def get_cache(backend_uri, key_prefix=''):
23     scheme, host, params = parse_backend_uri(backend_uri)
24     if scheme in BACKENDS:
25         name = 'django.core.cache.backends.%s' % BACKENDS[scheme]
26     else:
27         name = scheme
28     module = importlib.import_module(name)
29-    return getattr(module, 'CacheClass')(host, params)
30+    return getattr(module, 'CacheClass')(host, params, key_prefix)
31 
32-cache = get_cache(settings.CACHE_BACKEND)
33+cache = get_cache(settings.CACHE_BACKEND, settings.CACHE_KEY_PREFIX)
34 
35 # Some caches -- pythont-memcached in particular -- need to do a cleanup at the
36 # end of a request cycle. If the cache provides a close() method, wire it up
37Index: django/core/cache/backends/base.py
38===================================================================
39--- django/core/cache/backends/base.py  (revision 13357)
40+++ django/core/cache/backends/base.py  (working copy)
41@@ -1,19 +1,25 @@
42 "Base Cache class."
43 
44 from django.core.exceptions import ImproperlyConfigured
45+from django.utils.encoding import smart_str
46 
47 class InvalidCacheBackendError(ImproperlyConfigured):
48     pass
49 
50 class BaseCache(object):
51-    def __init__(self, params):
52+    def __init__(self, params, key_prefix=''):
53         timeout = params.get('timeout', 300)
54         try:
55             timeout = int(timeout)
56         except (ValueError, TypeError):
57             timeout = 300
58         self.default_timeout = timeout
59+        self.key_prefix = smart_str(key_prefix)
60 
61+    def _key(self, key):
62+        "Returns the key prepended with the `cache_prefix'."
63+        return self.key_prefix + smart_str(key)
64+
65     def add(self, key, value, timeout=None):
66         """
67         Set a value in the cache if the key does not already exist. If
68Index: django/core/cache/backends/locmem.py
69===================================================================
70--- django/core/cache/backends/locmem.py        (revision 13357)
71+++ django/core/cache/backends/locmem.py        (working copy)
72@@ -10,8 +10,8 @@
73 from django.utils.synch import RWLock
74 
75 class CacheClass(BaseCache):
76-    def __init__(self, _, params):
77-        BaseCache.__init__(self, params)
78+    def __init__(self, _, params, key_prefix):
79+        BaseCache.__init__(self, params, key_prefix)
80         self._cache = {}
81         self._expire_info = {}
82 
83@@ -30,6 +30,7 @@
84         self._lock = RWLock()
85 
86     def add(self, key, value, timeout=None):
87+        key = self._key(key)
88         self._lock.writer_enters()
89         try:
90             exp = self._expire_info.get(key)
91@@ -44,6 +45,7 @@
92             self._lock.writer_leaves()
93 
94     def get(self, key, default=None):
95+        key = self._key(key)
96         self._lock.reader_enters()
97         try:
98             exp = self._expire_info.get(key)
99@@ -76,6 +78,7 @@
100         self._expire_info[key] = time.time() + timeout
101 
102     def set(self, key, value, timeout=None):
103+        key = self._key(key)
104         self._lock.writer_enters()
105         # Python 2.4 doesn't allow combined try-except-finally blocks.
106         try:
107@@ -87,6 +90,7 @@
108             self._lock.writer_leaves()
109 
110     def has_key(self, key):
111+        key = self._key(key)
112         self._lock.reader_enters()
113         try:
114             exp = self._expire_info.get(key)
115@@ -127,6 +131,7 @@
116             pass
117 
118     def delete(self, key):
119+        key = self._key(key)
120         self._lock.writer_enters()
121         try:
122             self._delete(key)
123Index: django/core/cache/backends/filebased.py
124===================================================================
125--- django/core/cache/backends/filebased.py     (revision 13357)
126+++ django/core/cache/backends/filebased.py     (working copy)
127@@ -12,8 +12,8 @@
128 from django.utils.hashcompat import md5_constructor
129 
130 class CacheClass(BaseCache):
131-    def __init__(self, dir, params):
132-        BaseCache.__init__(self, params)
133+    def __init__(self, dir, params, key_prefix):
134+        BaseCache.__init__(self, params, key_prefix)
135 
136         max_entries = params.get('max_entries', 300)
137         try:
138@@ -32,6 +32,7 @@
139             self._createdir()
140 
141     def add(self, key, value, timeout=None):
142+        key = self._key(key)
143         if self.has_key(key):
144             return False
145 
146@@ -39,6 +40,7 @@
147         return True
148 
149     def get(self, key, default=None):
150+        key = self._key(key)
151         fname = self._key_to_file(key)
152         try:
153             f = open(fname, 'rb')
154@@ -56,6 +58,7 @@
155         return default
156 
157     def set(self, key, value, timeout=None):
158+        key = self._key(key)
159         fname = self._key_to_file(key)
160         dirname = os.path.dirname(fname)
161 
162@@ -79,6 +82,7 @@
163             pass
164 
165     def delete(self, key):
166+        key = self._key(key)
167         try:
168             self._delete(self._key_to_file(key))
169         except (IOError, OSError):
170@@ -95,6 +99,7 @@
171             pass
172 
173     def has_key(self, key):
174+        key = self._key(key)
175         fname = self._key_to_file(key)
176         try:
177             f = open(fname, 'rb')
178@@ -148,7 +153,8 @@
179         Thus, a cache key of "foo" gets turnned into a file named
180         ``{cache-dir}ac/bd/18db4cc2f85cedef654fccc4a4d8``.
181         """
182-        path = md5_constructor(key.encode('utf-8')).hexdigest()
183+        key = self._key(key)
184+        path = md5_constructor(key).hexdigest()
185         path = os.path.join(path[:2], path[2:4], path[4:])
186         return os.path.join(self._dir, path)
187 
188Index: django/core/cache/backends/db.py
189===================================================================
190--- django/core/cache/backends/db.py    (revision 13357)
191+++ django/core/cache/backends/db.py    (working copy)
192@@ -10,8 +10,8 @@
193     import pickle
194 
195 class CacheClass(BaseCache):
196-    def __init__(self, table, params):
197-        BaseCache.__init__(self, params)
198+    def __init__(self, table, params, key_prefix):
199+        BaseCache.__init__(self, params, key_prefix)
200         self._table = connection.ops.quote_name(table)
201         max_entries = params.get('max_entries', 300)
202         try:
203@@ -25,6 +25,7 @@
204             self._cull_frequency = 3
205 
206     def get(self, key, default=None):
207+        key = self._key(key)
208         cursor = connection.cursor()
209         cursor.execute("SELECT cache_key, value, expires FROM %s WHERE cache_key = %%s" % self._table, [key])
210         row = cursor.fetchone()
211@@ -45,6 +46,7 @@
212         return self._base_set('add', key, value, timeout)
213 
214     def _base_set(self, mode, key, value, timeout=None):
215+        key = self._key(key)
216         if timeout is None:
217             timeout = self.default_timeout
218         cursor = connection.cursor()
219@@ -74,11 +76,13 @@
220             return True
221 
222     def delete(self, key):
223+        key = self._key(key)
224         cursor = connection.cursor()
225         cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key])
226         transaction.commit_unless_managed()
227 
228     def has_key(self, key):
229+        key = self._key(key)
230         now = datetime.now().replace(microsecond=0)
231         cursor = connection.cursor()
232         cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s and expires > %%s" % self._table,
233Index: django/core/cache/backends/memcached.py
234===================================================================
235--- django/core/cache/backends/memcached.py     (revision 13357)
236+++ django/core/cache/backends/memcached.py     (working copy)
237@@ -3,7 +3,6 @@
238 import time
239 
240 from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError
241-from django.utils.encoding import smart_unicode, smart_str
242 
243 try:
244     import cmemcache as memcache
245@@ -19,8 +18,8 @@
246         raise InvalidCacheBackendError("Memcached cache backend requires either the 'memcache' or 'cmemcache' library")
247 
248 class CacheClass(BaseCache):
249-    def __init__(self, server, params):
250-        BaseCache.__init__(self, params)
251+    def __init__(self, server, params, key_prefix):
252+        BaseCache.__init__(self, params, key_prefix)
253         self._cache = memcache.Client(server.split(';'))
254 
255     def _get_memcache_timeout(self, timeout):
256@@ -40,29 +39,35 @@
257         return timeout
258 
259     def add(self, key, value, timeout=0):
260+        key = self._key(key)
261         if isinstance(value, unicode):
262             value = value.encode('utf-8')
263-        return self._cache.add(smart_str(key), value, self._get_memcache_timeout(timeout))
264+        return self._cache.add(key, value, self._get_memcache_timeout(timeout))
265 
266     def get(self, key, default=None):
267-        val = self._cache.get(smart_str(key))
268+        key = self._key(key)
269+        val = self._cache.get(key)
270         if val is None:
271             return default
272         return val
273 
274     def set(self, key, value, timeout=0):
275-        self._cache.set(smart_str(key), value, self._get_memcache_timeout(timeout))
276+        key = self._key(key)
277+        self._cache.set(key, value, self._get_memcache_timeout(timeout))
278 
279     def delete(self, key):
280-        self._cache.delete(smart_str(key))
281+        key = self._key(key)
282+        self._cache.delete(key)
283 
284     def get_many(self, keys):
285-        return self._cache.get_multi(map(smart_str,keys))
286+        keys = map(self._key, keys)
287+        return self._cache.get_multi(keys)
288 
289     def close(self, **kwargs):
290         self._cache.disconnect_all()
291 
292     def incr(self, key, delta=1):
293+        key = self._key(key)
294         try:
295             val = self._cache.incr(key, delta)
296 
297@@ -77,6 +82,7 @@
298         return val
299 
300     def decr(self, key, delta=1):
301+        key = self._key(key)
302         try:
303             val = self._cache.decr(key, delta)
304 
305@@ -92,13 +98,15 @@
306     def set_many(self, data, timeout=0):
307         safe_data = {}
308         for key, value in data.items():
309+            key = self._key(key)
310             if isinstance(value, unicode):
311                 value = value.encode('utf-8')
312-            safe_data[smart_str(key)] = value
313+            safe_data[key] = value
314         self._cache.set_multi(safe_data, self._get_memcache_timeout(timeout))
315 
316     def delete_many(self, keys):
317-        self._cache.delete_multi(map(smart_str, keys))
318+        keys = map(self._key, keys)
319+        self._cache.delete_multi(keys)
320 
321     def clear(self):
322         self._cache.flush_all()
323Index: tests/regressiontests/cache/tests.py
324===================================================================
325--- tests/regressiontests/cache/tests.py        (revision 13357)
326+++ tests/regressiontests/cache/tests.py        (working copy)
327@@ -143,6 +143,9 @@
328         "clear does nothing for the dummy cache backend"
329         self.cache.clear()
330 
331+class PrefixedDummyCacheTests(DummyCacheTests):
332+    def setUp(self):
333+        self.cache = get_cache('dummy://', 'dummy_prefix')
334 
335 class BaseCacheTests(object):
336     # A common set of tests to apply to all cache backends
337@@ -364,10 +367,20 @@
338         cursor = connection.cursor()
339         cursor.execute('DROP TABLE %s' % connection.ops.quote_name(self._table_name))
340 
341+class PrefixedDBCacheTests(DBCacheTests):
342+    def setUp(self):
343+        self._table_name = 'test cache table'
344+        management.call_command('createcachetable', self._table_name, verbosity=0, interactive=False)
345+        self.cache = get_cache('db://%s' % self._table_name, 'db_prefix')
346+
347 class LocMemCacheTests(unittest.TestCase, BaseCacheTests):
348     def setUp(self):
349         self.cache = get_cache('locmem://')
350 
351+class PrefixedLocMemCacheTests(LocMemCacheTests):
352+    def setUp(self):
353+        self.cache = get_cache('locmem://', 'locmem_prefix')
354+
355 # memcached backend isn't guaranteed to be available.
356 # To check the memcached backend, the test settings file will
357 # need to contain a CACHE_BACKEND setting that points at
358@@ -377,6 +390,10 @@
359         def setUp(self):
360             self.cache = get_cache(settings.CACHE_BACKEND)
361 
362+    class PrefixedMemcacheCacheTests(MemcachedCacheTests):
363+        def setUp(self):
364+            self.cache = get_cache(settings.CACHE_BACKEND, 'memcache_prefix')
365+
366 class FileBasedCacheTests(unittest.TestCase, BaseCacheTests):
367     """
368     Specific test cases for the file-based cache.
369@@ -406,21 +423,26 @@
370         self.assert_(not os.path.exists(os.path.dirname(keypath)))
371         self.assert_(not os.path.exists(os.path.dirname(os.path.dirname(keypath))))
372 
373+class PrefixedFileBasedCacheTests(FileBasedCacheTests):
374+    def setUp(self):
375+        self.dirname = tempfile.mkdtemp()
376+        self.cache = get_cache('file://%s' % self.dirname)
377+
378 class CacheUtils(unittest.TestCase):
379     """TestCase for django.utils.cache functions."""
380 
381     def setUp(self):
382         self.path = '/cache/test/'
383-        self.old_settings_key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
384-        self.old_middleware_seconds = settings.CACHE_MIDDLEWARE_SECONDS
385+        self.old_cache_middleware_key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
386+        self.old_cache_middleware_seconds = settings.CACHE_MIDDLEWARE_SECONDS
387         self.orig_use_i18n = settings.USE_I18N
388         settings.CACHE_MIDDLEWARE_KEY_PREFIX = 'settingsprefix'
389         settings.CACHE_MIDDLEWARE_SECONDS = 1
390         settings.USE_I18N = False
391 
392     def tearDown(self):
393-        settings.CACHE_MIDDLEWARE_KEY_PREFIX = self.old_settings_key_prefix
394-        settings.CACHE_MIDDLEWARE_SECONDS = self.old_middleware_seconds
395+        settings.CACHE_MIDDLEWARE_KEY_PREFIX = self.old_cache_middleware_key_prefix
396+        settings.CACHE_MIDDLEWARE_SECONDS = self.old_cache_middleware_seconds
397         settings.USE_I18N = self.orig_use_i18n
398 
399     def _get_request(self, path):
400@@ -473,6 +495,16 @@
401         learn_cache_key(request, response)
402         self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
403 
404+class PrefixedCacheUtils(CacheUtils):
405+    def setUp(self):
406+        super(PrefixedCacheUtils, self).setUp()
407+        self.old_cache_key_prefix = settings.CACHE_KEY_PREFIX
408+        settings.CACHE_KEY_PREFIX = 'cacheprefix'
409+
410+    def tearDown(self):
411+        super(PrefixedCacheUtils, self).tearDown()
412+        settings.CACHE_KEY_PREFIX = self.old_cache_key_prefix
413+
414 class CacheI18nTest(unittest.TestCase):
415 
416     def setUp(self):
417@@ -568,5 +600,15 @@
418         get_cache_data = FetchFromCacheMiddleware().process_request(request)
419         self.assertEqual(get_cache_data.content, es_message)
420 
421+class PrefixedCacheI18nTest(CacheI18nTest):
422+    def setUp(self):
423+        super(PrefixedCacheI18nTest, self).setUp()
424+        self.old_cache_key_prefix = settings.CACHE_KEY_PREFIX
425+        settings.CACHE_KEY_PREFIX = 'cacheprefix'
426+
427+    def tearDown(self):
428+        super(PrefixedCacheI18nTest, self).tearDown()
429+        settings.CACHE_KEY_PREFIX = self.old_cache_key_prefix
430+
431 if __name__ == '__main__':
432     unittest.main()
433Index: docs/topics/cache.txt
434===================================================================
435--- docs/topics/cache.txt       (revision 13357)
436+++ docs/topics/cache.txt       (working copy)
437@@ -600,6 +600,27 @@
438     However, if the backend doesn't natively provide an increment/decrement
439     operation, it will be implemented using a two-step retrieve/update.
440 
441+CACHE_KEY_PREFIX
442+----------------
443+
444+It is a common occurence to have a shared cache instance running on your development
445+or production server for use across multiple projects, i.e. all sites are pointing
446+to the memcached instance running on port 11211. As a result, cache key conflicts
447+may arise which could result in data from one site being used by another.
448+
449+To alleviate this, CACHE_KEY_PREFIX can be set. This will be prepended to all keys
450+that are used with the cache backend, transparently::
451+
452+    # in settings, CACHE_KEY_PREFIX = 'myproject_'
453+
454+    >>> cache.set('my_key', 'hello world!') # set with key 'myproject_my_key'
455+    >>> cache.get('my_key') # retrieved with key 'myproject_my_key'
456+    'hello world!'
457+
458+Of course, for sites that _do_ share content, simply set the CACHE_KEY_PREFIX for
459+both sites to the same value. The default value for CACHE_KEY_PREFIX is the empty
460+string ``''``.
461+
462 Upstream caches
463 ===============
464