From 3798a1c97db176271ba297fb1471fba84f6bb6d4 Mon Sep 17 00:00:00 2001 From: Bertrand Bordage Date: Fri, 17 Apr 2015 20:06:18 +0200 Subject: [PATCH] Adds a signal to trigger post-invalidation behaviours. --- cachalot/signals.py | 7 +++++ cachalot/tests/__init__.py | 1 + cachalot/tests/signals.py | 63 ++++++++++++++++++++++++++++++++++++++ cachalot/utils.py | 18 +++++------ docs/quickstart.rst | 41 ++++++++++++++++++++++++- 5 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 cachalot/signals.py create mode 100644 cachalot/tests/signals.py diff --git a/cachalot/signals.py b/cachalot/signals.py new file mode 100644 index 0000000..2d0ac20 --- /dev/null +++ b/cachalot/signals.py @@ -0,0 +1,7 @@ +# coding: utf-8 + +from __future__ import unicode_literals +from django.dispatch import Signal + + +post_invalidation = Signal(providing_args=['db_alias']) diff --git a/cachalot/tests/__init__.py b/cachalot/tests/__init__.py index 96d9f28..8406849 100644 --- a/cachalot/tests/__init__.py +++ b/cachalot/tests/__init__.py @@ -5,3 +5,4 @@ from .thread_safety import ThreadSafetyTestCase from .multi_db import MultiDatabaseTestCase from .settings import SettingsTestCase from .api import APITestCase, CommandTestCase +from .signals import SignalsTestCase diff --git a/cachalot/tests/signals.py b/cachalot/tests/signals.py new file mode 100644 index 0000000..0024cd5 --- /dev/null +++ b/cachalot/tests/signals.py @@ -0,0 +1,63 @@ +# coding: utf-8 + +from __future__ import unicode_literals +from django.contrib.auth.models import User + + +try: + from unittest import skipIf +except ImportError: # For Python 2.6 + from unittest2 import skipIf + +from django.conf import settings +from django.db import DEFAULT_DB_ALIAS +from django.test import TransactionTestCase + +from cachalot.signals import post_invalidation + +from .models import Test + + +class SignalsTestCase(TransactionTestCase): + def test_table_invalidated(self): + l = [] + + def receiver(sender, **kwargs): + db_alias = kwargs['db_alias'] + l.append((sender, db_alias)) + + post_invalidation.connect(receiver) + self.assertListEqual(l, []) + list(Test.objects.all()) + self.assertListEqual(l, []) + Test.objects.create(name='test1') + self.assertListEqual(l, [('cachalot_test', DEFAULT_DB_ALIAS)]) + post_invalidation.disconnect(receiver) + + del l[:] # Empties the list + post_invalidation.connect(receiver, sender=User._meta.db_table) + Test.objects.create(name='test2') + self.assertListEqual(l, []) + User.objects.create_user('user') + self.assertListEqual(l, [('auth_user', DEFAULT_DB_ALIAS)]) + + @skipIf(len(settings.DATABASES) == 1, + 'We can’t change the DB used since there’s only one configured') + def test_table_invalidated_multi_db(self): + db_alias2 = next(alias for alias in settings.DATABASES + if alias != DEFAULT_DB_ALIAS) + l = [] + + def receiver(sender, **kwargs): + db_alias = kwargs['db_alias'] + l.append((sender, db_alias)) + + post_invalidation.connect(receiver) + self.assertListEqual(l, []) + Test.objects.using(DEFAULT_DB_ALIAS).create(name='test') + self.assertListEqual(l, [('cachalot_test', DEFAULT_DB_ALIAS)]) + Test.objects.using(db_alias2).create(name='test') + self.assertListEqual(l, [ + ('cachalot_test', DEFAULT_DB_ALIAS), + ('cachalot_test', db_alias2)]) + post_invalidation.disconnect(receiver) diff --git a/cachalot/utils.py b/cachalot/utils.py index 8f685f1..104c65d 100644 --- a/cachalot/utils.py +++ b/cachalot/utils.py @@ -15,6 +15,7 @@ else: from django.utils.module_loading import import_by_path as import_string from .settings import cachalot_settings +from .signals import post_invalidation def get_query_cache_key(compiler): @@ -99,16 +100,6 @@ def _get_tables(query, db_alias): def _get_table_cache_keys(compiler): - """ - Returns a ``list`` of cache keys for all the SQL tables used - by ``compiler``. - - :arg compiler: A SQLCompiler that will generate the SQL query - :type compiler: django.db.models.sql.compiler.SQLCompiler - :return: Cache keys for the SQL tables used - :rtype: list - """ - db_alias = compiler.using tables = _get_tables(compiler.query, db_alias) return [_get_table_cache_key(db_alias, t) for t in tables] @@ -125,5 +116,10 @@ def _invalidate_table_cache_keys(cache, table_cache_keys): def _invalidate_tables(cache, compiler): - table_cache_keys = _get_table_cache_keys(compiler) + db_alias = compiler.using + tables = _get_tables(compiler.query, db_alias) + table_cache_keys = [_get_table_cache_key(db_alias, t) for t in tables] _invalidate_table_cache_keys(cache, table_cache_keys) + + for table in tables: + post_invalidation.send(table, db_alias=db_alias) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 29f17ba..1ef966e 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -95,7 +95,6 @@ Dynamic overriding ~~~~~~~~~~~~~~~~~~ Django-cachalot is built so that its settings can be dynamically changed. - For example: .. code:: python @@ -112,3 +111,43 @@ For example: # Globally disables SQL caching until you set it back to True settings.CACHALOT_ENABLED = False + + +Signal +...... + +``cachalot.signals.post_invalidation`` is available if you need to do something +just after a cache invalidation (when you modify something in a SQL table). +``sender`` is the name of the SQL table invalidated, and a keyword argument +``db_alias`` explains which database is affected by the invalidation. +Be careful when you specify ``sender``, as it is sensible to string type. +To be sure, use ``Model._meta.db_table``. + +Example: + +.. code:: python + + from cachalot.signals import post_invalidation + from django.dispatch import receiver + from django.core.mail import mail_admins + from django.contrib.auth import * + + # This prints a message to the console after each table invalidation + def invalidation_debug(sender, **kwargs): + db_alias = kwargs['db_alias'] + print('%s was invalidated in the DB configured as %s' + % (sender, db_alias)) + + post_invalidation.connect(invalidation_debug) + + # Using the `receiver` decorator is just a nicer way + # to write the same thing as `signal.connect`. + # Here we specify `sender` so that the function is executed only if + # the table invalidated is the one specified. + # We also connect it several times to be executed for several senders. + @receiver(post_invalidation, sender=User.groups.through._meta.db_table) + @receiver(post_invalidation, sender=User.user_permissions.through._meta.db_table) + @receiver(post_invalidation, sender=Group.permissions.through._meta.db_table) + def warn_admin(sender, **kwargs): + mail_admins('User permissions changed', + 'Someone probably gained or lost Django permissions.')