From 73e49e6423658893411afa75892b5c5963e90752 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 7 Oct 2014 00:06:11 +0200 Subject: [PATCH] Implement event-based caching --- src/tixlbase/cache.py | 73 ++++++++++++++++++++++++ src/tixlbase/models.py | 52 ++++++++++++++++- src/tixlplugins/timerestriction/tests.py | 40 ++++++------- 3 files changed, 142 insertions(+), 23 deletions(-) diff --git a/src/tixlbase/cache.py b/src/tixlbase/cache.py index e69de29bb2..b7f0ffa7b1 100644 --- a/src/tixlbase/cache.py +++ b/src/tixlbase/cache.py @@ -0,0 +1,73 @@ +import time +import hashlib + +from django.core.cache import caches + + +class EventRelatedCache: + """ + This object behaves exactly like the cache implementations by Django + but with one important difference: It stores all keys related to a + certain event, so you pass an event when creating this object and if + you store data in this cache, it is only stored for this event. The + main purpose of this is to be able to flush all cached data related + to this event at once. + """ + + def __init__(self, event, cache='default'): + self.cache = caches[cache] + self.prefix = self._build_prefix() + self.prefixkey = 'event:%d' % self.event.pk + + def _prefix_key(self, original_key): + # Race conditions can happen here, but should be very very rare. + # We could only handle this by going _really_ lowlevel using + # memcached's `add` keyword instead of `set`. + # See also: + # https://code.google.com/p/memcached/wiki/NewProgrammingTricks#Namespacing + prefix = self.cache.get(self.prefixkey) + if prefix is None: + prefix = int(time.time()) + self.cache.set(self.prefixkey, prefix) + key = 'event:%d:%d:%s' % (self.event.pk, prefix, original_key) + if len(key) > 200: # Hash long keys, as memcached has a length limit + # TODO: Use a more efficient, non-cryptographic hash algorithm + key = hashlib.sha256(key.encode("UTF-8")).hexdigest() + return key + + def clear(self): + try: + prefix = self.cache.incr(self.prefixkey, 1) + except ValueError: + prefix = int(time.time()) + self.cache.set(self.prefixkey, prefix) + + def set(self, key, value, timeout=300): + return self.cache.set(self._prefix_key(key), value, timeout) + + def get(self, key): + return self.cache.get(self._prefix_key(key)) + + def get_many(self, keys): + return self.cache.get_many([self._prefix_key(key) for key in keys]) + + def set_many(self, values, timeout=300): + newvalues = {} + for i in values.items(): + newvalues[self._prefix_key(i[0])] = i[1] + return self.cache.set_many([newvalues], timeout) + + def delete(self, key): + return self.cache.delete(self._prefix_key(key)) + + def delete_many(self, keys): + return self.cache.delete_many([self._prefix_key(key) for key in keys]) + + def incr(self, key, by=1): + return self.cache.incr(self._prefix_key(key), by) + + def decr(self, key, by=1): + return self.cache.decr(self._prefix_key(key), by) + + def close(self): + pass diff --git a/src/tixlbase/models.py b/src/tixlbase/models.py index ba0f313a6e..afcbd0341e 100644 --- a/src/tixlbase/models.py +++ b/src/tixlbase/models.py @@ -284,6 +284,10 @@ class Event(models.Model): def __str__(self): return self.name + def save(self, *args, **kwargs): + self.get_cache().clear() + return super().save(self, *args, **kwargs) + def get_date_from_display(self): return _date( self.date_from, @@ -298,6 +302,10 @@ class Event(models.Model): "DATETIME_FORMAT" if self.show_times else "DATE_FORMAT" ) + def get_cache(self): + from tixlbase.cache import EventRelatedCache + return EventRelatedCache(self) + class EventPermission(models.Model): """ @@ -353,6 +361,11 @@ class ItemCategory(models.Model): def __str__(self): return self.name + def save(self, *args, **kwargs): + if self.event: + self.event.get_cache().clear() + return super().save(self, *args, **kwargs) + class Property(models.Model): """ @@ -377,6 +390,11 @@ class Property(models.Model): def __str__(self): return self.name + def save(self, *args, **kwargs): + if self.event: + self.event.get_cache().clear() + return super().save(self, *args, **kwargs) + class PropertyValue(models.Model): """ @@ -405,6 +423,11 @@ class PropertyValue(models.Model): def __str__(self): return "%s: %s" % (self.prop.name, self.value) + def save(self, *args, **kwargs): + if self.prop: + self.prop.event.get_cache().clear() + return super().save(self, *args, **kwargs) + class Question(models.Model): """ @@ -446,6 +469,11 @@ class Question(models.Model): def __str__(self): return self.question + def save(self, *args, **kwargs): + if self.event: + self.event.get_cache().clear() + return super().save(self, *args, **kwargs) + class Item(models.Model): """ @@ -529,6 +557,11 @@ class Item(models.Model): def __str__(self): return self.name + def save(self, *args, **kwargs): + if self.event: + self.event.get_cache().clear() + return super().save(self, *args, **kwargs) + def delete(self): self.deleted = True self.active = False @@ -569,9 +602,6 @@ class Item(models.Model): return result - def get_cache(self): - return None - class ItemVariation(models.Model): """ @@ -614,6 +644,11 @@ class ItemVariation(models.Model): verbose_name = _("Item variation") verbose_name_plural = _("Item variations") + def save(self, *args, **kwargs): + if self.item: + self.item.event.get_cache().clear() + return super().save(self, *args, **kwargs) + class BaseRestriction(models.Model): """ @@ -621,6 +656,12 @@ class BaseRestriction(models.Model): of Items or ItemVariations. This model is just an abstract base class to be extended by restriction plugins. """ + event = models.ForeignKey( + Event, + on_delete=models.CASCADE, + related_name="restrictions_%(app_label)s_%(class)s", + verbose_name=_("Event"), + ) items = models.ManyToManyField( Item, related_name="restrictions_%(app_label)s_%(class)s", @@ -634,3 +675,8 @@ class BaseRestriction(models.Model): abstract = True verbose_name = _("Restriction") verbose_name_plural = _("Restrictions") + + def save(self, *args, **kwargs): + if self.event: + self.event.get_cache().clear() + return super().save(self, *args, **kwargs) diff --git a/src/tixlplugins/timerestriction/tests.py b/src/tixlplugins/timerestriction/tests.py index 86a7f3d58d..e18d4b1d04 100644 --- a/src/tixlplugins/timerestriction/tests.py +++ b/src/tixlplugins/timerestriction/tests.py @@ -33,8 +33,8 @@ class TimeRestrictionTest(TestCase): def test_nothing(self): result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 1) self.assertTrue('available' not in result[0] or result[0]['available'] is True) @@ -48,8 +48,8 @@ class TimeRestrictionTest(TestCase): r.items.add(self.item) result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 1) self.assertIn('available', result[0]) @@ -65,8 +65,8 @@ class TimeRestrictionTest(TestCase): r.items.add(self.item) result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 1) self.assertIn('available', result[0]) @@ -87,8 +87,8 @@ class TimeRestrictionTest(TestCase): r2.items.add(self.item) result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 1) self.assertIn('available', result[0]) @@ -110,8 +110,8 @@ class TimeRestrictionTest(TestCase): r2.items.add(self.item) result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 1) self.assertIn('available', result[0]) @@ -133,8 +133,8 @@ class TimeRestrictionTest(TestCase): r2.items.add(self.item) result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 1) self.assertIn('available', result[0]) @@ -156,8 +156,8 @@ class TimeRestrictionTest(TestCase): r2.items.add(self.item) result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 1) self.assertIn('available', result[0]) @@ -179,8 +179,8 @@ class TimeRestrictionTest(TestCase): r1.variations.add(v1) result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 3) for v in result: @@ -219,8 +219,8 @@ class TimeRestrictionTest(TestCase): r3.variations.add(v2) result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 3) for v in result: @@ -261,8 +261,8 @@ class TimeRestrictionTest(TestCase): r3.variations.add(v2) result = signals.availability_handler( None, item=self.item, - variations=self.item.get_all_variations(), - context=None, cache=self.item.get_cache() + variations=self.event.get_all_variations(), + context=None, cache=self.event.get_cache() ) self.assertEqual(len(result), 3) for v in result: