Browse Source
feat(cache): implement automatic cache invalidation and smart cache bypass
feat(cache): implement automatic cache invalidation and smart cache bypass
- Add utils/cache.py with invalidate_cache_patterns and smart_cached_view - Support cache bypass on Cache-Control: no-cache headers and query params - Add automatic signals (post_save, post_delete, m2m_changed) for library, video, podcast, article, calendar, and hadis - Hook cache invalidation in custom admin viewset actionsmaster
22 changed files with 309 additions and 104 deletions
-
4apps/article/apps.py
-
39apps/article/signals.py
-
23apps/article/urls.py
-
2apps/article/views_admin.py
-
4apps/dobodbi_calendar/apps.py
-
20apps/dobodbi_calendar/signals.py
-
8apps/dobodbi_calendar/urls.py
-
26apps/hadis/signals.py
-
8apps/hadis/urls.py
-
4apps/library/apps.py
-
27apps/library/signals.py
-
21apps/library/urls.py
-
4apps/library/views_admin.py
-
3apps/podcast/apps.py
-
41apps/podcast/signals.py
-
26apps/podcast/urls.py
-
7apps/podcast/views_admin.py
-
4apps/video/apps.py
-
41apps/video/signals.py
-
23apps/video/urls.py
-
7apps/video/views_admin.py
-
69utils/cache.py
@ -0,0 +1,39 @@ |
|||||
|
from django.db.models.signals import post_save, post_delete, m2m_changed |
||||
|
from django.dispatch import receiver |
||||
|
from utils.cache import invalidate_cache_patterns |
||||
|
from .models import ( |
||||
|
Article, |
||||
|
ArticleCategory, |
||||
|
ArticleCollection, |
||||
|
ArticleContent, |
||||
|
ArticleInCollection, |
||||
|
) |
||||
|
|
||||
|
TARGET_MODELS = [ |
||||
|
ArticleCategory, |
||||
|
ArticleCollection, |
||||
|
Article, |
||||
|
ArticleContent, |
||||
|
ArticleInCollection, |
||||
|
] |
||||
|
|
||||
|
|
||||
|
def invalidate_article_cache(): |
||||
|
""" |
||||
|
Invalidates all Redis cache keys for article API views. |
||||
|
""" |
||||
|
return invalidate_cache_patterns("*article_api*", "*article_list*") |
||||
|
|
||||
|
|
||||
|
@receiver(post_save) |
||||
|
@receiver(post_delete) |
||||
|
def clear_article_cache_on_save_or_delete(sender, instance, **kwargs): |
||||
|
if sender in TARGET_MODELS: |
||||
|
invalidate_article_cache() |
||||
|
|
||||
|
|
||||
|
@receiver(m2m_changed) |
||||
|
def clear_article_cache_on_m2m(sender, instance, action, **kwargs): |
||||
|
if action in ('post_add', 'post_remove', 'post_clear'): |
||||
|
if isinstance(instance, (Article, ArticleCollection, ArticleCategory)): |
||||
|
invalidate_article_cache() |
||||
@ -0,0 +1,20 @@ |
|||||
|
from django.db.models.signals import post_save, post_delete |
||||
|
from django.dispatch import receiver |
||||
|
from utils.cache import invalidate_cache_patterns |
||||
|
from .models import CalendarOccasions |
||||
|
|
||||
|
TARGET_MODELS = [CalendarOccasions] |
||||
|
|
||||
|
|
||||
|
def invalidate_calendar_cache(): |
||||
|
""" |
||||
|
Invalidates all Redis cache keys for calendar API views. |
||||
|
""" |
||||
|
return invalidate_cache_patterns("*calendar_api*") |
||||
|
|
||||
|
|
||||
|
@receiver(post_save) |
||||
|
@receiver(post_delete) |
||||
|
def clear_calendar_cache_on_save_or_delete(sender, instance, **kwargs): |
||||
|
if sender in TARGET_MODELS: |
||||
|
invalidate_calendar_cache() |
||||
@ -0,0 +1,27 @@ |
|||||
|
from django.db.models.signals import post_save, post_delete, m2m_changed |
||||
|
from django.dispatch import receiver |
||||
|
from utils.cache import invalidate_cache_patterns |
||||
|
from .models import Author, Book, BookCollection, Category |
||||
|
|
||||
|
TARGET_MODELS = [Author, Book, BookCollection, Category] |
||||
|
|
||||
|
|
||||
|
def invalidate_library_cache(): |
||||
|
""" |
||||
|
Invalidates all Redis cache keys for library API views. |
||||
|
""" |
||||
|
return invalidate_cache_patterns("*library_api*", "*library_books*") |
||||
|
|
||||
|
|
||||
|
@receiver(post_save) |
||||
|
@receiver(post_delete) |
||||
|
def clear_library_cache_on_save_or_delete(sender, instance, **kwargs): |
||||
|
if sender in TARGET_MODELS: |
||||
|
invalidate_library_cache() |
||||
|
|
||||
|
|
||||
|
@receiver(m2m_changed) |
||||
|
def clear_library_cache_on_m2m(sender, instance, action, **kwargs): |
||||
|
if action in ('post_add', 'post_remove', 'post_clear'): |
||||
|
if isinstance(instance, (Book, BookCollection, Category)): |
||||
|
invalidate_library_cache() |
||||
@ -0,0 +1,41 @@ |
|||||
|
from django.db.models.signals import post_save, post_delete, m2m_changed |
||||
|
from django.dispatch import receiver |
||||
|
from utils.cache import invalidate_cache_patterns |
||||
|
from .models import ( |
||||
|
PlaylistItem, |
||||
|
Podcast, |
||||
|
PodcastCategory, |
||||
|
PodcastCollection, |
||||
|
PodcastPlaylist, |
||||
|
PodcastPlaylistInCollection, |
||||
|
) |
||||
|
|
||||
|
TARGET_MODELS = [ |
||||
|
PodcastCategory, |
||||
|
PodcastCollection, |
||||
|
Podcast, |
||||
|
PodcastPlaylist, |
||||
|
PodcastPlaylistInCollection, |
||||
|
PlaylistItem, |
||||
|
] |
||||
|
|
||||
|
|
||||
|
def invalidate_podcast_cache(): |
||||
|
""" |
||||
|
Invalidates all Redis cache keys for podcast API views. |
||||
|
""" |
||||
|
return invalidate_cache_patterns("*podcast_api*", "*podcast_list*") |
||||
|
|
||||
|
|
||||
|
@receiver(post_save) |
||||
|
@receiver(post_delete) |
||||
|
def clear_podcast_cache_on_save_or_delete(sender, instance, **kwargs): |
||||
|
if sender in TARGET_MODELS: |
||||
|
invalidate_podcast_cache() |
||||
|
|
||||
|
|
||||
|
@receiver(m2m_changed) |
||||
|
def clear_podcast_cache_on_m2m(sender, instance, action, **kwargs): |
||||
|
if action in ('post_add', 'post_remove', 'post_clear'): |
||||
|
if isinstance(instance, (Podcast, PodcastPlaylist, PodcastCollection, PodcastCategory)): |
||||
|
invalidate_podcast_cache() |
||||
@ -0,0 +1,41 @@ |
|||||
|
from django.db.models.signals import post_save, post_delete, m2m_changed |
||||
|
from django.dispatch import receiver |
||||
|
from utils.cache import invalidate_cache_patterns |
||||
|
from .models import ( |
||||
|
PlaylistItem, |
||||
|
Video, |
||||
|
VideoCategory, |
||||
|
VideoCollection, |
||||
|
VideoPlaylist, |
||||
|
VideoPlaylistInCollection, |
||||
|
) |
||||
|
|
||||
|
TARGET_MODELS = [ |
||||
|
VideoCategory, |
||||
|
VideoCollection, |
||||
|
Video, |
||||
|
VideoPlaylist, |
||||
|
VideoPlaylistInCollection, |
||||
|
PlaylistItem, |
||||
|
] |
||||
|
|
||||
|
|
||||
|
def invalidate_video_cache(): |
||||
|
""" |
||||
|
Invalidates all Redis cache keys for video API views. |
||||
|
""" |
||||
|
return invalidate_cache_patterns("*video_api*", "*video_playlists*", "*video_list*") |
||||
|
|
||||
|
|
||||
|
@receiver(post_save) |
||||
|
@receiver(post_delete) |
||||
|
def clear_video_cache_on_save_or_delete(sender, instance, **kwargs): |
||||
|
if sender in TARGET_MODELS: |
||||
|
invalidate_video_cache() |
||||
|
|
||||
|
|
||||
|
@receiver(m2m_changed) |
||||
|
def clear_video_cache_on_m2m(sender, instance, action, **kwargs): |
||||
|
if action in ('post_add', 'post_remove', 'post_clear'): |
||||
|
if isinstance(instance, (Video, VideoPlaylist, VideoCollection, VideoCategory)): |
||||
|
invalidate_video_cache() |
||||
@ -0,0 +1,69 @@ |
|||||
|
from functools import wraps |
||||
|
import logging |
||||
|
from django.core.cache import cache |
||||
|
from django.views.decorators.cache import cache_page |
||||
|
from django.views.decorators.vary import vary_on_headers |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
def invalidate_cache_patterns(*patterns): |
||||
|
""" |
||||
|
Safely invalidates Redis cache keys matching given glob patterns. |
||||
|
Falls back to cache.clear() if the cache backend does not support delete_pattern. |
||||
|
""" |
||||
|
try: |
||||
|
if hasattr(cache, 'delete_pattern'): |
||||
|
total_deleted = 0 |
||||
|
for pattern in patterns: |
||||
|
deleted = cache.delete_pattern(pattern) |
||||
|
total_deleted += deleted or 0 |
||||
|
logger.info(f"Cache cleared for patterns {patterns} (deleted {total_deleted} keys)") |
||||
|
return total_deleted |
||||
|
else: |
||||
|
cache.clear() |
||||
|
logger.info(f"Cache cleared (fallback clear for patterns: {patterns})") |
||||
|
return 1 |
||||
|
except Exception as e: |
||||
|
logger.error(f"Cache invalidation failed for patterns {patterns}: {e}") |
||||
|
return 0 |
||||
|
|
||||
|
|
||||
|
def smart_cached_view(timeout=60 * 60 * 2, key_prefix=''): |
||||
|
""" |
||||
|
Smart cache decorator for API views that: |
||||
|
1. Caches responses per Accept-Language header using Django's cache_page. |
||||
|
2. Bypasses cache when: |
||||
|
- Cache-Control / Pragma: no-cache headers are present (e.g., Pull-to-refresh from mobile / web). |
||||
|
- Query parameters: no_cache=true, refresh=true, is_bookmark=true, in_playlist=true are passed. |
||||
|
""" |
||||
|
def decorator(view_func): |
||||
|
cached_func = cache_page(timeout, key_prefix=key_prefix)( |
||||
|
vary_on_headers('Accept-Language')(view_func) |
||||
|
) |
||||
|
|
||||
|
@wraps(view_func) |
||||
|
def _wrapped_view(request, *args, **kwargs): |
||||
|
# 1. Check HTTP headers for no-cache directives |
||||
|
cache_control = (request.META.get('HTTP_CACHE_CONTROL') or '').lower() |
||||
|
pragma = (request.META.get('HTTP_PRAGMA') or '').lower() |
||||
|
if 'no-cache' in cache_control or 'no-store' in cache_control or 'max-age=0' in cache_control or 'no-cache' in pragma: |
||||
|
return view_func(request, *args, **kwargs) |
||||
|
|
||||
|
# 2. Check query params for explicit bypass |
||||
|
query_params = getattr(request, 'GET', {}) |
||||
|
no_cache_param = (query_params.get('no_cache') or query_params.get('refresh') or '').lower() |
||||
|
if no_cache_param in ('1', 'true'): |
||||
|
return view_func(request, *args, **kwargs) |
||||
|
|
||||
|
# 3. Check bookmark & playlist specific bypasses |
||||
|
is_bookmark = (query_params.get('is_bookmark') or query_params.get('is_bookmarked') or '').lower() |
||||
|
in_playlist = (query_params.get('in_playlist') or '').lower() |
||||
|
if is_bookmark in ('1', 'true') or in_playlist in ('1', 'true'): |
||||
|
return view_func(request, *args, **kwargs) |
||||
|
|
||||
|
return cached_func(request, *args, **kwargs) |
||||
|
|
||||
|
return _wrapped_view |
||||
|
|
||||
|
return decorator |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue