Fix remaining cache issues: index TTL, sub-index cleanup, top-level imports
- RedisCache.put: add pipe.expire() on index sets matching entry TTL, prevents orphaned index entries when cache values expire - Broad purge: delete_indexes_by_prefix() cleans per-param sub-indexes (mizan:idx:ctx:k=v) that previously leaked as dead sets - Move cache imports to top of executor.py (were inline in view functions) - Update KNOWN_ISSUES.md — all 16 issues now resolved or documented Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -172,6 +172,9 @@ def cache_purge(
|
|||||||
keys_to_delete = backend.get_index(index_key)
|
keys_to_delete = backend.get_index(index_key)
|
||||||
backend.delete_index(index_key)
|
backend.delete_index(index_key)
|
||||||
|
|
||||||
|
# Clean up per-param sub-indexes (e.g., mizan:idx:user:user_id=5)
|
||||||
|
backend.delete_indexes_by_prefix(f"mizan:idx:{context}:")
|
||||||
|
|
||||||
if keys_to_delete:
|
if keys_to_delete:
|
||||||
return backend.delete_many(list(keys_to_delete))
|
return backend.delete_many(list(keys_to_delete))
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
20
packages/mizan-django/src/mizan/cache/backend.py
vendored
20
packages/mizan-django/src/mizan/cache/backend.py
vendored
@@ -20,6 +20,7 @@ class CacheBackend(Protocol):
|
|||||||
def get_index(self, index_key: str) -> set[str]: ...
|
def get_index(self, index_key: str) -> set[str]: ...
|
||||||
def remove_from_index(self, index_key: str, members: set[str]) -> None: ...
|
def remove_from_index(self, index_key: str, members: set[str]) -> None: ...
|
||||||
def delete_index(self, index_key: str) -> None: ...
|
def delete_index(self, index_key: str) -> None: ...
|
||||||
|
def delete_indexes_by_prefix(self, prefix: str) -> None: ...
|
||||||
def clear(self) -> None: ...
|
def clear(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
@@ -65,6 +66,11 @@ class MemoryCache:
|
|||||||
def delete_index(self, index_key: str) -> None:
|
def delete_index(self, index_key: str) -> None:
|
||||||
self._indexes.pop(index_key, None)
|
self._indexes.pop(index_key, None)
|
||||||
|
|
||||||
|
def delete_indexes_by_prefix(self, prefix: str) -> None:
|
||||||
|
to_delete = [k for k in self._indexes if k.startswith(prefix)]
|
||||||
|
for k in to_delete:
|
||||||
|
del self._indexes[k]
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
self._store.clear()
|
self._store.clear()
|
||||||
self._indexes.clear()
|
self._indexes.clear()
|
||||||
@@ -116,6 +122,7 @@ class RedisCache:
|
|||||||
pipe.set(prefixed_key, value, ex=self._ttl)
|
pipe.set(prefixed_key, value, ex=self._ttl)
|
||||||
for idx in indexes:
|
for idx in indexes:
|
||||||
pipe.sadd(self._key(idx), key)
|
pipe.sadd(self._key(idx), key)
|
||||||
|
pipe.expire(self._key(idx), self._ttl)
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
def delete_many(self, keys: list[str]) -> int:
|
def delete_many(self, keys: list[str]) -> int:
|
||||||
@@ -135,6 +142,19 @@ class RedisCache:
|
|||||||
def delete_index(self, index_key: str) -> None:
|
def delete_index(self, index_key: str) -> None:
|
||||||
self._client.delete(self._key(index_key))
|
self._client.delete(self._key(index_key))
|
||||||
|
|
||||||
|
def delete_indexes_by_prefix(self, prefix: str) -> None:
|
||||||
|
pattern = f"{self._prefix}{prefix}*"
|
||||||
|
cursor = 0
|
||||||
|
while True:
|
||||||
|
cursor, keys = self._client.scan(cursor, match=pattern, count=100)
|
||||||
|
if keys:
|
||||||
|
pipe = self._client.pipeline()
|
||||||
|
for key in keys:
|
||||||
|
pipe.unlink(key)
|
||||||
|
pipe.execute()
|
||||||
|
if cursor == 0:
|
||||||
|
break
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
pattern = f"{self._prefix}*"
|
pattern = f"{self._prefix}*"
|
||||||
cursor = 0
|
cursor = 0
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ from django.http import HttpRequest, HttpResponse, JsonResponse
|
|||||||
from django.views.decorators.csrf import csrf_protect
|
from django.views.decorators.csrf import csrf_protect
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
|
from mizan.cache import get_cache, cache_get, cache_put, cache_purge
|
||||||
from mizan.setup.registry import get_function, get_context_groups
|
from mizan.setup.registry import get_function, get_context_groups
|
||||||
|
from mizan.setup.settings import get_settings
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pass
|
pass
|
||||||
@@ -662,8 +664,6 @@ def function_call_view(request: HttpRequest) -> JsonResponse:
|
|||||||
response["X-Mizan-Invalidate"] = _format_invalidate_header(invalidate_contexts)
|
response["X-Mizan-Invalidate"] = _format_invalidate_header(invalidate_contexts)
|
||||||
|
|
||||||
# Purge origin-side cache for invalidated contexts
|
# Purge origin-side cache for invalidated contexts
|
||||||
import logging
|
|
||||||
from mizan.cache import get_cache, cache_purge
|
|
||||||
_cache_log = logging.getLogger("mizan.cache")
|
_cache_log = logging.getLogger("mizan.cache")
|
||||||
cache = get_cache()
|
cache = get_cache()
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
@@ -776,9 +776,6 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse:
|
|||||||
params = request.GET.dict()
|
params = request.GET.dict()
|
||||||
|
|
||||||
# Origin-side cache lookup
|
# Origin-side cache lookup
|
||||||
import logging
|
|
||||||
from mizan.cache import get_cache, cache_get, cache_put
|
|
||||||
from mizan.setup.settings import get_settings
|
|
||||||
_cache_log = logging.getLogger("mizan.cache")
|
_cache_log = logging.getLogger("mizan.cache")
|
||||||
cache = get_cache()
|
cache = get_cache()
|
||||||
cache_settings = get_settings()
|
cache_settings = get_settings()
|
||||||
|
|||||||
Reference in New Issue
Block a user