Waits until the end of transaction before triggering the signal, and trigger the signal on all invalidations.

This commit is contained in:
Bertrand Bordage 2015-10-28 12:54:39 +01:00
parent e02632881e
commit d959f3e42f
6 changed files with 114 additions and 30 deletions

View File

@ -7,24 +7,23 @@ from django.db import connections
from django.utils.six import string_types
from .cache import cachalot_caches
from .utils import _get_table_cache_key, _invalidate_table_cache_keys
from .signals import post_invalidation
from .utils import _get_table_cache_key, _invalidate_tables
__all__ = ('invalidate', 'get_last_invalidation')
def _get_table_cache_keys_per_cache_and_db(tables, cache_alias, db_alias):
def _cache_db_tables_iterator(tables, cache_alias, db_alias):
no_tables = not tables
cache_aliases = settings.CACHES if cache_alias is None else (cache_alias,)
db_aliases = settings.DATABASES if db_alias is None else (db_alias,)
for db_alias in db_aliases:
if no_tables:
tables = connections[db_alias].introspection.table_names()
for cache_alias in cache_aliases:
table_cache_keys = [
_get_table_cache_key(db_alias, t) for t in tables]
if table_cache_keys:
yield cache_alias, db_alias, table_cache_keys
if tables:
for cache_alias in cache_aliases:
yield cache_alias, db_alias, tables
def _get_tables(tables_or_models):
@ -61,11 +60,15 @@ def invalidate(*tables_or_models, **kwargs):
raise TypeError(
"invalidate() got an unexpected keyword argument '%s'" % k)
table_cache_keys_per_cache = _get_table_cache_keys_per_cache_and_db(
_get_tables(tables_or_models), cache_alias, db_alias)
for cache_alias, db_alias, table_cache_keys in table_cache_keys_per_cache:
_invalidate_table_cache_keys(
cachalot_caches.get_cache(cache_alias, db_alias), table_cache_keys)
invalidated = set()
for cache_alias, db_alias, tables in _cache_db_tables_iterator(
_get_tables(tables_or_models), cache_alias, db_alias):
_invalidate_tables(
cachalot_caches.get_cache(cache_alias, db_alias), db_alias, tables)
invalidated.update(tables)
for table in invalidated:
post_invalidation.send(table, db_alias=db_alias)
def get_last_invalidation(*tables_or_models, **kwargs):
@ -97,9 +100,9 @@ def get_last_invalidation(*tables_or_models, **kwargs):
"keyword argument '%s'" % k)
last_invalidation = 0.0
table_cache_keys_per_cache = _get_table_cache_keys_per_cache_and_db(
_get_tables(tables_or_models), cache_alias, db_alias)
for cache_alias, db_alias, table_cache_keys in table_cache_keys_per_cache:
for cache_alias, db_alias, tables in _cache_db_tables_iterator(
_get_tables(tables_or_models), cache_alias, db_alias):
table_cache_keys = [_get_table_cache_key(db_alias, t) for t in tables]
invalidations = cachalot_caches.get_cache(
cache_alias, db_alias).get_many(table_cache_keys).values()
if invalidations:

View File

@ -8,6 +8,7 @@ from django.core.cache import caches
from django.db import DEFAULT_DB_ALIAS
from .settings import cachalot_settings
from .signals import post_invalidation
from .transaction import AtomicCache
@ -21,7 +22,7 @@ class CacheHandler(local):
def get_atomic_cache(self, cache_alias, db_alias, level):
if cache_alias not in self.atomic_caches[db_alias][level]:
self.atomic_caches[db_alias][level][cache_alias] = AtomicCache(
self.get_cache(cache_alias, db_alias, level-1))
self.get_cache(cache_alias, db_alias, level-1), db_alias)
return self.atomic_caches[db_alias][level][cache_alias]
def get_cache(self, cache_alias=None, db_alias=None, atomic_level=-1):
@ -45,8 +46,14 @@ class CacheHandler(local):
db_alias = DEFAULT_DB_ALIAS
atomic_caches = self.atomic_caches[db_alias].pop().values()
if commit:
to_be_invalidated = set()
for atomic_cache in atomic_caches:
atomic_cache.commit()
to_be_invalidated.update(atomic_cache.to_be_invalidated)
# This happens when committing the outermost atomic block.
if not self.atomic_caches[db_alias]:
for table in to_be_invalidated:
post_invalidation.send(table, db_alias=db_alias)
cachalot_caches = CacheHandler()

View File

@ -5,10 +5,11 @@ from unittest import skipIf
from django.conf import settings
from django.contrib.auth.models import User
from django.db import DEFAULT_DB_ALIAS
from django.db import DEFAULT_DB_ALIAS, transaction
from django.test import TransactionTestCase
from cachalot.signals import post_invalidation
from ..api import invalidate
from ..signals import post_invalidation
from .models import Test
@ -35,6 +36,62 @@ class SignalsTestCase(TransactionTestCase):
self.assertListEqual(l, [])
User.objects.create_user('user')
self.assertListEqual(l, [('auth_user', DEFAULT_DB_ALIAS)])
post_invalidation.disconnect(receiver, sender=User._meta.db_table)
def test_table_invalidated_in_transaction(self):
"""
Checks that the ``post_invalidation`` signal is triggered only after
the end of a transaction.
"""
l = []
def receiver(sender, **kwargs):
db_alias = kwargs['db_alias']
l.append((sender, db_alias))
post_invalidation.connect(receiver)
self.assertListEqual(l, [])
with transaction.atomic():
Test.objects.create(name='test1')
self.assertListEqual(l, [])
self.assertListEqual(l, [('cachalot_test', DEFAULT_DB_ALIAS)])
del l[:] # Empties the list
self.assertListEqual(l, [])
with transaction.atomic():
Test.objects.create(name='test2')
with transaction.atomic():
Test.objects.create(name='test3')
self.assertListEqual(l, [])
self.assertListEqual(l, [])
self.assertListEqual(l, [('cachalot_test', DEFAULT_DB_ALIAS)])
post_invalidation.disconnect(receiver)
def test_table_invalidated_once_per_transaction_or_invalidate(self):
"""
Checks that the ``post_invalidation`` signal is triggered only after
the end of a transaction.
"""
l = []
def receiver(sender, **kwargs):
db_alias = kwargs['db_alias']
l.append((sender, db_alias))
post_invalidation.connect(receiver)
self.assertListEqual(l, [])
with transaction.atomic():
Test.objects.create(name='test1')
self.assertListEqual(l, [])
Test.objects.create(name='test2')
self.assertListEqual(l, [])
self.assertListEqual(l, [('cachalot_test', DEFAULT_DB_ALIAS)])
del l[:] # Empties the list
self.assertListEqual(l, [])
invalidate(Test, db_alias=DEFAULT_DB_ALIAS)
self.assertListEqual(l, [('cachalot_test', DEFAULT_DB_ALIAS)])
post_invalidation.disconnect(receiver)
@skipIf(len(settings.DATABASES) == 1,
'We cant change the DB used since theres only one configured')

View File

@ -2,13 +2,12 @@
from __future__ import unicode_literals
from .utils import _invalidate_table_cache_keys
class AtomicCache(dict):
def __init__(self, parent_cache):
def __init__(self, parent_cache, db_alias):
super(AtomicCache, self).__init__()
self.parent_cache = parent_cache
self.db_alias = db_alias
self.to_be_invalidated = set()
def set(self, k, v, timeout):
@ -31,4 +30,10 @@ class AtomicCache(dict):
self.parent_cache.set_many(self, None)
# The previous `set_many` is not enough. The parent cache needs to be
# invalidated in case another transaction occurred in the meantime.
_invalidate_table_cache_keys(self.parent_cache, self.to_be_invalidated)
_invalidate_tables(self.parent_cache, self.db_alias,
self.to_be_invalidated)
# We import this after AtomicCache to avoid a circular import issue and
# avoid importing this locally, which degrades performance.
from .utils import _invalidate_tables

View File

@ -13,6 +13,7 @@ from django.utils.six import text_type, binary_type
from .settings import cachalot_settings
from .signals import post_invalidation
from .transaction import AtomicCache
class UncachableQuery(Exception):
@ -145,18 +146,21 @@ def _get_table_cache_keys(compiler):
return [_get_table_cache_key(db_alias, t) for t in tables]
def _invalidate_table_cache_keys(cache, table_cache_keys):
if hasattr(cache, 'to_be_invalidated'):
cache.to_be_invalidated.update(table_cache_keys)
def _invalidate_tables(cache, db_alias, tables):
now = time()
d = {}
for k in table_cache_keys:
d[k] = now
for table in tables:
d[_get_table_cache_key(db_alias, table)] = now
cache.set_many(d, None)
if isinstance(cache, AtomicCache):
cache.to_be_invalidated.update(tables)
def _invalidate_table(cache, db_alias, table):
table_cache_key = _get_table_cache_key(db_alias, table)
_invalidate_table_cache_keys(cache, (table_cache_key,))
cache.set(_get_table_cache_key(db_alias, table), time(), None)
post_invalidation.send(table, db_alias=db_alias)
if isinstance(cache, AtomicCache):
cache.to_be_invalidated.add(table)
else:
post_invalidation.send(table, db_alias=db_alias)

View File

@ -204,6 +204,14 @@ just after a cache invalidation (when you modify something in a SQL table).
Be careful when you specify ``sender``, as it is sensible to string type.
To be sure, use ``Model._meta.db_table``.
This signal is not directly triggered during transactions,
it waits until the current transaction ends. This signal is also triggered
when invalidating using the API or the ``manage.py`` command. Be careful
when using multiple databases, if you invalidate all databases by simply
calling ``invalidate()``, this signal will be triggered one time
for each database and for each model. If you have 3 databases and 20 models,
``invalidate()`` will trigger the signal 60 times.
Example:
.. code:: python