Browse Source

Added cachebuster. #227

Rafał Pitoń 11 years ago
parent
commit
b1bc68ddc0

+ 63 - 1
docs/developers/cache_buster.rst

@@ -2,4 +2,66 @@
 Cache Buster
 ============
 
-Cache buster is small feature that allows certain cache-based systems find out when data they were dependant on has been invalidated and must be replaced by new one.
+Cache buster is small feature that allows certain cache-based systems find out when data they were dependant on has been changed, making their cache no longer valid.
+
+Using Cache Buster
+==================
+
+Cache buster lives in :py:mod:`misago.core.cachebuster` and provides following API:
+
+
+is_valid
+--------
+
+.. function:: is_valid(cache, version)
+
+Checks if specific cache version is valid or raises ``ValueError`` if cache key is invalid.
+
+
+get_version
+-----------
+
+.. function:: get_version(cache)
+
+Returns current valid cache version as an integer number or raises ``ValueError`` if cache key is invalid.
+
+
+invalidate
+----------
+
+.. function:: invalidate(cache)
+
+Makes specified cache invalid.
+
+
+invalidate_all
+--------------
+
+.. function:: invalidate_all()
+
+Makes all versioned caches invalid.
+
+
+Adding Custom Cache Buster
+==========================
+
+You may add and remove your own cache names to cache buster by using following commands:
+
+.. note::
+   Don't forget to call `invalidate_all` function after adding or removing cache name from buster to force it to rebuild its own cache.
+
+
+register
+--------
+
+.. function:: register(cache)
+
+Registers new cache in cache buster for tracking.
+
+
+unregister
+----------
+
+.. function:: unregister(cache)
+
+Removes cache from cache buster and disables its tracking. This function will raise ``ValueError`` if cache you are trying to unregister is not registered.

+ 1 - 1
docs/developers/thread_store.rst

@@ -4,7 +4,7 @@ Misago Thread Store
 
 Thread store is simple memory-based cache some Misago features use to maintain state for request duration.
 
-It offers subset of standard cache API known from Django:
+Thread store lives in :py:mod:`misago.core.threadstore` and offers subset of standard cache API known from Django
 
 
 get

+ 97 - 0
misago/core/cachebuster.py

@@ -0,0 +1,97 @@
+from django.core.cache import cache as default_cache
+from django.db import models
+from misago.core import threadstore
+
+
+CACHE_KEY = 'misago_cachebuster'
+
+
+class CacheBusterController(object):
+    def register_cache(self, cache):
+        from misago.core.models import CacheVersion
+        CacheVersion.objects.create(cache=cache)
+
+    def unregister_cache(self, cache):
+        from misago.core.models import CacheVersion
+        try:
+            cache = CacheVersion.objects.get(cache=cache)
+            cache.delete()
+        except CacheVersion.DoesNotExist:
+            raise ValueError('Cache "%s" is not registered' % cache)
+
+    @property
+    def cache(self):
+        return self.read_threadstore()
+
+    def read_threadstore(self):
+        data = threadstore.get(CACHE_KEY, 'nada')
+        if data == 'nada':
+            data = self.read_cache()
+            threadstore.set(CACHE_KEY, data)
+        return data
+
+    def read_cache(self):
+        data = default_cache.get(CACHE_KEY, 'nada')
+        if data == 'nada':
+            data = self.read_db()
+            default_cache.set(CACHE_KEY, data)
+        return data
+
+    def read_db(self):
+        from misago.core.models import CacheVersion
+        data = {}
+        for cache_version in CacheVersion.objects.iterator():
+            data[cache_version.cache] = cache_version.version
+        return data
+
+    def get_cache_version(self, cache):
+        try:
+            return self.cache[cache]
+        except KeyError:
+            raise ValueError('Cache "%s" is not registered' % cache)
+
+    def is_cache_valid(self, cache, version):
+        try:
+            return self.cache[cache] == version
+        except KeyError:
+            raise ValueError('Cache "%s" is not registered' % cache)
+
+    def invalidate_cache(self, cache):
+        from misago.core.models import CacheVersion
+        CacheVersion.objects.filter(cache=cache).update(
+            version=models.F('version') + 1)
+        self.cache[cache] += 1
+        default_cache.delete(CACHE_KEY)
+
+    def invalidate_all(self):
+        from misago.core.models import CacheVersion
+        CacheVersion.objects.update(version=models.F('version') + 1)
+        default_cache.delete(CACHE_KEY)
+
+
+_controller = CacheBusterController()
+
+
+# Expose controller API
+def register(cache):
+    _controller.register_cache(cache)
+
+
+def unregister(cache):
+    _controller.unregister_cache(cache)
+
+
+def get_version(cache):
+    return _controller.get_cache_version(cache)
+
+
+def is_valid(cache, version):
+    return _controller.is_cache_valid(cache, version)
+
+
+def invalidate(cache):
+    _controller.invalidate_cache(cache)
+
+
+def invalidate_all():
+    _controller.invalidate_all()

+ 2 - 5
misago/core/models.py

@@ -1,9 +1,6 @@
 from django.db import models
 
 
-class VersionControl(models.Model):
-    key = models.CharField(max_length=128)
+class CacheVersion(models.Model):
+    cache = models.CharField(max_length=128)
     version = models.PositiveIntegerField(default=0)
-
-    class Meta:
-        app_label = 'misago_core'

+ 75 - 0
misago/core/tests/test_cachebuster.py

@@ -0,0 +1,75 @@
+from django.test import TestCase
+from misago.core import cachebuster
+from misago.core import threadstore
+from misago.core.models import CacheVersion
+
+
+class CacheBusterTests(TestCase):
+    def test_register_unregister_cache(self):
+        """register and unregister adds/removes cache"""
+        test_cache_name = 'eric_the_fish'
+        with self.assertRaises(CacheVersion.DoesNotExist):
+            CacheVersion.objects.get(cache=test_cache_name)
+
+        cachebuster.register(test_cache_name)
+        CacheVersion.objects.get(cache=test_cache_name)
+
+        cachebuster.unregister(test_cache_name)
+        with self.assertRaises(CacheVersion.DoesNotExist):
+            CacheVersion.objects.get(cache=test_cache_name)
+
+
+class CacheBusterCacheTests(TestCase):
+    def setUp(self):
+        self.cache_name = 'eric_the_fish'
+        cachebuster.register(self.cache_name)
+
+    def tearDown(self):
+        threadstore.clear()
+
+    def test_cache_validation(self):
+        """cache correctly validates"""
+        version = cachebuster.get_version(self.cache_name)
+        self.assertEqual(version, 0)
+
+        db_version = CacheVersion.objects.get(cache=self.cache_name).version
+        self.assertEqual(db_version, 0)
+
+        self.assertEqual(db_version, version)
+        self.assertTrue(cachebuster.is_valid(self.cache_name, version))
+        self.assertTrue(cachebuster.is_valid(self.cache_name, db_version))
+
+    def test_cache_invalidation(self):
+        """invalidate has increased valid version number"""
+        db_version = CacheVersion.objects.get(cache=self.cache_name).version
+        cachebuster.invalidate(self.cache_name)
+
+        new_version = cachebuster.get_version(self.cache_name)
+        new_db_version = CacheVersion.objects.get(cache=self.cache_name)
+        new_db_version = new_db_version.version
+
+        self.assertEqual(new_version, 1)
+        self.assertEqual(new_db_version, 1)
+        self.assertEqual(new_version, new_db_version)
+        self.assertFalse(cachebuster.is_valid(self.cache_name, db_version))
+        self.assertTrue(cachebuster.is_valid(self.cache_name, new_db_version))
+
+    def test_cache_invalidation_all(self):
+        """invalidate_all has increased valid version number"""
+        cache_a = "eric_the_halibut"
+        cache_b = "eric_the_crab"
+        cache_c = "eric_the_lion"
+
+        cachebuster.register(cache_a)
+        cachebuster.register(cache_b)
+        cachebuster.register(cache_c)
+
+        cachebuster.invalidate_all()
+
+        new_version_a = CacheVersion.objects.get(cache=cache_a).version
+        new_version_b = CacheVersion.objects.get(cache=cache_b).version
+        new_version_c = CacheVersion.objects.get(cache=cache_c).version
+
+        self.assertEqual(new_version_a, 1)
+        self.assertEqual(new_version_b, 1)
+        self.assertEqual(new_version_c, 1)