From 6c136360449900c138da1f9d7fefff134eb8a568 Mon Sep 17 00:00:00 2001 From: Bertrand Bordage Date: Mon, 5 Oct 2015 23:48:48 +0200 Subject: [PATCH] Creates get_last_invalidation(), while simplifying API usage. --- cachalot/api.py | 60 +++++++++++++++++-- .../commands/invalidate_cachalot.py | 2 +- cachalot/monkey_patch.py | 4 +- cachalot/tests/api.py | 22 ++++++- docs/api.rst | 5 +- 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/cachalot/api.py b/cachalot/api.py index b0d356094..b88bff3c7 100644 --- a/cachalot/api.py +++ b/cachalot/api.py @@ -11,7 +11,7 @@ from .utils import _get_table_cache_key, _invalidate_table_cache_keys -__all__ = ('invalidate',) +__all__ = ('invalidate', 'get_last_invalidation') def _get_table_cache_keys_per_cache(tables, cache_alias, db_alias): @@ -35,11 +35,12 @@ def _get_tables(tables_or_models): for o in tables_or_models] -def invalidate(tables_or_models=(), cache_alias=None, db_alias=None): +def invalidate(*tables_or_models, **kwargs): """ Clears what was cached by django-cachalot implying one or more SQL tables - or models from ``tables_or_models``. If ``tables_or_models`` - is not specified, all tables found in the database are invalidated. + or models from ``tables_or_models``. + If ``tables_or_models`` is not specified, all tables found in the database + (including those outside Django) are invalidated. If ``cache_alias`` is specified, it only clears the SQL queries stored on this cache, otherwise queries from all caches are cleared. @@ -47,8 +48,8 @@ def invalidate(tables_or_models=(), cache_alias=None, db_alias=None): If ``db_alias`` is specified, it only clears the SQL queries executed on this database, otherwise queries from all databases are cleared. - :arg tables_or_models: SQL tables names - :type tables_or_models: iterable of strings or models, or NoneType + :arg tables_or_models: SQL tables names or models (or combination of both) + :type tables_or_models: tuple of strings or models :arg cache_alias: Alias from the Django ``CACHES`` setting :type cache_alias: string or NoneType :arg db_alias: Alias from the Django ``DATABASES`` setting @@ -56,9 +57,56 @@ def invalidate(tables_or_models=(), cache_alias=None, db_alias=None): :returns: Nothing :rtype: NoneType """ + # TODO: Replace with positional arguments when we drop Python 2 support. + cache_alias = kwargs.pop('cache_alias', None) + db_alias = kwargs.pop('db_alias', None) + for k in kwargs: + raise TypeError( + "invalidate() got an unexpected keyword argument '%s'" % k) table_cache_keys_per_cache = _get_table_cache_keys_per_cache( _get_tables(tables_or_models), cache_alias, db_alias) for cache_alias, table_cache_keys in table_cache_keys_per_cache.items(): _invalidate_table_cache_keys(cachalot_caches.get_cache(cache_alias), table_cache_keys) + + +def get_last_invalidation(*tables_or_models, **kwargs): + """ + Returns the timestamp of the most recent invalidation of the given + ``tables_or_models``. If ``tables_or_models`` is not specified, + all tables found in the database (including those outside Django) are used. + + If ``cache_alias`` is specified, it only clears the SQL queries stored + on this cache, otherwise queries from all caches are cleared. + + If ``db_alias`` is specified, it only clears the SQL queries executed + on this database, otherwise queries from all databases are cleared. + + :arg tables_or_models: SQL tables names or models (or combination of both) + :type tables_or_models: tuple of strings or models + :arg cache_alias: Alias from the Django ``CACHES`` setting + :type cache_alias: string or NoneType + :arg db_alias: Alias from the Django ``DATABASES`` setting + :type db_alias: string or NoneType + :returns: The timestamp of the most recent invalidation + :rtype: float + """ + # TODO: Replace with positional arguments when we drop Python 2 support. + cache_alias = kwargs.pop('cache_alias', None) + db_alias = kwargs.pop('db_alias', None) + for k in kwargs: + raise TypeError("get_last_invalidation() got an unexpected " + "keyword argument '%s'" % k) + + last_invalidation = 0.0 + table_cache_keys_per_cache = _get_table_cache_keys_per_cache( + _get_tables(tables_or_models), cache_alias, db_alias) + for cache_alias, table_cache_keys in table_cache_keys_per_cache.items(): + invalidations = cachalot_caches.get_cache( + cache_alias).get_many(table_cache_keys).values() + if invalidations: + current_last_invalidation = max(invalidations) + if current_last_invalidation > last_invalidation: + last_invalidation = current_last_invalidation + return last_invalidation diff --git a/cachalot/management/commands/invalidate_cachalot.py b/cachalot/management/commands/invalidate_cachalot.py index f47b77e9d..4a41ef67f 100644 --- a/cachalot/management/commands/invalidate_cachalot.py +++ b/cachalot/management/commands/invalidate_cachalot.py @@ -40,6 +40,6 @@ def handle(self, *args, **options): cache_str, db_str])) + '...') - invalidate(models, cache_alias=cache_alias, db_alias=db_alias) + invalidate(*models, cache_alias=cache_alias, db_alias=db_alias) if verbosity > 0: self.stdout.write('Cache keys successfully invalidated.') diff --git a/cachalot/monkey_patch.py b/cachalot/monkey_patch.py index 6fa0350e5..530e9c3bf 100644 --- a/cachalot/monkey_patch.py +++ b/cachalot/monkey_patch.py @@ -110,7 +110,7 @@ def inner(cursor, sql, *args, **kwargs): sql = sql.lower() if 'update' in sql or 'insert' in sql or 'delete' in sql: tables = _get_tables_from_sql(cursor.db, sql) - invalidate(tables, db_alias=cursor.db.alias) + invalidate(*tables, db_alias=cursor.db.alias) return out return inner @@ -143,7 +143,7 @@ def inner(self, exc_type, exc_value, traceback): def _invalidate_on_migration(sender, **kwargs): - invalidate(sender.get_models(), db_alias=kwargs['using']) + invalidate(*sender.get_models(), db_alias=kwargs['using']) def patch(): diff --git a/cachalot/tests/api.py b/cachalot/tests/api.py index 37b8bbe7c..1577d5c0d 100644 --- a/cachalot/tests/api.py +++ b/cachalot/tests/api.py @@ -1,6 +1,7 @@ # coding: utf-8 from __future__ import unicode_literals +from time import time, sleep from unittest import skipIf from django.conf import settings @@ -34,7 +35,7 @@ def test_invalidate_tables(self): data2 = list(Test.objects.values_list('name', flat=True)) self.assertListEqual(data2, ['test1']) - invalidate(['cachalot_test']) + invalidate('cachalot_test') with self.assertNumQueries(1): data3 = list(Test.objects.values_list('name', flat=True)) @@ -55,7 +56,7 @@ def test_invalidate_models(self): data2 = list(Test.objects.values_list('name', flat=True)) self.assertListEqual(data2, ['test1']) - invalidate([Test]) + invalidate(Test) with self.assertNumQueries(1): data3 = list(Test.objects.values_list('name', flat=True)) @@ -89,6 +90,23 @@ def test_invalidate_all_in_atomic(self): with self.assertNumQueries(1): Test.objects.get() + def test_get_last_invalidation(self): + invalidate() + timestamp = get_last_invalidation() + self.assertAlmostEqual(timestamp, time(), delta=0.1) + + sleep(0.1) + + invalidate('cachalot_test') + timestamp = get_last_invalidation('cachalot_test') + self.assertAlmostEqual(timestamp, time(), delta=0.1) + + timestamp = get_last_invalidation('cachalot_testparent') + self.assertNotAlmostEqual(timestamp, time(), delta=0.1) + timestamp = get_last_invalidation('cachalot_testparent', + 'cachalot_test') + self.assertAlmostEqual(timestamp, time(), delta=0.1) + class CommandTestCase(TransactionTestCase): multi_db = True diff --git a/docs/api.rst b/docs/api.rst index e403ad8de..ed043e72a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,8 +3,9 @@ API --- -Use these tools if the automatic behaviour of django-cachalot is not enough. -See :ref:`Raw queries limits`. +Use these tools to interact with django-cachalot, especially if you face +:ref:`Raw queries limits` or if you need to create a cache key from the +last table invalidation timestamp. .. automodule:: cachalot.api :members: