2014-09-26 14:53:44 +00:00
|
|
|
|
# coding: utf-8
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import unicode_literals
|
2014-10-04 23:25:48 +00:00
|
|
|
|
from collections import defaultdict, Iterable
|
2014-10-05 22:58:39 +00:00
|
|
|
|
from functools import wraps
|
2014-10-06 01:01:46 +00:00
|
|
|
|
from hashlib import md5
|
2014-10-06 11:45:57 +00:00
|
|
|
|
import pickle
|
2014-09-27 23:44:43 +00:00
|
|
|
|
import re
|
|
|
|
|
|
|
2014-10-04 23:25:48 +00:00
|
|
|
|
from django.conf import settings
|
|
|
|
|
|
# TODO: Replace with caches[CACHALOT_CACHE] when we drop Django 1.6 support.
|
2014-10-04 10:59:45 +00:00
|
|
|
|
from django.core.cache import get_cache as get_django_cache
|
2014-09-29 17:54:08 +00:00
|
|
|
|
from django.db import connection
|
2014-09-26 14:53:44 +00:00
|
|
|
|
from django.db.models.query import EmptyResultSet
|
|
|
|
|
|
from django.db.models.sql.compiler import (
|
|
|
|
|
|
SQLCompiler, SQLAggregateCompiler, SQLDateCompiler, SQLDateTimeCompiler,
|
|
|
|
|
|
SQLInsertCompiler, SQLUpdateCompiler, SQLDeleteCompiler)
|
2014-09-27 23:44:43 +00:00
|
|
|
|
from django.db.models.sql.where import ExtraWhere
|
2014-09-29 17:16:53 +00:00
|
|
|
|
from django.db.transaction import Atomic
|
2014-10-04 23:25:48 +00:00
|
|
|
|
|
|
|
|
|
|
from .settings import cachalot_settings
|
2014-09-26 14:53:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
COMPILERS = (SQLCompiler,
|
|
|
|
|
|
SQLAggregateCompiler, SQLDateCompiler, SQLDateTimeCompiler,
|
|
|
|
|
|
SQLInsertCompiler, SQLUpdateCompiler, SQLDeleteCompiler)
|
|
|
|
|
|
WRITE_COMPILERS = (SQLInsertCompiler, SQLUpdateCompiler, SQLDeleteCompiler)
|
|
|
|
|
|
READ_COMPILERS = [c for c in COMPILERS if c not in WRITE_COMPILERS]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PATCHED = False
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-10-06 10:34:51 +00:00
|
|
|
|
def hash_cache_key(unicode_key):
|
|
|
|
|
|
return md5(unicode_key.encode('utf-8')).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-10-06 01:01:46 +00:00
|
|
|
|
def _get_query_cache_key(compiler):
|
2014-10-06 10:34:51 +00:00
|
|
|
|
return hash_cache_key('%s:%s' % compiler.as_sql())
|
2014-10-06 01:01:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
2014-09-27 23:44:43 +00:00
|
|
|
|
def _get_tables(query):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Returns a ``set`` of all database table names used by ``query``.
|
|
|
|
|
|
"""
|
2014-09-28 17:36:49 +00:00
|
|
|
|
tables = set(query.tables)
|
2014-09-27 23:44:43 +00:00
|
|
|
|
tables.add(query.model._meta.db_table)
|
|
|
|
|
|
return tables
|
|
|
|
|
|
|
2014-09-26 14:53:44 +00:00
|
|
|
|
|
2014-10-04 23:25:48 +00:00
|
|
|
|
def _get_table_cache_key(table):
|
2014-10-06 10:34:51 +00:00
|
|
|
|
return hash_cache_key('%s_queries' % table)
|
2014-10-04 23:25:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
2014-09-27 23:44:43 +00:00
|
|
|
|
def _get_tables_cache_keys(query):
|
2014-10-04 23:41:24 +00:00
|
|
|
|
return [_get_table_cache_key(t) for t in _get_tables(query)]
|
2014-09-26 14:53:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
2014-09-29 17:16:53 +00:00
|
|
|
|
def _update_tables_queries(cache, query, cache_key):
|
2014-09-27 23:44:43 +00:00
|
|
|
|
tables_cache_keys = _get_tables_cache_keys(query)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
tables_queries = cache.get_many(tables_cache_keys)
|
|
|
|
|
|
for k in tables_cache_keys:
|
|
|
|
|
|
queries = tables_queries.get(k, [])
|
|
|
|
|
|
queries.append(cache_key)
|
|
|
|
|
|
tables_queries[k] = queries
|
|
|
|
|
|
cache.set_many(tables_queries)
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-10-04 23:25:48 +00:00
|
|
|
|
def _invalidate_tables_cache_keys(cache, tables_cache_keys):
|
2014-09-26 14:53:44 +00:00
|
|
|
|
tables_queries = cache.get_many(tables_cache_keys)
|
2014-10-04 22:59:05 +00:00
|
|
|
|
queries = [q for k in tables_cache_keys for q in tables_queries.get(k, [])]
|
|
|
|
|
|
cache.delete_many(queries + tables_cache_keys)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
2014-10-04 23:25:48 +00:00
|
|
|
|
def _invalidate_tables(cache, query):
|
|
|
|
|
|
tables_cache_keys = _get_tables_cache_keys(query)
|
|
|
|
|
|
_invalidate_tables_cache_keys(cache, tables_cache_keys)
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-10-05 01:20:29 +00:00
|
|
|
|
def clear_cache(cache):
|
2014-10-04 23:25:48 +00:00
|
|
|
|
tables = connection.introspection.table_names()
|
2014-10-05 22:58:39 +00:00
|
|
|
|
tables_cache_keys = [_get_table_cache_key(t) for t in tables]
|
2014-10-04 23:25:48 +00:00
|
|
|
|
_invalidate_tables_cache_keys(cache, tables_cache_keys)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_all_caches():
|
|
|
|
|
|
for cache in settings.CACHES:
|
|
|
|
|
|
clear_cache(get_django_cache(cache))
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-09-27 23:44:43 +00:00
|
|
|
|
COLUMN_RE = re.compile(r'^"(?P<table>[\w_]+)"\."(?P<column>[\w_]+)"$')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _has_extra_select_or_where(query):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Returns True if ``query`` contains a ``QuerySet.extra`` with ``select``
|
|
|
|
|
|
or ``where`` arguments.
|
|
|
|
|
|
|
|
|
|
|
|
We also have to check for ``prefetch_related``, as it internally uses a
|
|
|
|
|
|
``QuerySet.extra(select={'_prefetch_related_val_…': '"table"."column"'})``.
|
|
|
|
|
|
|
|
|
|
|
|
For more details on how prefetch_related uses ``QuerySet.extra``, see:
|
|
|
|
|
|
https://github.com/django/django/blob/1.6.7/django/db/models/fields/related.py#L553-L577
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# Checks if there’s an extra where
|
|
|
|
|
|
if any(isinstance(child, ExtraWhere) for child in query.where.children):
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
# Checks if there’s an extra select
|
|
|
|
|
|
if query.extra_select and query.extra_select_mask is None:
|
|
|
|
|
|
tables = _get_tables(query)
|
|
|
|
|
|
# Checks if extra subqueries are something else than one or several
|
|
|
|
|
|
# ``prefetch_related``.
|
|
|
|
|
|
for subquery, params in query.extra_select.values():
|
|
|
|
|
|
match = COLUMN_RE.match(subquery)
|
|
|
|
|
|
return match is None or match.group('table') not in tables
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-10-04 23:25:48 +00:00
|
|
|
|
NESTED_CACHES = defaultdict(list)
|
2014-09-29 17:16:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AtomicCache(dict):
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
super(AtomicCache, self).__init__()
|
2014-10-04 10:59:45 +00:00
|
|
|
|
self.parent_cache = get_cache()
|
2014-09-29 17:16:53 +00:00
|
|
|
|
self.to_be_deleted = set()
|
|
|
|
|
|
|
|
|
|
|
|
def get(self, k, default=None):
|
|
|
|
|
|
if k in self.to_be_deleted:
|
|
|
|
|
|
return default
|
|
|
|
|
|
if k in self:
|
|
|
|
|
|
return self[k]
|
|
|
|
|
|
return self.parent_cache.get(k, default)
|
|
|
|
|
|
|
|
|
|
|
|
def set(self, k, v):
|
|
|
|
|
|
if k in self.to_be_deleted:
|
|
|
|
|
|
self.to_be_deleted.remove(k)
|
|
|
|
|
|
self[k] = v
|
|
|
|
|
|
|
|
|
|
|
|
def delete(self, k):
|
|
|
|
|
|
self.to_be_deleted.add(k)
|
|
|
|
|
|
|
|
|
|
|
|
def get_many(self, keys):
|
|
|
|
|
|
data = {}
|
|
|
|
|
|
for k in keys:
|
2014-10-07 14:08:23 +00:00
|
|
|
|
v = self.get(k)
|
|
|
|
|
|
if v is not None:
|
2014-09-29 17:16:53 +00:00
|
|
|
|
data[k] = v
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
def set_many(self, data):
|
|
|
|
|
|
self.to_be_deleted.difference_update(set(data))
|
|
|
|
|
|
self.update(data)
|
|
|
|
|
|
|
|
|
|
|
|
def delete_many(self, keys):
|
|
|
|
|
|
self.to_be_deleted.update(keys)
|
|
|
|
|
|
|
|
|
|
|
|
def commit(self):
|
|
|
|
|
|
self.parent_cache.set_many(self)
|
|
|
|
|
|
self.parent_cache.delete_many(self.to_be_deleted)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_cache():
|
2014-10-04 23:25:48 +00:00
|
|
|
|
cache_name = cachalot_settings.CACHALOT_CACHE
|
|
|
|
|
|
nested_caches = NESTED_CACHES[cache_name]
|
|
|
|
|
|
if nested_caches:
|
|
|
|
|
|
return nested_caches[-1]
|
|
|
|
|
|
return get_django_cache(cache_name)
|
2014-09-29 17:16:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _patch_orm_read():
|
|
|
|
|
|
def patch_execute_sql(original):
|
2014-10-05 22:58:39 +00:00
|
|
|
|
@wraps(original)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
def inner(compiler, *args, **kwargs):
|
2014-10-05 00:36:19 +00:00
|
|
|
|
if not cachalot_settings.CACHALOT_ENABLED \
|
|
|
|
|
|
or isinstance(compiler, WRITE_COMPILERS):
|
2014-09-29 17:16:53 +00:00
|
|
|
|
return original(compiler, *args, **kwargs)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
|
2014-09-27 23:44:43 +00:00
|
|
|
|
query = compiler.query
|
|
|
|
|
|
|
|
|
|
|
|
if _has_extra_select_or_where(query):
|
2014-09-29 17:16:53 +00:00
|
|
|
|
return original(compiler, *args, **kwargs)
|
2014-09-27 23:44:43 +00:00
|
|
|
|
|
2014-09-26 14:53:44 +00:00
|
|
|
|
try:
|
2014-10-06 01:01:46 +00:00
|
|
|
|
cache_key = _get_query_cache_key(compiler)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
except EmptyResultSet:
|
2014-09-29 17:16:53 +00:00
|
|
|
|
return original(compiler, *args, **kwargs)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
|
2014-09-29 17:16:53 +00:00
|
|
|
|
cache = get_cache()
|
2014-10-07 14:08:23 +00:00
|
|
|
|
result = cache.get(cache_key)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
|
2014-10-07 14:08:23 +00:00
|
|
|
|
if result is None:
|
2014-09-29 17:16:53 +00:00
|
|
|
|
result = original(compiler, *args, **kwargs)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
if isinstance(result, Iterable) \
|
|
|
|
|
|
and not isinstance(result, (tuple, list)):
|
|
|
|
|
|
result = list(result)
|
|
|
|
|
|
|
2014-09-29 17:16:53 +00:00
|
|
|
|
_update_tables_queries(cache, query, cache_key)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
|
2014-10-06 11:45:57 +00:00
|
|
|
|
cache.set(cache_key, pickle.dumps(result))
|
|
|
|
|
|
else:
|
|
|
|
|
|
result = pickle.loads(result)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
2014-09-29 17:16:53 +00:00
|
|
|
|
inner.original = original
|
2014-09-26 14:53:44 +00:00
|
|
|
|
return inner
|
|
|
|
|
|
|
|
|
|
|
|
for compiler in READ_COMPILERS:
|
|
|
|
|
|
compiler.execute_sql = patch_execute_sql(compiler.execute_sql)
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-09-29 17:16:53 +00:00
|
|
|
|
def _patch_orm_write():
|
|
|
|
|
|
def patch_execute_sql(original):
|
2014-10-05 22:58:39 +00:00
|
|
|
|
@wraps(original)
|
2014-09-26 14:53:44 +00:00
|
|
|
|
def inner(compiler, *args, **kwargs):
|
2014-09-29 17:16:53 +00:00
|
|
|
|
_invalidate_tables(get_cache(), compiler.query)
|
|
|
|
|
|
return original(compiler, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
inner.original = original
|
2014-09-26 14:53:44 +00:00
|
|
|
|
return inner
|
|
|
|
|
|
|
|
|
|
|
|
for compiler in WRITE_COMPILERS:
|
|
|
|
|
|
compiler.execute_sql = patch_execute_sql(compiler.execute_sql)
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-09-29 17:16:53 +00:00
|
|
|
|
def _patch_atomic():
|
|
|
|
|
|
def patch_enter(original):
|
2014-10-05 22:58:39 +00:00
|
|
|
|
@wraps(original)
|
2014-09-29 17:16:53 +00:00
|
|
|
|
def inner(self):
|
2014-10-04 23:25:48 +00:00
|
|
|
|
nested_caches = NESTED_CACHES[cachalot_settings.CACHALOT_CACHE]
|
|
|
|
|
|
nested_caches.append(AtomicCache())
|
2014-09-29 17:16:53 +00:00
|
|
|
|
original(self)
|
|
|
|
|
|
|
|
|
|
|
|
inner.original = original
|
|
|
|
|
|
return inner
|
|
|
|
|
|
|
|
|
|
|
|
def patch_exit(original):
|
2014-10-05 22:58:39 +00:00
|
|
|
|
@wraps(original)
|
2014-09-29 17:16:53 +00:00
|
|
|
|
def inner(self, exc_type, exc_value, traceback):
|
2014-10-13 18:57:36 +00:00
|
|
|
|
needs_rollback = connection.needs_rollback
|
|
|
|
|
|
|
|
|
|
|
|
original(self, exc_type, exc_value, traceback)
|
|
|
|
|
|
|
2014-10-04 23:25:48 +00:00
|
|
|
|
nested_caches = NESTED_CACHES[cachalot_settings.CACHALOT_CACHE]
|
|
|
|
|
|
atomic_cache = nested_caches.pop()
|
2014-10-13 18:57:36 +00:00
|
|
|
|
if exc_type is None and not needs_rollback:
|
2014-09-29 17:16:53 +00:00
|
|
|
|
atomic_cache.commit()
|
|
|
|
|
|
|
|
|
|
|
|
inner.original = original
|
|
|
|
|
|
return inner
|
|
|
|
|
|
|
|
|
|
|
|
Atomic.__enter__ = patch_enter(Atomic.__enter__)
|
|
|
|
|
|
Atomic.__exit__ = patch_exit(Atomic.__exit__)
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-09-29 20:03:05 +00:00
|
|
|
|
def _patch_test_db():
|
2014-10-04 23:46:24 +00:00
|
|
|
|
def patch_creation(original):
|
2014-10-05 22:58:39 +00:00
|
|
|
|
@wraps(original)
|
2014-09-29 20:03:05 +00:00
|
|
|
|
def inner(*args, **kwargs):
|
2014-10-04 23:25:48 +00:00
|
|
|
|
out = original(*args, **kwargs)
|
|
|
|
|
|
clear_all_caches()
|
|
|
|
|
|
return out
|
2014-09-29 20:03:05 +00:00
|
|
|
|
|
|
|
|
|
|
inner.original = original
|
|
|
|
|
|
return inner
|
|
|
|
|
|
|
2014-10-04 23:46:24 +00:00
|
|
|
|
def patch_destruction(original):
|
2014-10-05 22:58:39 +00:00
|
|
|
|
@wraps(original)
|
2014-10-04 23:46:24 +00:00
|
|
|
|
def inner(*args, **kwargs):
|
|
|
|
|
|
clear_all_caches()
|
|
|
|
|
|
return original(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
inner.original = original
|
|
|
|
|
|
return inner
|
|
|
|
|
|
|
2014-09-29 20:03:05 +00:00
|
|
|
|
creation = connection.creation
|
2014-10-04 23:46:24 +00:00
|
|
|
|
creation.create_test_db = patch_creation(creation.create_test_db)
|
|
|
|
|
|
creation.destroy_test_db = patch_destruction(creation.destroy_test_db)
|
2014-09-29 20:03:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
2014-09-29 17:16:53 +00:00
|
|
|
|
def patch():
|
2014-09-26 14:53:44 +00:00
|
|
|
|
global PATCHED
|
2014-09-29 20:03:05 +00:00
|
|
|
|
_patch_test_db()
|
2014-09-29 17:16:53 +00:00
|
|
|
|
_patch_orm_write()
|
|
|
|
|
|
_patch_orm_read()
|
|
|
|
|
|
_patch_atomic()
|
2014-09-26 14:53:44 +00:00
|
|
|
|
PATCHED = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_patched():
|
|
|
|
|
|
return PATCHED
|