From 7daec1c2e23635c33779bb6d1eba3000e2f183f2 Mon Sep 17 00:00:00 2001 From: Ryth Azhur Date: Mon, 6 Apr 2026 23:09:22 -0400 Subject: [PATCH] Fix remaining cache issues: index TTL, sub-index cleanup, top-level imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../mizan-django/src/mizan/cache/__init__.py | 3 +++ .../mizan-django/src/mizan/cache/backend.py | 20 +++++++++++++++++++ .../mizan-django/src/mizan/client/executor.py | 7 ++----- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/mizan-django/src/mizan/cache/__init__.py b/packages/mizan-django/src/mizan/cache/__init__.py index 2f5292b..b06a364 100644 --- a/packages/mizan-django/src/mizan/cache/__init__.py +++ b/packages/mizan-django/src/mizan/cache/__init__.py @@ -172,6 +172,9 @@ def cache_purge( keys_to_delete = backend.get_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: return backend.delete_many(list(keys_to_delete)) return 0 diff --git a/packages/mizan-django/src/mizan/cache/backend.py b/packages/mizan-django/src/mizan/cache/backend.py index 9b097ae..d531b9c 100644 --- a/packages/mizan-django/src/mizan/cache/backend.py +++ b/packages/mizan-django/src/mizan/cache/backend.py @@ -20,6 +20,7 @@ class CacheBackend(Protocol): def get_index(self, index_key: str) -> set[str]: ... def remove_from_index(self, index_key: str, members: set[str]) -> None: ... def delete_index(self, index_key: str) -> None: ... + def delete_indexes_by_prefix(self, prefix: str) -> None: ... def clear(self) -> None: ... @@ -65,6 +66,11 @@ class MemoryCache: def delete_index(self, index_key: str) -> 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: self._store.clear() self._indexes.clear() @@ -116,6 +122,7 @@ class RedisCache: pipe.set(prefixed_key, value, ex=self._ttl) for idx in indexes: pipe.sadd(self._key(idx), key) + pipe.expire(self._key(idx), self._ttl) pipe.execute() def delete_many(self, keys: list[str]) -> int: @@ -135,6 +142,19 @@ class RedisCache: def delete_index(self, index_key: str) -> None: 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: pattern = f"{self._prefix}*" cursor = 0 diff --git a/packages/mizan-django/src/mizan/client/executor.py b/packages/mizan-django/src/mizan/client/executor.py index 1ec31ba..fe2e94c 100644 --- a/packages/mizan-django/src/mizan/client/executor.py +++ b/packages/mizan-django/src/mizan/client/executor.py @@ -27,7 +27,9 @@ from django.http import HttpRequest, HttpResponse, JsonResponse from django.views.decorators.csrf import csrf_protect 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.settings import get_settings if TYPE_CHECKING: pass @@ -662,8 +664,6 @@ def function_call_view(request: HttpRequest) -> JsonResponse: response["X-Mizan-Invalidate"] = _format_invalidate_header(invalidate_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 = get_cache() if cache is not None: @@ -776,9 +776,6 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse: params = request.GET.dict() # 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 = get_cache() cache_settings = get_settings()