django-cachalot/cachalot/api.py
2023-04-04 15:28:15 -04:00

155 lines
5.9 KiB
Python

from contextlib import contextmanager
from typing import Any, Optional, Tuple, Union
from django.apps import apps
from django.conf import settings
from django.db import connections
from .cache import cachalot_caches
from .settings import cachalot_settings
from .signals import post_invalidation
from .transaction import AtomicCache
from .utils import _invalidate_tables
try:
from asgiref.local import Local
LOCAL_STORAGE = Local()
except ImportError:
import threading
LOCAL_STORAGE = threading.local()
__all__ = ('invalidate', 'get_last_invalidation', 'cachalot_disabled')
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()
if tables:
for cache_alias in cache_aliases:
yield cache_alias, db_alias, tables
def _get_tables(tables_or_models):
for table_or_model in tables_or_models:
if isinstance(table_or_model, str) and '.' in table_or_model:
try:
table_or_model = apps.get_model(table_or_model)
except LookupError:
pass
yield (table_or_model if isinstance(table_or_model, str)
else table_or_model._meta.db_table)
def invalidate(
*tables_or_models: Tuple[Union[str, Any], ...],
cache_alias: Optional[str] = None,
db_alias: Optional[str] = None,
) -> None:
"""
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
(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.
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, models or models lookups
(or a combination)
:type tables_or_models: tuple of strings or models
:arg cache_alias: Alias from the Django ``CACHES`` setting
:arg db_alias: Alias from the Django ``DATABASES`` setting
:returns: Nothing
"""
send_signal = False
invalidated = set()
for cache_alias, db_alias, tables in _cache_db_tables_iterator(
list(_get_tables(tables_or_models)), cache_alias, db_alias):
cache = cachalot_caches.get_cache(cache_alias, db_alias)
if not isinstance(cache, AtomicCache):
send_signal = True
_invalidate_tables(cache, db_alias, tables)
invalidated.update(tables)
if send_signal:
for table in invalidated:
post_invalidation.send(table, db_alias=db_alias)
def get_last_invalidation(
*tables_or_models: Tuple[Union[str, Any], ...],
cache_alias: Optional[str] = None,
db_alias: Optional[str] = None,
) -> float:
"""
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 fetches invalidations
in this cache, otherwise invalidations in all caches are fetched.
If ``db_alias`` is specified, it only fetches invalidations
for this database, otherwise invalidations for all databases are fetched.
:arg tables_or_models: SQL tables names, models or models lookups
(or a combination)
:type tables_or_models: tuple of strings or models
:arg cache_alias: Alias from the Django ``CACHES`` setting
:arg db_alias: Alias from the Django ``DATABASES`` setting
:returns: The timestamp of the most recent invalidation
"""
last_invalidation = 0.0
for cache_alias, db_alias, tables in _cache_db_tables_iterator(
list(_get_tables(tables_or_models)), cache_alias, db_alias):
get_table_cache_key = cachalot_settings.CACHALOT_TABLE_KEYGEN
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:
current_last_invalidation = max(invalidations)
if current_last_invalidation > last_invalidation:
last_invalidation = current_last_invalidation
return last_invalidation
@contextmanager
def cachalot_disabled(all_queries: bool = False):
"""
Context manager for temporarily disabling cachalot.
If you evaluate the same queryset a second time,
like normally for Django querysets, this will access
the variable that saved it in-memory. For example:
.. code-block:: python
with cachalot_disabled():
qs = Test.objects.filter(blah=blah)
# Does a single query to the db
list(qs) # Evaluates queryset
# Because the qs was evaluated, it's
# saved in memory:
list(qs) # this does 0 queries.
# This does 1 query to the db
list(Test.objects.filter(blah=blah))
If you evaluate the queryset outside the context manager, any duplicate
query will use the cached result unless an object creation happens in between
the original and duplicate query.
:arg all_queries: Any query, including already evaluated queries, are re-evaluated.
"""
was_enabled = getattr(LOCAL_STORAGE, "cachalot_enabled", cachalot_settings.CACHALOT_ENABLED)
LOCAL_STORAGE.cachalot_enabled = False
LOCAL_STORAGE.disable_on_all = all_queries
yield
LOCAL_STORAGE.cachalot_enabled = was_enabled