Adds a signal to trigger post-invalidation behaviours.

This commit is contained in:
Bertrand Bordage 2015-04-17 20:06:18 +02:00
parent 389aded5bf
commit 3798a1c97d
5 changed files with 118 additions and 12 deletions

7
cachalot/signals.py Normal file
View File

@ -0,0 +1,7 @@
# coding: utf-8
from __future__ import unicode_literals
from django.dispatch import Signal
post_invalidation = Signal(providing_args=['db_alias'])

View File

@ -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

63
cachalot/tests/signals.py Normal file
View File

@ -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 cant change the DB used since theres 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)

View File

@ -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)

View File

@ -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.')