From 14c111cbbdd2f02fcf6232e964f924d8ff78d7d9 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Fri, 3 Jul 2026 16:38:48 +0300 Subject: [PATCH 01/13] Create CacheKeyService base methods. --- app/core/config.py | 1 + app/core/redis/cache_keys_service.py | 86 ++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 app/core/redis/cache_keys_service.py diff --git a/app/core/config.py b/app/core/config.py index 8e47191..7f26647 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -127,6 +127,7 @@ class NotificationServiceConfig(BaseModel): class Settings(BaseSettings): + service_name: str = "movie-catalog" base_dir: Path = Path(__file__).parent.parent database: DataBaseConfig = DataBaseConfig() redis: RedisConfig = RedisConfig() diff --git a/app/core/redis/cache_keys_service.py b/app/core/redis/cache_keys_service.py new file mode 100644 index 0000000..0ca2916 --- /dev/null +++ b/app/core/redis/cache_keys_service.py @@ -0,0 +1,86 @@ +from core.config import settings +from core.constants import PrimitiveType +from core.redis import RedisService + + +class CacheKeyService: + list_field = "list" + + def __init__(self, redis: RedisService) -> None: + self.redis = redis + + async def __build( + self, + entity: str, + action: str, + entity_id: int | None = None, + **params: PrimitiveType, + ) -> str: + if entity_id is None: + version = await self.__get_version(entity) + if version is None: + await self.__init_version(entity) + key = f"{settings.service_name}:{entity}:{self.list_field}:version:{version}:{action}" + else: + key = f"{settings.service_name}:{entity}:{entity_id}:{action}" + + for parameter_name, value in params.items(): + key = f"{key}:{parameter_name}:{value}" + + return key + + async def __get_version(self, entity: str) -> int: + key = self.__create_version_entity_key(entity) + version = await self.redis.get(key, is_integer=True) + if not await self.redis.exists(key): + raise ValueError + return version + + async def __update_version(self, entity: str) -> None: + key = self.__create_version_entity_key(entity) + await self.redis.incr_by(key, amount=1) + + async def __init_version(self, entity: str) -> None: + key = self.__create_version_entity_key(entity) + await self.redis.set( + key=key, + value=1, + ) + + @classmethod + def __create_version_entity_key(cls, entity: str) -> str: + return f"{settings.service_name}:{cls.list_field}:{entity}:version" + + async def invalidate_list_keys( + self, + entity: str, + ) -> None: + await self.__update_version( + entity=entity, + ) + + async def build_item_key( + self, + entity: str, + action: str, + entity_id: int, + **params: PrimitiveType, + ) -> str: + return await self.__build( + entity=entity, + entity_id=entity_id, + action=action, + **params, + ) + + async def build_list_key( + self, + entity: str, + action: str, + **params: PrimitiveType, + ) -> str: + return await self.__build( + entity=entity, + action=action, + **params, + ) From e0bac04eaa25dbbb94a13a6e194d3c773ba4660d Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Mon, 6 Jul 2026 16:03:17 +0300 Subject: [PATCH 02/13] Rework genre caching: use cache versioning. --- app/api/api_v1/genres/list_views.py | 4 +- app/cache_services/genre.py | 82 ++++++++++++------- ...e_keys_service.py => cache_key_service.py} | 67 ++++++--------- app/core/redis/client.py | 7 +- app/core/redis/service.py | 10 ++- app/core/utils.py | 10 ++- app/dependencies/cache_services.py | 8 +- app/services/genre.py | 4 +- 8 files changed, 113 insertions(+), 79 deletions(-) rename app/core/redis/{cache_keys_service.py => cache_key_service.py} (52%) diff --git a/app/api/api_v1/genres/list_views.py b/app/api/api_v1/genres/list_views.py index b0ccf50..4b386db 100644 --- a/app/api/api_v1/genres/list_views.py +++ b/app/api/api_v1/genres/list_views.py @@ -36,12 +36,12 @@ async def get_genres( ], ) async def search_genres_by_name( - genre_name: str, + search_query: str, genre_cache_service: GenreCacheServiceDep, size: PaginationSizeDep = 10, page: PaginationPageDep = 1, ) -> GenreResponseList: - return await genre_cache_service.search_genres_by_name(genre_name, size, page) + return await genre_cache_service.search_genres_by_name(search_query, size, page) @router.post( diff --git a/app/cache_services/genre.py b/app/cache_services/genre.py index 9c2e63a..ca60f05 100644 --- a/app/cache_services/genre.py +++ b/app/cache_services/genre.py @@ -1,5 +1,7 @@ +import asyncio from typing import cast +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.genre import ( GenreCreate, @@ -15,19 +17,25 @@ class GenreCacheService: def __init__( self, genre_service: GenreService, - cache_service: RedisService, + redis_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.genre_service = genre_service - self.cache_service = cache_service + self.redis_service = redis_service + self.cache_key_service = cache_key_service async def get_genre_by_id(self, genre_id: int) -> GenreResponse: - key = RedisService.create_cache_key("genre", genre_id=genre_id) - cached_genre_response = await self.cache_service.get(key, GenreResponse) + key = self.cache_key_service.build_item_key( + entity="genre", + entity_id=genre_id, + action="get", + ) + cached_genre_response = await self.redis_service.get(key, GenreResponse) if cached_genre_response is not None: return cast(GenreResponse, cached_genre_response) genre_response = await self.genre_service.get_genre_by_id(genre_id) - await self.cache_service.set(key, genre_response, ttl=1800) + await self.redis_service.set(key, genre_response, ttl=24 * 60 * 60) return genre_response async def get_all_genres( @@ -35,48 +43,48 @@ async def get_all_genres( size: int = 10, page: int = 1, ) -> GenreResponseList: - key = RedisService.create_cache_key( - "genres", + key = await self.cache_key_service.build_list_key( + entity="genre", + action="get", size=size, page=page, ) - cached_genres_response = await self.cache_service.get(key, GenreResponseList) + cached_genres_response = await self.redis_service.get(key, GenreResponseList) if cached_genres_response is not None: return cast(GenreResponseList, cached_genres_response) genres_response = await self.genre_service.get_all_genres(size, page) - await self.cache_service.set(key, genres_response, ttl=180) + await self.redis_service.set(key, genres_response, ttl=24 * 60 * 60) return genres_response async def search_genres_by_name( self, - name: str, + search_query: str, size: int = 10, page: int = 1, ) -> GenreResponseList: - key = RedisService.create_cache_key( - "genres", - name=name, + key = await self.cache_key_service.build_list_key( + entity="genre", + action="get", + search_query=search_query, size=size, page=page, ) - cached_genres_response = await self.cache_service.get(key, GenreResponseList) + cached_genres_response = await self.redis_service.get(key, GenreResponseList) if cached_genres_response is not None: return cast(GenreResponseList, cached_genres_response) genres_response = await self.genre_service.search_genres_by_name( - name, + search_query, size, page, ) - await self.cache_service.set(key, genres_response, ttl=180) + await self.redis_service.set(key, genres_response, ttl=24 * 60 * 60) return genres_response async def create_genre(self, create_data: GenreCreate) -> GenreResponse: genre_response = await self.genre_service.create_genre(create_data) - key = RedisService.create_cache_key("genre") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + await self.cache_key_service.invalidate_list_keys(entity="genre") return genre_response async def update_genre( @@ -85,9 +93,15 @@ async def update_genre( update_data: GenreUpdate, ) -> GenreResponse: genre_response = await self.genre_service.update_genre(genre_id, update_data) - key = RedisService.create_cache_key("genre") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + key = self.cache_key_service.build_item_key( + entity="genre", + entity_id=genre_response.id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity="genre"), + self.redis_service.delete(key=key), + ) return genre_response async def partial_update_genre( @@ -99,13 +113,25 @@ async def partial_update_genre( genre_id, update_data, ) - key = RedisService.create_cache_key("genre") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + key = self.cache_key_service.build_item_key( + entity="genre", + entity_id=genre_response.id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity="genre"), + self.redis_service.delete(key=key), + ) return genre_response async def delete_genre_by_id(self, genre_id: int) -> None: await self.genre_service.delete_genre_by_id(genre_id) - key = RedisService.create_cache_key("genre") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + key = self.cache_key_service.build_item_key( + entity="genre", + entity_id=genre_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity="genre"), + self.redis_service.delete(key=key), + ) diff --git a/app/core/redis/cache_keys_service.py b/app/core/redis/cache_key_service.py similarity index 52% rename from app/core/redis/cache_keys_service.py rename to app/core/redis/cache_key_service.py index 0ca2916..70aac49 100644 --- a/app/core/redis/cache_keys_service.py +++ b/app/core/redis/cache_key_service.py @@ -1,3 +1,5 @@ +from typing import cast + from core.config import settings from core.constants import PrimitiveType from core.redis import RedisService @@ -9,47 +11,27 @@ class CacheKeyService: def __init__(self, redis: RedisService) -> None: self.redis = redis - async def __build( - self, - entity: str, - action: str, - entity_id: int | None = None, - **params: PrimitiveType, - ) -> str: - if entity_id is None: - version = await self.__get_version(entity) - if version is None: - await self.__init_version(entity) - key = f"{settings.service_name}:{entity}:{self.list_field}:version:{version}:{action}" - else: - key = f"{settings.service_name}:{entity}:{entity_id}:{action}" - - for parameter_name, value in params.items(): - key = f"{key}:{parameter_name}:{value}" - - return key - async def __get_version(self, entity: str) -> int: key = self.__create_version_entity_key(entity) version = await self.redis.get(key, is_integer=True) - if not await self.redis.exists(key): - raise ValueError - return version + return cast(int, version) async def __update_version(self, entity: str) -> None: key = self.__create_version_entity_key(entity) await self.redis.incr_by(key, amount=1) - async def __init_version(self, entity: str) -> None: + async def __init_version(self, entity: str) -> int: key = self.__create_version_entity_key(entity) + start_value = 1 await self.redis.set( key=key, - value=1, + value=start_value, ) + return start_value @classmethod def __create_version_entity_key(cls, entity: str) -> str: - return f"{settings.service_name}:{cls.list_field}:{entity}:version" + return f"{settings.service_name}:version:{cls.list_field}:{entity}" async def invalidate_list_keys( self, @@ -59,28 +41,31 @@ async def invalidate_list_keys( entity=entity, ) - async def build_item_key( - self, + @staticmethod + def build_item_key( entity: str, - action: str, entity_id: int, + action: str = "get", **params: PrimitiveType, ) -> str: - return await self.__build( - entity=entity, - entity_id=entity_id, - action=action, - **params, - ) + key = f"{settings.service_name}:{entity}:{entity_id}:{action}" + for parameter_name, value in params.items(): + key = f"{key}:{parameter_name}:{value}" + + return key async def build_list_key( self, entity: str, - action: str, + action: str = "get", **params: PrimitiveType, ) -> str: - return await self.__build( - entity=entity, - action=action, - **params, - ) + version = await self.__get_version(entity) + if version is None: + version = await self.__init_version(entity) + key = f"{settings.service_name}:{entity}:{self.list_field}:version:{version}:{action}" # noqa: E501 + + for parameter_name, value in params.items(): + key = f"{key}:{parameter_name}:{value}" + + return key diff --git a/app/core/redis/client.py b/app/core/redis/client.py index 6c83a66..4bc776b 100644 --- a/app/core/redis/client.py +++ b/app/core/redis/client.py @@ -38,7 +38,12 @@ async def get(self, key: str) -> str | None: async def exists(self, key: str) -> bool: return cast(bool, await self._redis.exists(key)) - async def set(self, key: str, value: str | int, expire: int) -> None: + async def set( + self, + key: str, + value: str | int, + expire: int | None = None, + ) -> None: await self._redis.set( key, value, diff --git a/app/core/redis/service.py b/app/core/redis/service.py index 75372f1..b70484e 100644 --- a/app/core/redis/service.py +++ b/app/core/redis/service.py @@ -59,7 +59,12 @@ async def _get_boolean(self, key: str) -> bool | None: return boolean[value] return None - async def set(self, key: str, value: Any, ttl: int = 300) -> None: + async def set( + self, + key: str, + value: Any, + ttl: int | None = None, + ) -> None: encoded_value = self.convert_object_to_string(value) await self.redis.set(key, encoded_value, ttl) @@ -87,7 +92,8 @@ def create_cache_key(cls, prefix: str, **kwargs: Any) -> str: @staticmethod def convert_string_to_object( - value: str | int, schema: type[AnyPydanticType], + value: str | int, + schema: type[AnyPydanticType], ) -> AnyPydanticType | PrimitiveType: if schema is None: return value diff --git a/app/core/utils.py b/app/core/utils.py index eca4984..1ea2bbd 100644 --- a/app/core/utils.py +++ b/app/core/utils.py @@ -7,6 +7,7 @@ from cache_services import GenreCacheService, MovieCacheService from core.database import session_factory from core.redis import RedisClient, RedisService +from core.redis.cache_key_service import CacheKeyService from dependencies.redis_client import ( get_genre_redis_client as get_genre_redis_client_dependency, ) @@ -56,9 +57,14 @@ async def get_genre_redis_service() -> AsyncGenerator[RedisService]: async def get_genre_cache_service() -> AsyncGenerator[GenreCacheService]: async with ( get_genre_service() as genre_service, - get_genre_redis_service() as cache_service, + get_genre_redis_service() as redis_service, ): - genre_cache_service = GenreCacheService(genre_service, cache_service) + cache_key_service = CacheKeyService(redis_service) + genre_cache_service = GenreCacheService( + genre_service, + redis_service, + cache_key_service, + ) yield genre_cache_service diff --git a/app/dependencies/cache_services.py b/app/dependencies/cache_services.py index b71f269..c4527fc 100644 --- a/app/dependencies/cache_services.py +++ b/app/dependencies/cache_services.py @@ -11,6 +11,7 @@ UserCacheService, ) from cache_services.watch_history import WatchHistoryCacheService +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from dependencies.redis_services import ( get_favorite_movie_redis_service, @@ -49,7 +50,12 @@ async def get_genre_cache_service( ], ) -> AsyncGenerator[GenreCacheService]: try: - genre_cache_service = GenreCacheService(genre_service, genre_redis_service) + cache_key_service = CacheKeyService(genre_redis_service) + genre_cache_service = GenreCacheService( + genre_service, + genre_redis_service, + cache_key_service, + ) yield genre_cache_service finally: """ diff --git a/app/services/genre.py b/app/services/genre.py index c4b1a87..e45deff 100644 --- a/app/services/genre.py +++ b/app/services/genre.py @@ -55,14 +55,14 @@ async def get_all_genres( async def search_genres_by_name( self, - name: str, + search_query: str, size: int = 10, page: int = 1, ) -> GenreResponseList: genre_list = [ GenreResponse.model_validate(genre) for genre in await self.genre_repository.search_genres_by_name( - name, + search_query, size, page, ) From cedfdce572e7e59f709106b7fc4309b8aadd0555 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Mon, 6 Jul 2026 16:09:21 +0300 Subject: [PATCH 03/13] Small fixes (frontend). --- frontend/app/api/api_v1/genres.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/api/api_v1/genres.js b/frontend/app/api/api_v1/genres.js index f69dd10..8c9326d 100644 --- a/frontend/app/api/api_v1/genres.js +++ b/frontend/app/api/api_v1/genres.js @@ -14,7 +14,7 @@ var p = page != null ? page : 1; var s = size != null ? size : 10; var q = - "genre_name=" + + "search_query=" + encodeURIComponent(name) + "&page=" + encodeURIComponent(p) + From b16ee2196171c46e118f3fdc535045aa2e44c4ff Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Tue, 7 Jul 2026 17:42:32 +0300 Subject: [PATCH 04/13] Clean code: use separate redis db for cache_versioning. --- app/core/config.py | 1 + app/core/redis/cache_key_service.py | 18 +++---- app/core/utils.py | 6 +-- .../annotations/cache_services.py | 47 ----------------- .../annotations/redis_services.py | 51 +++++++++++++++++++ app/dependencies/cache_key_service.py | 25 +++++++++ app/dependencies/cache_services.py | 6 ++- app/dependencies/rate_limiter.py | 2 +- .../{redis_client.py => redis_clients.py} | 4 ++ app/dependencies/redis_services.py | 13 ++++- 10 files changed, 111 insertions(+), 62 deletions(-) create mode 100644 app/dependencies/annotations/redis_services.py create mode 100644 app/dependencies/cache_key_service.py rename app/dependencies/{redis_client.py => redis_clients.py} (93%) diff --git a/app/core/config.py b/app/core/config.py index 7f26647..56f08ac 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -34,6 +34,7 @@ class RedisDataBaseConfig(BaseModel): watch_history: int = 6 auth: int = 7 celery_backend: int = 8 + cache_versioning: int = 9 class RedisConfig(BaseModel): diff --git a/app/core/redis/cache_key_service.py b/app/core/redis/cache_key_service.py index 70aac49..4b87856 100644 --- a/app/core/redis/cache_key_service.py +++ b/app/core/redis/cache_key_service.py @@ -11,15 +11,6 @@ class CacheKeyService: def __init__(self, redis: RedisService) -> None: self.redis = redis - async def __get_version(self, entity: str) -> int: - key = self.__create_version_entity_key(entity) - version = await self.redis.get(key, is_integer=True) - return cast(int, version) - - async def __update_version(self, entity: str) -> None: - key = self.__create_version_entity_key(entity) - await self.redis.incr_by(key, amount=1) - async def __init_version(self, entity: str) -> int: key = self.__create_version_entity_key(entity) start_value = 1 @@ -29,6 +20,15 @@ async def __init_version(self, entity: str) -> int: ) return start_value + async def __get_version(self, entity: str) -> int: + key = self.__create_version_entity_key(entity) + version = await self.redis.get(key, is_integer=True) + return cast(int, version) + + async def __update_version(self, entity: str) -> None: + key = self.__create_version_entity_key(entity) + await self.redis.incr_by(key, amount=1) + @classmethod def __create_version_entity_key(cls, entity: str) -> str: return f"{settings.service_name}:version:{cls.list_field}:{entity}" diff --git a/app/core/utils.py b/app/core/utils.py index 1ea2bbd..86ff9c6 100644 --- a/app/core/utils.py +++ b/app/core/utils.py @@ -8,13 +8,13 @@ from core.database import session_factory from core.redis import RedisClient, RedisService from core.redis.cache_key_service import CacheKeyService -from dependencies.redis_client import ( +from dependencies.redis_clients import ( get_genre_redis_client as get_genre_redis_client_dependency, ) -from dependencies.redis_client import ( +from dependencies.redis_clients import ( get_movie_redis_client as get_movie_redis_client_dependency, ) -from dependencies.redis_client import ( +from dependencies.redis_clients import ( get_watch_history_redis_client as get_watch_history_redis_client_dependency, ) from services import GenreService, MovieService, UserService diff --git a/app/dependencies/annotations/cache_services.py b/app/dependencies/annotations/cache_services.py index a3fd375..2d3143e 100644 --- a/app/dependencies/annotations/cache_services.py +++ b/app/dependencies/annotations/cache_services.py @@ -10,7 +10,6 @@ UserCacheService, WatchHistoryCacheService, ) -from core.redis import RedisService from dependencies.cache_services import ( get_favorite_movie_cache_service, get_genre_cache_service, @@ -19,52 +18,6 @@ get_user_cache_service, get_watch_history_cache_service, ) -from dependencies.redis_services import ( - get_favorite_movie_redis_service, - get_genre_redis_service, - get_movie_redis_service, - get_review_redis_service, - get_user_redis_service, - get_watch_history_redis_service, -) - -GenreRedisServiceDep = Annotated[ - RedisService, - Depends(get_genre_redis_service), -] - -MovieRedisServiceDep = Annotated[ - RedisService, - Depends(get_movie_redis_service), -] - -FavoriteMovieRedisServiceDep = Annotated[ - RedisService, - Depends( - get_favorite_movie_redis_service, - ), -] - -ReviewRedisServiceDep = Annotated[ - RedisService, - Depends( - get_review_redis_service, - ), -] - -UserRedisServiceDep = Annotated[ - RedisService, - Depends( - get_user_redis_service, - ), -] - -WatchHistoryRedisServiceDep = Annotated[ - RedisService, - Depends( - get_watch_history_redis_service, - ), -] GenreCacheServiceDep = Annotated[ GenreCacheService, diff --git a/app/dependencies/annotations/redis_services.py b/app/dependencies/annotations/redis_services.py new file mode 100644 index 0000000..826fdb1 --- /dev/null +++ b/app/dependencies/annotations/redis_services.py @@ -0,0 +1,51 @@ +from typing import Annotated + +from fastapi import Depends + +from core.redis import RedisService +from dependencies.redis_services import ( + get_favorite_movie_redis_service, + get_genre_redis_service, + get_movie_redis_service, + get_review_redis_service, + get_user_redis_service, + get_watch_history_redis_service, +) + +GenreRedisServiceDep = Annotated[ + RedisService, + Depends(get_genre_redis_service), +] + +MovieRedisServiceDep = Annotated[ + RedisService, + Depends(get_movie_redis_service), +] + +FavoriteMovieRedisServiceDep = Annotated[ + RedisService, + Depends( + get_favorite_movie_redis_service, + ), +] + +ReviewRedisServiceDep = Annotated[ + RedisService, + Depends( + get_review_redis_service, + ), +] + +UserRedisServiceDep = Annotated[ + RedisService, + Depends( + get_user_redis_service, + ), +] + +WatchHistoryRedisServiceDep = Annotated[ + RedisService, + Depends( + get_watch_history_redis_service, + ), +] diff --git a/app/dependencies/cache_key_service.py b/app/dependencies/cache_key_service.py new file mode 100644 index 0000000..6827feb --- /dev/null +++ b/app/dependencies/cache_key_service.py @@ -0,0 +1,25 @@ +from collections.abc import AsyncGenerator +from typing import Annotated + +from fastapi import Depends + +from core.redis import RedisService +from core.redis.cache_key_service import CacheKeyService +from dependencies.redis_services import get_cache_versioning_redis_service + + +async def get_cache_key_service( + cache_key_redis_service: Annotated[ + RedisService, + Depends( + get_cache_versioning_redis_service, + ), + ], +) -> AsyncGenerator[CacheKeyService]: + try: + cache_key_service = CacheKeyService(cache_key_redis_service) + yield cache_key_service + finally: + """ + Действия после view. + """ diff --git a/app/dependencies/cache_services.py b/app/dependencies/cache_services.py index c4527fc..d0c340e 100644 --- a/app/dependencies/cache_services.py +++ b/app/dependencies/cache_services.py @@ -13,6 +13,7 @@ from cache_services.watch_history import WatchHistoryCacheService from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService +from dependencies.cache_key_service import get_cache_key_service from dependencies.redis_services import ( get_favorite_movie_redis_service, get_genre_redis_service, @@ -48,9 +49,12 @@ async def get_genre_cache_service( RedisService, Depends(get_genre_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[GenreCacheService]: try: - cache_key_service = CacheKeyService(genre_redis_service) genre_cache_service = GenreCacheService( genre_service, genre_redis_service, diff --git a/app/dependencies/rate_limiter.py b/app/dependencies/rate_limiter.py index 3317afc..3b39b0b 100644 --- a/app/dependencies/rate_limiter.py +++ b/app/dependencies/rate_limiter.py @@ -7,7 +7,7 @@ from core.exceptions.base import TooManyRequestsError from core.redis.client import RedisClient from core.redis.rate_limiter import RateLimiter -from dependencies.redis_client import get_rate_limiter_redis_client +from dependencies.redis_clients import get_rate_limiter_redis_client def rate_limit_dependency_factory( diff --git a/app/dependencies/redis_client.py b/app/dependencies/redis_clients.py similarity index 93% rename from app/dependencies/redis_client.py rename to app/dependencies/redis_clients.py index 93466bd..5cdfc06 100644 --- a/app/dependencies/redis_client.py +++ b/app/dependencies/redis_clients.py @@ -53,3 +53,7 @@ async def get_redis_client() -> AsyncGenerator[RedisClient]: get_auth_redis_client = redis_client_factory( db=settings.redis.db.auth, ) + +get_cache_versioning_redis_client = redis_client_factory( + db=settings.redis.db.cache_versioning, +) diff --git a/app/dependencies/redis_services.py b/app/dependencies/redis_services.py index ffe33d3..48b5d92 100644 --- a/app/dependencies/redis_services.py +++ b/app/dependencies/redis_services.py @@ -4,8 +4,9 @@ from fastapi import Depends from core.redis import RedisClient, RedisService -from dependencies.redis_client import ( +from dependencies.redis_clients import ( get_auth_redis_client, + get_cache_versioning_redis_client, get_favorite_movie_redis_client, get_genre_redis_client, get_movie_redis_client, @@ -41,21 +42,31 @@ async def dependency( get_genre_redis_service = redis_service_factory( get_genre_redis_client, ) + get_movie_redis_service = redis_service_factory( get_movie_redis_client, ) + get_review_redis_service = redis_service_factory( get_review_redis_client, ) + get_favorite_movie_redis_service = redis_service_factory( get_favorite_movie_redis_client, ) + get_watch_history_redis_service = redis_service_factory( get_watch_history_redis_client, ) + get_user_redis_service = redis_service_factory( get_user_redis_client, ) + get_auth_redis_service = redis_service_factory( get_auth_redis_client, ) + +get_cache_versioning_redis_service = redis_service_factory( + get_cache_versioning_redis_client, +) From 72db1463dfdac736f13b4fb8653f48d4cd298a21 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Fri, 10 Jul 2026 14:16:21 +0300 Subject: [PATCH 05/13] Edit delete list of keys, delete by pattern methods in redis service. --- app/core/redis/client.py | 7 ++++++- app/core/redis/service.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/core/redis/client.py b/app/core/redis/client.py index 4bc776b..ff95425 100644 --- a/app/core/redis/client.py +++ b/app/core/redis/client.py @@ -53,9 +53,14 @@ async def set( async def delete(self, key: str) -> None: await self._redis.delete(key) + async def delete_list_of_keys(self, keys: list[str]) -> None: + await self._redis.delete(*keys) + async def delete_by_pattern(self, pattern: str) -> None: + keys_to_delete = [] async for key in self._redis.scan_iter(match=pattern): - await self.delete(key) + keys_to_delete.append(key) + await self.delete_list_of_keys(keys_to_delete) async def expire(self, key: str, ttl: int = 300) -> None: await self._redis.expire(key, ttl) diff --git a/app/core/redis/service.py b/app/core/redis/service.py index b70484e..c3bc601 100644 --- a/app/core/redis/service.py +++ b/app/core/redis/service.py @@ -80,6 +80,9 @@ async def exists(self, key: str) -> bool: async def delete(self, key: str) -> None: await self.redis.delete(key) + async def delete_list_of_keys(self, keys: list[str]) -> None: + await self.redis.delete(*keys) + async def delete_by_pattern(self, pattern: str) -> None: await self.redis.delete_by_pattern(pattern) From 088fc3829adb173195249ed0b905f7aa213573d1 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Fri, 10 Jul 2026 14:21:53 +0300 Subject: [PATCH 06/13] Add celery task to invalidate movie detail due to update genre info. --- app/cache_services/genre.py | 33 +++++++++++++++++-------- app/core/celery/tasks.py | 14 ++++++++++- app/core/celery/utils.py | 26 ++++++++++++++++++-- app/core/constants.py | 5 ++++ app/core/redis/cache_key_service.py | 37 ++++++++++++++++++++--------- app/core/utils.py | 25 ++++++++++++++++++- packages/celery/constants.py | 4 ++++ 7 files changed, 119 insertions(+), 25 deletions(-) diff --git a/app/cache_services/genre.py b/app/cache_services/genre.py index ca60f05..4711911 100644 --- a/app/cache_services/genre.py +++ b/app/cache_services/genre.py @@ -1,6 +1,10 @@ import asyncio from typing import cast +from packages.celery.constants import Queue, TaskType + +from core.celery.celery_app import app +from core.constants import CacheEntity from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.genre import ( @@ -26,7 +30,7 @@ def __init__( async def get_genre_by_id(self, genre_id: int) -> GenreResponse: key = self.cache_key_service.build_item_key( - entity="genre", + entity=CacheEntity.genre, entity_id=genre_id, action="get", ) @@ -44,7 +48,7 @@ async def get_all_genres( page: int = 1, ) -> GenreResponseList: key = await self.cache_key_service.build_list_key( - entity="genre", + entity=CacheEntity.genre, action="get", size=size, page=page, @@ -64,7 +68,7 @@ async def search_genres_by_name( page: int = 1, ) -> GenreResponseList: key = await self.cache_key_service.build_list_key( - entity="genre", + entity=CacheEntity.genre, action="get", search_query=search_query, size=size, @@ -84,7 +88,7 @@ async def search_genres_by_name( async def create_genre(self, create_data: GenreCreate) -> GenreResponse: genre_response = await self.genre_service.create_genre(create_data) - await self.cache_key_service.invalidate_list_keys(entity="genre") + await self.cache_key_service.invalidate_list_keys(entity=CacheEntity.genre) return genre_response async def update_genre( @@ -94,12 +98,13 @@ async def update_genre( ) -> GenreResponse: genre_response = await self.genre_service.update_genre(genre_id, update_data) key = self.cache_key_service.build_item_key( - entity="genre", + entity=CacheEntity.genre, entity_id=genre_response.id, action="get", ) await asyncio.gather( - self.cache_key_service.invalidate_list_keys(entity="genre"), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.genre), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), self.redis_service.delete(key=key), ) return genre_response @@ -114,12 +119,13 @@ async def partial_update_genre( update_data, ) key = self.cache_key_service.build_item_key( - entity="genre", + entity=CacheEntity.genre, entity_id=genre_response.id, action="get", ) await asyncio.gather( - self.cache_key_service.invalidate_list_keys(entity="genre"), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.genre), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), self.redis_service.delete(key=key), ) return genre_response @@ -127,11 +133,18 @@ async def partial_update_genre( async def delete_genre_by_id(self, genre_id: int) -> None: await self.genre_service.delete_genre_by_id(genre_id) key = self.cache_key_service.build_item_key( - entity="genre", + entity=CacheEntity.genre, entity_id=genre_id, action="get", ) await asyncio.gather( - self.cache_key_service.invalidate_list_keys(entity="genre"), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.genre), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), self.redis_service.delete(key=key), ) + + app.apply_async( + args=[genre_id], + name=TaskType.delete_cached_movie_detail_data_by_genre_id.value, + queue=Queue.app.value, + ) diff --git a/app/core/celery/tasks.py b/app/core/celery/tasks.py index a873228..10c0da7 100644 --- a/app/core/celery/tasks.py +++ b/app/core/celery/tasks.py @@ -3,7 +3,10 @@ from packages.celery.utils import sync_run_coroutine_function from .celery_app import app -from .utils import get_data_to_send_inactive_users_movie_selection +from .utils import ( + delete_cached_movie_detail_data_by_genre_id, + get_data_to_send_inactive_users_movie_selection, +) @app.task( # type: ignore[untyped-decorator] @@ -31,3 +34,12 @@ def create_chain_to_notify_inactive_users() -> None: send_inactive_users_email, ) task_chain.delay() + + +@app.task( + name=TaskType.delete_cached_movie_detail_data_by_genre_id.value, +) +def delete_cached_movie_detail_data_by_genre(genre_id: int) -> None: + sync_run_coroutine_function( + delete_cached_movie_detail_data_by_genre_id(genre_id), + ) diff --git a/app/core/celery/utils.py b/app/core/celery/utils.py index 4166191..04326cb 100644 --- a/app/core/celery/utils.py +++ b/app/core/celery/utils.py @@ -9,8 +9,13 @@ SendInactiveUsersMovieSelectionData, ) -from core.constants import INACTIVE_DAYS, SortMonotony, SortType -from core.utils import get_movie_service, get_user_service +from core.constants import INACTIVE_DAYS, CacheEntity, SortMonotony, SortType +from core.utils import ( + get_cache_key_service, + get_movie_redis_service, + get_movie_service, + get_user_service, +) from schemas.movie import MovieFilter @@ -63,3 +68,20 @@ async def get_data_to_send_inactive_users_movie_selection() -> ( inactive_users=inactive_users, movie_selection=movie_selection, ) + + +async def delete_cached_movie_detail_data_by_genre_id(genre_id: int) -> None: + async with ( + get_movie_service() as movie_service, + get_movie_redis_service() as movie_redis_service, + get_cache_key_service() as cache_key_service, + ): + movies = await movie_service.get_movies_by_genre_id(genre_id=genre_id) + movie_ids = [movie.id for movie in movies] + movie_ids_regex = "|".join(movie_ids) + pattern = cache_key_service.build_item_regex_key( + entity_regex=CacheEntity.movie, + entity_id_regex=movie_ids_regex, + action_regex="get", + ) + await movie_redis_service.delete_by_pattern(pattern) diff --git a/app/core/constants.py b/app/core/constants.py index 8aa16cb..6bc5c2d 100644 --- a/app/core/constants.py +++ b/app/core/constants.py @@ -68,6 +68,11 @@ class ConfirmationCodeType(StrEnum): recover_password = "recover" +class CacheEntity(StrEnum): + genre = "genre" + movie = "movie" + + INACTIVE_DAYS = 7 ATTEMPT_FIELD = "attempt" diff --git a/app/core/redis/cache_key_service.py b/app/core/redis/cache_key_service.py index 4b87856..8c834ef 100644 --- a/app/core/redis/cache_key_service.py +++ b/app/core/redis/cache_key_service.py @@ -1,7 +1,7 @@ from typing import cast from core.config import settings -from core.constants import PrimitiveType +from core.constants import CacheEntity, PrimitiveType from core.redis import RedisService @@ -11,7 +11,7 @@ class CacheKeyService: def __init__(self, redis: RedisService) -> None: self.redis = redis - async def __init_version(self, entity: str) -> int: + async def __init_version(self, entity: CacheEntity) -> int: key = self.__create_version_entity_key(entity) start_value = 1 await self.redis.set( @@ -20,22 +20,22 @@ async def __init_version(self, entity: str) -> int: ) return start_value - async def __get_version(self, entity: str) -> int: + async def __get_version(self, entity: CacheEntity) -> int: key = self.__create_version_entity_key(entity) version = await self.redis.get(key, is_integer=True) return cast(int, version) - async def __update_version(self, entity: str) -> None: + async def __update_version(self, entity: CacheEntity) -> None: key = self.__create_version_entity_key(entity) await self.redis.incr_by(key, amount=1) @classmethod - def __create_version_entity_key(cls, entity: str) -> str: - return f"{settings.service_name}:version:{cls.list_field}:{entity}" + def __create_version_entity_key(cls, entity: CacheEntity) -> str: + return f"{settings.service_name}:version:{cls.list_field}:{entity.value}" async def invalidate_list_keys( self, - entity: str, + entity: CacheEntity, ) -> None: await self.__update_version( entity=entity, @@ -43,27 +43,42 @@ async def invalidate_list_keys( @staticmethod def build_item_key( - entity: str, + entity: CacheEntity, entity_id: int, action: str = "get", **params: PrimitiveType, ) -> str: - key = f"{settings.service_name}:{entity}:{entity_id}:{action}" + key = f"{settings.service_name}:{entity.value}:{entity_id}:{action}" for parameter_name, value in params.items(): key = f"{key}:{parameter_name}:{value}" return key + @classmethod + def build_item_regex_key( + cls, + entity_regex: CacheEntity | str, + entity_id_regex: int | str, + action_regex: str = "get", + **params: PrimitiveType, + ) -> str: + return cls.build_item_key( + entity=entity_regex, + entity_id=entity_id_regex, + action=action_regex, + **params, + ) + async def build_list_key( self, - entity: str, + entity: CacheEntity, action: str = "get", **params: PrimitiveType, ) -> str: version = await self.__get_version(entity) if version is None: version = await self.__init_version(entity) - key = f"{settings.service_name}:{entity}:{self.list_field}:version:{version}:{action}" # noqa: E501 + key = f"{settings.service_name}:{entity.value}:{self.list_field}:version:{version}:{action}" # noqa: E501 for parameter_name, value in params.items(): key = f"{key}:{parameter_name}:{value}" diff --git a/app/core/utils.py b/app/core/utils.py index 86ff9c6..e312d83 100644 --- a/app/core/utils.py +++ b/app/core/utils.py @@ -8,6 +8,9 @@ from core.database import session_factory from core.redis import RedisClient, RedisService from core.redis.cache_key_service import CacheKeyService +from dependencies.redis_clients import ( + get_cache_versioning_redis_client as get_cache_versioning_redis_client_dependency, +) from dependencies.redis_clients import ( get_genre_redis_client as get_genre_redis_client_dependency, ) @@ -20,6 +23,26 @@ from services import GenreService, MovieService, UserService +@asynccontextmanager +async def get_cache_versioning_redis_client() -> AsyncGenerator[RedisClient]: + async for redis_client in get_cache_versioning_redis_client_dependency(): + yield redis_client + + +@asynccontextmanager +async def get_cache_versioning_redis_service() -> AsyncGenerator[RedisService]: + async with get_cache_versioning_redis_client() as redis_client: + redis_service = RedisService(redis_client) + yield redis_service + + +@asynccontextmanager +async def get_cache_key_service() -> AsyncGenerator[CacheKeyService]: + async with get_cache_versioning_redis_service() as redis_service: + cache_key_service = CacheKeyService(redis_service) + yield cache_key_service + + @asynccontextmanager async def get_session() -> AsyncGenerator[AsyncSession]: async with session_factory() as session: @@ -58,8 +81,8 @@ async def get_genre_cache_service() -> AsyncGenerator[GenreCacheService]: async with ( get_genre_service() as genre_service, get_genre_redis_service() as redis_service, + get_cache_key_service() as cache_key_service, ): - cache_key_service = CacheKeyService(redis_service) genre_cache_service = GenreCacheService( genre_service, redis_service, diff --git a/packages/celery/constants.py b/packages/celery/constants.py index 091ce5d..a3db525 100644 --- a/packages/celery/constants.py +++ b/packages/celery/constants.py @@ -24,3 +24,7 @@ class TaskType(StrEnum): "movie-catalog.mailing-list.get-data-to-send-inactive-users-email" ) send_inactive_users_email = "notification-service.email.send-inactive-users-email" + + delete_cached_movie_detail_data_by_genre_id = ( + "movie-catalog.cache.delete-cached-movie-detail-data" + ) From 0bb1771376a188819afa34f75bd4639001a6cc39 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Fri, 10 Jul 2026 18:08:58 +0300 Subject: [PATCH 07/13] Add correct caching to movie reviews. --- app/cache_services/review.py | 139 ++++++++++++++++++++-------- app/core/celery/utils.py | 2 +- app/core/constants.py | 4 + app/core/redis/cache_key_service.py | 18 +++- app/dependencies/cache_services.py | 10 +- app/repositories/review.py | 9 +- app/services/genre.py | 1 - app/services/review.py | 8 +- docker-compose.yml | 2 +- 9 files changed, 142 insertions(+), 51 deletions(-) diff --git a/app/cache_services/review.py b/app/cache_services/review.py index cf3deca..534e611 100644 --- a/app/cache_services/review.py +++ b/app/cache_services/review.py @@ -1,5 +1,8 @@ +import asyncio from typing import cast +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from dependencies.annotations.validators import PaginationPageDep, PaginationSizeDep from schemas.review import ( @@ -19,16 +22,23 @@ def __init__( self, review_service: ReviewService, cache_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.review_service = review_service self.cache_service = cache_service + self.cache_key_service = cache_key_service async def get_reviews( self, size: int = 10, page: int = 1, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key("reviews", size=size, page=page) + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get", + size=size, + page=page, + ) cached_reviews_response = await self.cache_service.get( key, ReviewWithUserResponseList, @@ -40,12 +50,16 @@ async def get_reviews( await self.cache_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response async def get_review_by_id(self, review_id: int) -> ReviewWithUserResponse: - key = RedisService.create_cache_key("review", review_id=review_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=review_id, + action="get", + ) cached_review_response = await self.cache_service.get( key, ReviewWithUserResponse, @@ -57,7 +71,7 @@ async def get_review_by_id(self, review_id: int) -> ReviewWithUserResponse: await self.cache_service.set( key, review_response, - ttl=1800, + ttl=30 * 60, ) return review_response @@ -67,8 +81,9 @@ async def get_user_reviews( size: int = 10, page: int = 1, ) -> ReviewWithMovieResponseList: - key = RedisService.create_cache_key( - "reviews", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get", user_id=user_id, size=size, page=page, @@ -88,7 +103,7 @@ async def get_user_reviews( await self.cache_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -97,10 +112,11 @@ async def get_user_review_about_movie( user_id: int, movie_id: int, ) -> ReviewResponse: - key = RedisService.create_cache_key( - "review", - user_id=user_id, - movie_id=movie_id, + entity_id = (user_id, movie_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=entity_id, + action="get", ) cached_review_response = await self.cache_service.get(key, ReviewResponse) if cached_review_response is not None: @@ -113,7 +129,7 @@ async def get_user_review_about_movie( await self.cache_service.set( key, review_response, - ttl=1800, + ttl=30 * 60, ) return review_response @@ -123,8 +139,9 @@ async def get_movie_reviews( size: int = 10, page: int = 1, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get", movie_id=movie_id, size=size, page=page, @@ -144,7 +161,7 @@ async def get_movie_reviews( await self.cache_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -154,8 +171,9 @@ async def get_low_rated_movie_reviews( size: int, page: int, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews:low-rated", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get-low-rated", movie_id=movie_id, size=size, page=page, @@ -175,7 +193,7 @@ async def get_low_rated_movie_reviews( await self.cache_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -185,8 +203,9 @@ async def get_top_rated_movie_reviews( size: PaginationSizeDep = 10, page: PaginationPageDep = 1, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews:top-rated", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get-top-rated", movie_id=movie_id, size=size, page=page, @@ -216,8 +235,9 @@ async def get_top_newest_movie_reviews( size: int, page: int, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews:top-newest", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get-top-newest", movie_id=movie_id, size=size, page=page, @@ -237,7 +257,7 @@ async def get_top_newest_movie_reviews( await self.cache_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -247,8 +267,9 @@ async def get_top_oldest_movie_reviews( size: int, page: int, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews:top-oldest", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get-top-oldest", movie_id=movie_id, size=size, page=page, @@ -268,7 +289,7 @@ async def get_top_oldest_movie_reviews( await self.cache_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -281,8 +302,12 @@ async def create_review( user_id, create_review_data, ) - key = RedisService.create_cache_key("review") - pattern = key + "*" + pattern = await self.cache_key_service.build_list_regex_key( + entity=CacheEntity.review, + action_regex="get*", + size="*", + page="*", + ) await self.cache_service.delete_by_pattern(pattern) return review_response @@ -297,9 +322,21 @@ async def update_review( review_id, update_review_data, ) - key = RedisService.create_cache_key("review") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + pattern = await self.cache_key_service.build_list_regex_key( + entity=CacheEntity.review, + action_regex="get*", + size="*", + page="*", + ) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=review_id, + action="get", + ) + await asyncio.gather( + self.cache_service.delete_by_pattern(pattern), + self.cache_service.delete(key), + ) return review_response async def partial_update_review( @@ -313,9 +350,21 @@ async def partial_update_review( review_id, update_review_data, ) - key = RedisService.create_cache_key("review") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + pattern = await self.cache_key_service.build_list_regex_key( + entity=CacheEntity.review, + action_regex="get*", + size="*", + page="*", + ) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=review_id, + action="get", + ) + await asyncio.gather( + self.cache_service.delete_by_pattern(pattern), + self.cache_service.delete(key), + ) return review_response async def delete_review( @@ -323,10 +372,24 @@ async def delete_review( current_user_id: int, review_id: int, ) -> None: - await self.review_service.delete_review( + review = await self.review_service.delete_review( current_user_id, review_id, ) - key = RedisService.create_cache_key("review") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + pattern = await self.cache_key_service.build_list_regex_key( + entity=CacheEntity.review, + action_regex="get*", + size="*", + page="*", + ) + user_id, movie_id = review.user_id, review.movie_id + entity_id = (user_id, movie_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=entity_id, + action="get", + ) + await asyncio.gather( + self.cache_service.delete_by_pattern(pattern), + self.cache_service.delete(key), + ) diff --git a/app/core/celery/utils.py b/app/core/celery/utils.py index 04326cb..9882d95 100644 --- a/app/core/celery/utils.py +++ b/app/core/celery/utils.py @@ -80,7 +80,7 @@ async def delete_cached_movie_detail_data_by_genre_id(genre_id: int) -> None: movie_ids = [movie.id for movie in movies] movie_ids_regex = "|".join(movie_ids) pattern = cache_key_service.build_item_regex_key( - entity_regex=CacheEntity.movie, + entity=CacheEntity.movie, entity_id_regex=movie_ids_regex, action_regex="get", ) diff --git a/app/core/constants.py b/app/core/constants.py index 6bc5c2d..e161111 100644 --- a/app/core/constants.py +++ b/app/core/constants.py @@ -71,6 +71,10 @@ class ConfirmationCodeType(StrEnum): class CacheEntity(StrEnum): genre = "genre" movie = "movie" + favorite_movie = "favorite_movie" + review = "review" + user = "user" + watch_history = "watch_history" INACTIVE_DAYS = 7 diff --git a/app/core/redis/cache_key_service.py b/app/core/redis/cache_key_service.py index 8c834ef..7cf90da 100644 --- a/app/core/redis/cache_key_service.py +++ b/app/core/redis/cache_key_service.py @@ -44,7 +44,7 @@ async def invalidate_list_keys( @staticmethod def build_item_key( entity: CacheEntity, - entity_id: int, + entity_id: int | tuple, action: str = "get", **params: PrimitiveType, ) -> str: @@ -57,13 +57,13 @@ def build_item_key( @classmethod def build_item_regex_key( cls, - entity_regex: CacheEntity | str, + entity: CacheEntity, entity_id_regex: int | str, action_regex: str = "get", **params: PrimitiveType, ) -> str: return cls.build_item_key( - entity=entity_regex, + entity=entity, entity_id=entity_id_regex, action=action_regex, **params, @@ -84,3 +84,15 @@ async def build_list_key( key = f"{key}:{parameter_name}:{value}" return key + + async def build_list_regex_key( + self, + entity: CacheEntity, + action_regex: str = "get", + **params: PrimitiveType, + ) -> str: + return await self.build_list_key( + entity=entity, + action=action_regex, + **params, + ) diff --git a/app/dependencies/cache_services.py b/app/dependencies/cache_services.py index d0c340e..21320aa 100644 --- a/app/dependencies/cache_services.py +++ b/app/dependencies/cache_services.py @@ -125,9 +125,17 @@ async def get_review_cache_service( RedisService, Depends(get_review_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[ReviewCacheService]: try: - review_cache_service = ReviewCacheService(review_service, review_redis_service) + review_cache_service = ReviewCacheService( + review_service, + review_redis_service, + cache_key_service, + ) yield review_cache_service finally: """ diff --git a/app/repositories/review.py b/app/repositories/review.py index 3804662..272a7ec 100644 --- a/app/repositories/review.py +++ b/app/repositories/review.py @@ -209,11 +209,12 @@ async def partial_update_review( await self.session.refresh(review) return await self.get_review_by_id(review.id) - async def delete_review(self, review_id: int) -> bool: - if await self.get_review_by_id(review_id) is None: - return False + async def delete_review(self, review_id: int) -> Review | None: + review = await self.get_review_by_id(review_id) + if review is None: + return None stmt = delete(Review).where(Review.id == review_id) await self.session.execute(stmt) await self.session.commit() - return True + return review diff --git a/app/services/genre.py b/app/services/genre.py index e45deff..b6116de 100644 --- a/app/services/genre.py +++ b/app/services/genre.py @@ -42,7 +42,6 @@ async def get_all_genres( size: int = 10, page: int = 1, ) -> GenreResponseList: - genres = [ GenreResponse.model_validate(genre) for genre in await self.genre_repository.get_all_genres(size, page) diff --git a/app/services/review.py b/app/services/review.py index a09f6a8..c831ff2 100644 --- a/app/services/review.py +++ b/app/services/review.py @@ -9,6 +9,7 @@ ReviewNotFoundByUserAndMovieError, ) from core.exceptions.user import UserIdNotFoundError +from models import Review from repositories import MovieRepository, ReviewRepository, UserRepository from schemas.review import ( ReviewCreate, @@ -273,7 +274,7 @@ async def delete_review( self, current_user_id: int, review_id: int, - ) -> None: + ) -> Review: if not await self.user_repository.user_id_exists(current_user_id): raise UserIdNotFoundError(current_user_id) @@ -286,7 +287,10 @@ async def delete_review( ): raise PermissionDeniedError - await self.review_repository.delete_review(review_id) + review = await self.review_repository.delete_review(review_id) + if review is None: + raise ReviewIdNotFoundError(review_id) + return review async def get_review_owner(self, review_id: int) -> UserResponse: user = await self.review_repository.get_review_owner(review_id) diff --git a/docker-compose.yml b/docker-compose.yml index 4a78656..bcf7515 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,7 +70,7 @@ services: start_period: 3s interval: 2s timeout: 2s - retries: 3 + retries: 5 depends_on: minio: condition: service_healthy From b384c8d457c21dbb427aa175758d27f59f9ee8d4 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Mon, 13 Jul 2026 15:00:28 +0300 Subject: [PATCH 08/13] Add correct caching to watch history. --- app/api/api_v1/movies/list_views.py | 3 +- app/cache_services/watch_history.py | 175 ++++++++++++++++++++++++---- app/core/redis/client.py | 4 +- app/dependencies/cache_services.py | 5 + 4 files changed, 163 insertions(+), 24 deletions(-) diff --git a/app/api/api_v1/movies/list_views.py b/app/api/api_v1/movies/list_views.py index c08dc42..6d57e9b 100644 --- a/app/api/api_v1/movies/list_views.py +++ b/app/api/api_v1/movies/list_views.py @@ -49,8 +49,7 @@ async def watch_movie( movie_cache_service: MovieCacheServiceDep, ) -> RedirectResponse: movie = await movie_cache_service.watch_movie(user_id, create_watch_history_data) - url = BASE_MINIO_URL + movie.source_url - return RedirectResponse(url=url) + return RedirectResponse(url=movie.source_url) @router.post( diff --git a/app/cache_services/watch_history.py b/app/cache_services/watch_history.py index de2b9fe..9a1f785 100644 --- a/app/cache_services/watch_history.py +++ b/app/cache_services/watch_history.py @@ -1,8 +1,12 @@ +import asyncio from datetime import date from typing import cast +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.watch_history import ( + WatchHistoryCreate, WatchHistoryWithMovieResponse, WatchHistoryWithMovieResponseList, ) @@ -14,17 +18,20 @@ def __init__( self, watch_history_service: WatchHistoryService, cache_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.watch_history_service = watch_history_service self.cache_service = cache_service + self.cache_key_service = cache_key_service async def get_watch_history_by_id( self, watch_history_id: int, ) -> WatchHistoryWithMovieResponse: - key = RedisService.create_cache_key( - "watch_history", - watch_history_id=watch_history_id, + key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=watch_history_id, + action="get", ) cached_watch_history_response = await self.cache_service.get( key, @@ -36,7 +43,11 @@ async def get_watch_history_by_id( watch_history_response = ( await self.watch_history_service.get_watch_history_by_id(watch_history_id) ) - await self.cache_service.set(key, watch_history_response) + await self.cache_service.set( + key, + watch_history_response, + ttl=5 * 60, + ) return watch_history_response async def get_watch_history_list( @@ -45,8 +56,9 @@ async def get_watch_history_list( size: int = 10, page: int = 1, ) -> WatchHistoryWithMovieResponseList: - key = RedisService.create_cache_key( - "watch_history_list", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.watch_history, + action="get", user_id=user_id, size=size, page=page, @@ -64,7 +76,11 @@ async def get_watch_history_list( watch_history_list_response = ( await self.watch_history_service.get_watch_history_list(user_id, size, page) ) - await self.cache_service.set(key, watch_history_list_response) + await self.cache_service.set( + key, + watch_history_list_response, + ttl=15 * 60, + ) return watch_history_list_response async def get_watch_history_by_date_range( @@ -75,8 +91,9 @@ async def get_watch_history_by_date_range( size: int = 10, page: int = 1, ) -> WatchHistoryWithMovieResponseList: - key = RedisService.create_cache_key( - "watch_history_list", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.watch_history, + action="get", user_id=user_id, start_date=start_date, end_date=end_date, @@ -102,35 +119,151 @@ async def get_watch_history_by_date_range( page, ) ) - await self.cache_service.set(key, watch_history_list_response) + await self.cache_service.set( + key, + watch_history_list_response, + ttl=15 * 60, + ) return watch_history_list_response async def count_user_watch_history(self, user_id: int) -> int: - key = RedisService.create_cache_key("watch_history", user_id=user_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=user_id, + action="get-count", + ) cached_watch_history_response = await self.cache_service.get(key) if cached_watch_history_response is not None: return cast(int, cached_watch_history_response) watch_history_response = ( await self.watch_history_service.count_user_watch_history(user_id) ) - await self.cache_service.set(key, watch_history_response) + await self.cache_service.set( + key, + watch_history_response, + ttl=24 * 60 * 60, + ) return watch_history_response + async def add_movie_to_watch_history( + self, + user_id: int, + create_watch_history_data: WatchHistoryCreate, + ) -> WatchHistoryWithMovieResponse: ... + async def delete_watch_history_by_id( self, user_id: int, watch_history_id: int, ) -> None: - await self.watch_history_service.delete_watch_history_by_id( - user_id, - watch_history_id, + watch_history_count_key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=user_id, + action="get-count", + ) + watch_history_key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=watch_history_id, + action="get", + ) + user_watch_history_pattern_coroutine = ( + self.cache_key_service.build_list_regex_key( + entity=CacheEntity.watch_history, + action_regex="get", + user_id=user_id, + size="*", + page="*", + ) + ) + user_watch_history_range_date_pattern_coroutine = ( + self.cache_key_service.build_list_key( + entity=CacheEntity.watch_history, + action="get", + user_id=user_id, + start_date="*", + end_date="*", + size="*", + page="*", + ) + ) + user_watch_history_pattern, user_watch_history_range_date_pattern = ( + await asyncio.gather( + user_watch_history_pattern_coroutine, + user_watch_history_range_date_pattern_coroutine, + ) + ) + + delete_watch_history_item = ( + self.watch_history_service.delete_watch_history_by_id( + user_id, + watch_history_id, + ) + ) + change_watch_history_count_cache = self.cache_service.incr_by( + key=watch_history_count_key, + amount=-1, + ) + delete_watch_history_item_cache = self.cache_service.delete(watch_history_key) + delete_watch_history_cache = self.cache_service.delete_by_pattern( + user_watch_history_pattern, + ) + delete_watch_history_range_date_cache = self.cache_service.delete_by_pattern( + user_watch_history_range_date_pattern, + ) + await asyncio.gather( + delete_watch_history_item, + change_watch_history_count_cache, + delete_watch_history_item_cache, + delete_watch_history_cache, + delete_watch_history_range_date_cache, ) - key = RedisService.create_cache_key("watch_history") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) async def delete_user_watch_history(self, user_id: int) -> None: await self.watch_history_service.delete_user_watch_history(user_id) - key = RedisService.create_cache_key("watch_history") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + + watch_history_count_key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=user_id, + action="get-count", + ) + user_watch_history_pattern_coroutine = ( + self.cache_key_service.build_list_regex_key( + entity=CacheEntity.watch_history, + action_regex="get", + user_id=user_id, + size="*", + page="*", + ) + ) + user_watch_history_range_date_pattern_coroutine = ( + self.cache_key_service.build_list_key( + entity=CacheEntity.watch_history, + action="get", + user_id=user_id, + start_date="*", + end_date="*", + size="*", + page="*", + ) + ) + user_watch_history_pattern, user_watch_history_range_date_pattern = ( + await asyncio.gather( + user_watch_history_pattern_coroutine, + user_watch_history_range_date_pattern_coroutine, + ) + ) + + delete_watch_history_count_cache = self.cache_service.delete( + key=watch_history_count_key, + ) + delete_watch_history_cache = self.cache_service.delete_by_pattern( + user_watch_history_pattern, + ) + delete_watch_history_range_date_cache = self.cache_service.delete_by_pattern( + user_watch_history_range_date_pattern, + ) + await asyncio.gather( + delete_watch_history_count_cache, + delete_watch_history_cache, + delete_watch_history_range_date_cache, + ) diff --git a/app/core/redis/client.py b/app/core/redis/client.py index ff95425..409801d 100644 --- a/app/core/redis/client.py +++ b/app/core/redis/client.py @@ -60,7 +60,9 @@ async def delete_by_pattern(self, pattern: str) -> None: keys_to_delete = [] async for key in self._redis.scan_iter(match=pattern): keys_to_delete.append(key) - await self.delete_list_of_keys(keys_to_delete) + + if len(keys_to_delete) > 0: + await self.delete_list_of_keys(keys_to_delete) async def expire(self, key: str, ttl: int = 300) -> None: await self._redis.expire(key, ttl) diff --git a/app/dependencies/cache_services.py b/app/dependencies/cache_services.py index 21320aa..3dc2dcc 100644 --- a/app/dependencies/cache_services.py +++ b/app/dependencies/cache_services.py @@ -152,11 +152,16 @@ async def get_watch_history_cache_service( RedisService, Depends(get_watch_history_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[WatchHistoryCacheService]: try: watch_history_cache_service = WatchHistoryCacheService( watch_history_service, watch_history_redis_service, + cache_key_service, ) yield watch_history_cache_service finally: From 701e32f7b950909180c54c9fbbcfa10f66d26163 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Mon, 13 Jul 2026 16:59:58 +0300 Subject: [PATCH 09/13] Add update cache watch history on watch movie. --- app/cache_services/movie.py | 17 +++++++++ app/core/rabbitmq/consumers.py | 64 ++++++++++++++++++++++++++++++++-- app/core/rabbitmq/startup.py | 24 +++++++++++++ packages/rabbitmq/constants.py | 10 ++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/app/cache_services/movie.py b/app/cache_services/movie.py index 09f7df1..96c58bf 100644 --- a/app/cache_services/movie.py +++ b/app/cache_services/movie.py @@ -1,5 +1,8 @@ from typing import cast +from packages.rabbitmq.constants import Exchange, ExchangeType, Queue +from packages.rabbitmq.utils import create_message + from core.redis.service import RedisService from dependencies.annotations.validators import PaginationPageDep, PaginationSizeDep from schemas.movie import ( @@ -122,6 +125,20 @@ async def watch_movie( key = RedisService.create_cache_key("watch_history") pattern = key + "*" await self.cache_service_for_watch_history.delete_by_pattern(pattern) + + rabbitmq_service = self.movie_service.rabbitmq_service + exchange = await rabbitmq_service.declare_exchange( + name=Exchange.app, + type=ExchangeType.direct, + durable=True, + ) + data = {"user_id": user_id} + message = create_message(data) + await rabbitmq_service.publish( + message=message, + routing_key=Queue.update_watch_history_cache_on_watch_movie.value, + exchange=exchange, + ) return movie_response async def create_movie( diff --git a/app/core/rabbitmq/consumers.py b/app/core/rabbitmq/consumers.py index c36563a..db826b0 100644 --- a/app/core/rabbitmq/consumers.py +++ b/app/core/rabbitmq/consumers.py @@ -1,3 +1,4 @@ +import asyncio from collections.abc import Callable, Coroutine from typing import Any @@ -6,12 +7,71 @@ from packages.rabbitmq.utils import create_message, get_message from cache_services import GenreCacheService, MovieCacheService -from core.constants import AnyPydanticType -from core.utils import get_genre_cache_service, get_movie_cache_service +from core.constants import AnyPydanticType, CacheEntity +from core.utils import ( + get_cache_key_service, + get_genre_cache_service, + get_movie_cache_service, + get_watch_history_redis_service, +) from schemas.genre import GenrePartialUpdate from schemas.movie import MoviePartialUpdate +async def update_watch_history_cache_on_watch_movie(message: IncomingMessage) -> None: + async with ( + message.process(), + get_watch_history_redis_service() as redis_service, + get_cache_key_service() as cache_key_service, + ): + data = get_message(message=message) + user_id = data["user_id"] + watch_history_count_key = cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=user_id, + action="get-count", + ) + user_watch_history_pattern_coroutine = cache_key_service.build_list_regex_key( + entity=CacheEntity.watch_history, + action_regex="get", + user_id=user_id, + size="*", + page="*", + ) + user_watch_history_range_date_pattern_coroutine = ( + cache_key_service.build_list_key( + entity=CacheEntity.watch_history, + action="get", + user_id=user_id, + start_date="*", + end_date="*", + size="*", + page="*", + ) + ) + user_watch_history_pattern, user_watch_history_range_date_pattern = ( + await asyncio.gather( + user_watch_history_pattern_coroutine, + user_watch_history_range_date_pattern_coroutine, + ) + ) + + delete_watch_history_count_cache = redis_service.delete( + key=watch_history_count_key, + ) + delete_watch_history_cache = redis_service.delete_by_pattern( + user_watch_history_pattern, + ) + delete_watch_history_range_date_cache = redis_service.delete_by_pattern( + user_watch_history_range_date_pattern, + ) + await asyncio.gather( + delete_watch_history_count_cache, + delete_watch_history_cache, + delete_watch_history_range_date_cache, + ) + + def update_media_factory( schema: AnyPydanticType, service_class: Any, diff --git a/app/core/rabbitmq/startup.py b/app/core/rabbitmq/startup.py index 4303947..c38911a 100644 --- a/app/core/rabbitmq/startup.py +++ b/app/core/rabbitmq/startup.py @@ -8,6 +8,7 @@ update_genre_poster_url, update_movie_poster_url, update_movie_source_url, + update_watch_history_cache_on_watch_movie, ) @@ -31,6 +32,29 @@ async def rabbitmq_consumer_queues_startup() -> AsyncGenerator[None]: durable=True, ) + ### + app_exchange = await rabbitmq_service.declare_exchange( + name=Exchange.app, + type=ExchangeType.direct, + durable=True, + ) + queue_update_watch_history_cache_on_watch_movie = ( + await rabbitmq_service.declare_queue( + name=Queue.update_watch_history_cache_on_watch_movie, + durable=True, + ) + ) + await rabbitmq_service.bind( + queue_update_watch_history_cache_on_watch_movie, + app_exchange, + Queue.update_watch_history_cache_on_watch_movie.value, + ) + await rabbitmq_service.consume( + queue_update_watch_history_cache_on_watch_movie, + update_watch_history_cache_on_watch_movie, + ) + #### + await rabbitmq_service.bind( queue_update_genre_poster_url, exchange, diff --git a/packages/rabbitmq/constants.py b/packages/rabbitmq/constants.py index c6d3af2..285e067 100644 --- a/packages/rabbitmq/constants.py +++ b/packages/rabbitmq/constants.py @@ -22,6 +22,10 @@ class ActionType(StrEnum): copy_file = "copy_file" delete_file = "delete_file" + update_watch_history_cache_on_watch_movie = ( + "update_watch_history_cache_on_watch_movie" + ) + class ExchangeType(StrEnum): direct = "direct" @@ -69,3 +73,9 @@ class Queue(StrEnum): entity="content", action=ActionType.delete_file, ) + + update_watch_history_cache_on_watch_movie = create_queue_name( + consumer=ConsumerType.app, + entity="content", + action=ActionType.update_watch_history_cache_on_watch_movie, + ) From 80ac27ee421c759e006db6af7ddae2c3392e00c9 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Tue, 14 Jul 2026 15:29:48 +0300 Subject: [PATCH 10/13] Add caching to favorite movie. --- app/cache_services/favorite_movie.py | 182 ++++++++++++++++++++++----- app/core/redis/service.py | 7 +- app/dependencies/cache_services.py | 5 + app/repositories/favorite_movie.py | 27 ++-- app/services/favorite_movie.py | 29 +++-- 5 files changed, 196 insertions(+), 54 deletions(-) diff --git a/app/cache_services/favorite_movie.py b/app/cache_services/favorite_movie.py index 357d600..3234560 100644 --- a/app/cache_services/favorite_movie.py +++ b/app/cache_services/favorite_movie.py @@ -1,8 +1,11 @@ from typing import cast +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.favorite_movie import ( FavoriteMovieCreate, + FavoriteMovieResponse, FavoriteMovieWithMovieResponse, FavoriteMovieWithMovieResponseList, ) @@ -14,9 +17,11 @@ def __init__( self, favorite_movie_service: FavoriteMovieService, cache_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.favorite_movie_service = favorite_movie_service self.cache_service = cache_service + self.cache_key_service = cache_key_service async def get_favorite_movies_by_user_id( self, @@ -24,11 +29,12 @@ async def get_favorite_movies_by_user_id( size: int = 10, page: int = 1, ) -> FavoriteMovieWithMovieResponseList: - key = self.cache_service.create_cache_key( - "favorite movies", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.favorite_movie, + action="get", user_id=user_id, - size=size, page=page, + size=size, ) cached_favorite_movies_response = await self.cache_service.get( key, @@ -51,7 +57,7 @@ async def get_favorite_movies_by_user_id( await self.cache_service.set( key, favorite_movies_response, - ttl=1800, + ttl=24 * 60 * 60, ) return favorite_movies_response @@ -59,9 +65,10 @@ async def get_favorite_movie_by_id( self, favorite_movie_id: int, ) -> FavoriteMovieWithMovieResponse: - key = self.cache_service.create_cache_key( - "favorite movie", - favorite_movie_id=favorite_movie_id, + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=favorite_movie_id, + action="get", ) cached_favorite_movie_response = await self.cache_service.get( key, @@ -75,13 +82,18 @@ async def get_favorite_movie_by_id( favorite_movie_id, ) ) - await self.cache_service.set(key, favorite_movie_response, ttl=1800) + await self.cache_service.set( + key, + favorite_movie_response, + ttl=5 * 60, + ) return favorite_movie_response async def count_favorites_by_movie(self, movie_id: int) -> int: - key = self.cache_service.create_cache_key( - "favorite movie:count", - movie_id=movie_id, + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=movie_id, + action="get-count", ) cached_favorite_movie_response = await self.cache_service.get(key) if cached_favorite_movie_response is not None: @@ -90,18 +102,26 @@ async def count_favorites_by_movie(self, movie_id: int) -> int: favorite_movie_response = ( await self.favorite_movie_service.count_favorites_by_movie(movie_id) ) - await self.cache_service.set(key, favorite_movie_response, ttl=1800) + await self.cache_service.set( + key, + favorite_movie_response, + ttl=24 * 60 * 60, + ) return favorite_movie_response async def check_favorite_movie_status(self, user_id: int, movie_id: int) -> bool: - # key = self.cache_service.create_cache_key( - # "favorite movie:status", - # user_id=user_id, - # movie_id=movie_id, - # ) - # cached_favorite_movie_response = await self.cache_service.get(key) - # if cached_favorite_movie_response is not None: - # return cast(bool, cached_favorite_movie_response) + entity_id = (user_id, movie_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=entity_id, + action="get-status", + ) + cached_favorite_movie_response = await self.cache_service.get( + key, + is_boolean=True, + ) + if cached_favorite_movie_response is not None: + return cast(bool, cached_favorite_movie_response) favorite_movie_response = ( await self.favorite_movie_service.check_favorite_movie_status( @@ -109,7 +129,11 @@ async def check_favorite_movie_status(self, user_id: int, movie_id: int) -> bool movie_id, ) ) - # await self.cache_service.set(key, favorite_movie_response, ttl=1800) + await self.cache_service.set( + key, + favorite_movie_response, + ttl=24 * 60 * 60, + ) return favorite_movie_response async def create_user_favorite_movie( @@ -123,32 +147,122 @@ async def create_user_favorite_movie( create_favorite_movie_data, ) ) - key = self.cache_service.create_cache_key( - "favorite movie", + pattern = await self.cache_key_service.build_list_regex_key( + entity=CacheEntity.favorite_movie, + action_regex="get", + user_id=user_id, + page="*", + size="*", ) - pattern = key + "*" await self.cache_service.delete_by_pattern(pattern) + + movie_id = favorite_movie_response.movie_id + entity_id = (user_id, movie_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=entity_id, + action="get-status", + ) + await self.cache_service.delete(key) + + movie_id = create_favorite_movie_data.movie_id + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=movie_id, + action="get-count", + ) + await self.cache_service.incr_by( + key, + amount=1, + ) return favorite_movie_response async def delete_favorite_movie_by_id( self, user_id: int, favorite_movie_id: int, - ) -> None: - await self.favorite_movie_service.delete_favorite_movie_by_id( + ) -> FavoriteMovieResponse: + favorite_movie = await self.favorite_movie_service.delete_favorite_movie_by_id( user_id, favorite_movie_id, ) - key = self.cache_service.create_cache_key( - "favorite movie", + pattern = await self.cache_key_service.build_list_regex_key( + entity=CacheEntity.favorite_movie, + action_regex="get", + user_id=user_id, + page="*", + size="*", ) - pattern = key + "*" await self.cache_service.delete_by_pattern(pattern) - async def delete_user_favorite_movie(self, user_id: int, movie_id: int) -> None: - await self.favorite_movie_service.delete_user_favorite_movie(user_id, movie_id) - key = self.cache_service.create_cache_key( - "favorite movie", + movie_id = favorite_movie.movie_id + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=movie_id, + action="get-count", + ) + await self.cache_service.incr_by( + key, + amount=-1, + ) + + entity_id = (user_id, movie_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=entity_id, + action="get-status", + ) + await self.cache_service.delete(key) + + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=favorite_movie_id, + action="get-status", + ) + await self.cache_service.delete(key) + return favorite_movie + + async def delete_user_favorite_movie( + self, + user_id: int, + movie_id: int, + ) -> FavoriteMovieResponse: + favorite_movie = await self.favorite_movie_service.delete_user_favorite_movie( + user_id, + movie_id, + ) + pattern = await self.cache_key_service.build_list_regex_key( + entity=CacheEntity.favorite_movie, + action_regex="get", + user_id=user_id, + page="*", + size="*", ) - pattern = key + "*" await self.cache_service.delete_by_pattern(pattern) + + favorite_movie_id = favorite_movie.id + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=movie_id, + action="get-count", + ) + await self.cache_service.incr_by( + key, + amount=-1, + ) + + entity_id = (user_id, movie_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=entity_id, + action="get-status", + ) + await self.cache_service.delete(key) + + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=favorite_movie_id, + action="get-status", + ) + await self.cache_service.delete(key) + return favorite_movie diff --git a/app/core/redis/service.py b/app/core/redis/service.py index c3bc601..0d1869d 100644 --- a/app/core/redis/service.py +++ b/app/core/redis/service.py @@ -104,9 +104,14 @@ def convert_string_to_object( @staticmethod def convert_object_to_string(value: Any) -> Any: + if isinstance(value, bool): + boolean = {True: "True", False: "False"} + return boolean[value] + if isinstance( value, - (str, int, float, bool), + (str, int, float), ): return value + return value.model_dump_json() diff --git a/app/dependencies/cache_services.py b/app/dependencies/cache_services.py index 3dc2dcc..53fee54 100644 --- a/app/dependencies/cache_services.py +++ b/app/dependencies/cache_services.py @@ -103,11 +103,16 @@ async def get_favorite_movie_cache_service( RedisService, Depends(get_favorite_movie_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[FavoriteMovieCacheService]: try: favorite_movie_cache_service = FavoriteMovieCacheService( favorite_movie_service, favorite_movie_redis_service, + cache_key_service, ) yield favorite_movie_cache_service finally: diff --git a/app/repositories/favorite_movie.py b/app/repositories/favorite_movie.py index 71e447a..d21a4a6 100644 --- a/app/repositories/favorite_movie.py +++ b/app/repositories/favorite_movie.py @@ -97,20 +97,25 @@ async def create_user_favorite_movie( await self.session.refresh(favorite_movie) return await self.get_favorite_movie_by_id(favorite_movie.id) - async def delete_favorite_movie_by_id(self, favorite_movie_id: int) -> bool: - if await self.get_favorite_movie_by_id(favorite_movie_id) is None: - return False + async def delete_favorite_movie_by_id( + self, favorite_movie_id: int, + ) -> FavoriteMovie | None: + favorite_movie = await self.get_favorite_movie_by_id(favorite_movie_id) + if favorite_movie is None: + return None stmt = delete(FavoriteMovie).where(FavoriteMovie.id == favorite_movie_id) await self.session.execute(stmt) await self.session.commit() - return True + return favorite_movie - async def delete_user_favorite_movie(self, user_id: int, movie_id: int) -> bool: - if ( - await self.get_user_favorite_movie(user_id=user_id, movie_id=movie_id) - is None - ): - return False + async def delete_user_favorite_movie( + self, user_id: int, movie_id: int, + ) -> FavoriteMovie | None: + favorite_movie = await self.get_user_favorite_movie( + user_id=user_id, movie_id=movie_id, + ) + if favorite_movie is None: + return None stmt = delete(FavoriteMovie).where( and_( @@ -120,4 +125,4 @@ async def delete_user_favorite_movie(self, user_id: int, movie_id: int) -> bool: ) await self.session.execute(stmt) await self.session.commit() - return True + return favorite_movie diff --git a/app/services/favorite_movie.py b/app/services/favorite_movie.py index be7155d..fcf8bf1 100644 --- a/app/services/favorite_movie.py +++ b/app/services/favorite_movie.py @@ -12,6 +12,7 @@ from repositories.favorite_movie import FavoriteMovieRepository from schemas.favorite_movie import ( FavoriteMovieCreate, + FavoriteMovieResponse, FavoriteMovieWithMovieResponse, FavoriteMovieWithMovieResponseList, ) @@ -123,7 +124,7 @@ async def delete_favorite_movie_by_id( self, user_id: int, favorite_movie_id: int, - ) -> None: + ) -> FavoriteMovieResponse: if not await self.user_repository.user_id_exists(user_id): raise UserIdNotFoundError(user_id) @@ -131,17 +132,29 @@ async def delete_favorite_movie_by_id( if owner.id != user_id: raise PermissionDeniedError - await self.favorite_movie_repository.delete_favorite_movie_by_id( - favorite_movie_id, + favorite_movie = ( + await self.favorite_movie_repository.delete_favorite_movie_by_id( + favorite_movie_id, + ) ) - async def delete_user_favorite_movie(self, user_id: int, movie_id: int) -> None: + if favorite_movie is None: + raise FavoriteMovieIdNotFoundError(favorite_movie_id) + return FavoriteMovieResponse.model_validate(favorite_movie) + + async def delete_user_favorite_movie( + self, user_id: int, movie_id: int, + ) -> FavoriteMovieResponse: if not await self.user_repository.user_id_exists(user_id): raise UserIdNotFoundError(user_id) if not await self.movie_repository.movie_id_exists(movie_id): raise MovieIdNotFoundError(movie_id) - if not await self.favorite_movie_repository.delete_user_favorite_movie( - user_id, - movie_id, - ): + favorite_movie = ( + await self.favorite_movie_repository.delete_user_favorite_movie( + user_id, + movie_id, + ) + ) + if favorite_movie is None: raise FavoriteMovieNotFoundByUserAndMovieError(user_id, movie_id) + return FavoriteMovieResponse.model_validate(favorite_movie) From 2a83d2acef9bba5dd83d81c238f95c369ffb8b93 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Tue, 14 Jul 2026 19:41:26 +0300 Subject: [PATCH 11/13] Small changes. --- app/cache_services/favorite_movie.py | 131 +++++++++++-------------- app/cache_services/genre.py | 18 +++- app/cache_services/movie.py | 66 +++++++------ app/cache_services/review.py | 140 +++++++++++++++++---------- app/cache_services/watch_history.py | 125 ++++++------------------ app/core/celery/tasks.py | 2 +- app/core/celery/utils.py | 2 +- app/core/rabbitmq/consumers.py | 59 +---------- app/core/rabbitmq/startup.py | 24 ----- app/core/redis/cache_key_service.py | 63 +++++++++--- app/core/redis/client.py | 11 +-- app/core/utils.py | 10 +- app/dependencies/cache_services.py | 5 + 13 files changed, 287 insertions(+), 369 deletions(-) diff --git a/app/cache_services/favorite_movie.py b/app/cache_services/favorite_movie.py index 3234560..9a6710d 100644 --- a/app/cache_services/favorite_movie.py +++ b/app/cache_services/favorite_movie.py @@ -1,3 +1,4 @@ +import asyncio from typing import cast from core.constants import CacheEntity @@ -16,11 +17,11 @@ class FavoriteMovieCacheService: def __init__( self, favorite_movie_service: FavoriteMovieService, - cache_service: RedisService, + redis_service: RedisService, cache_key_service: CacheKeyService, ) -> None: self.favorite_movie_service = favorite_movie_service - self.cache_service = cache_service + self.redis_service = redis_service self.cache_key_service = cache_key_service async def get_favorite_movies_by_user_id( @@ -32,11 +33,12 @@ async def get_favorite_movies_by_user_id( key = await self.cache_key_service.build_list_key( entity=CacheEntity.favorite_movie, action="get", + version_params={"user_id": user_id}, user_id=user_id, page=page, size=size, ) - cached_favorite_movies_response = await self.cache_service.get( + cached_favorite_movies_response = await self.redis_service.get( key, FavoriteMovieWithMovieResponseList, ) @@ -54,7 +56,7 @@ async def get_favorite_movies_by_user_id( ) ) - await self.cache_service.set( + await self.redis_service.set( key, favorite_movies_response, ttl=24 * 60 * 60, @@ -70,7 +72,7 @@ async def get_favorite_movie_by_id( entity_id=favorite_movie_id, action="get", ) - cached_favorite_movie_response = await self.cache_service.get( + cached_favorite_movie_response = await self.redis_service.get( key, FavoriteMovieWithMovieResponse, ) @@ -82,7 +84,7 @@ async def get_favorite_movie_by_id( favorite_movie_id, ) ) - await self.cache_service.set( + await self.redis_service.set( key, favorite_movie_response, ttl=5 * 60, @@ -95,14 +97,14 @@ async def count_favorites_by_movie(self, movie_id: int) -> int: entity_id=movie_id, action="get-count", ) - cached_favorite_movie_response = await self.cache_service.get(key) + cached_favorite_movie_response = await self.redis_service.get(key) if cached_favorite_movie_response is not None: return cast(int, cached_favorite_movie_response) favorite_movie_response = ( await self.favorite_movie_service.count_favorites_by_movie(movie_id) ) - await self.cache_service.set( + await self.redis_service.set( key, favorite_movie_response, ttl=24 * 60 * 60, @@ -116,7 +118,7 @@ async def check_favorite_movie_status(self, user_id: int, movie_id: int) -> bool entity_id=entity_id, action="get-status", ) - cached_favorite_movie_response = await self.cache_service.get( + cached_favorite_movie_response = await self.redis_service.get( key, is_boolean=True, ) @@ -129,7 +131,7 @@ async def check_favorite_movie_status(self, user_id: int, movie_id: int) -> bool movie_id, ) ) - await self.cache_service.set( + await self.redis_service.set( key, favorite_movie_response, ttl=24 * 60 * 60, @@ -147,33 +149,29 @@ async def create_user_favorite_movie( create_favorite_movie_data, ) ) - pattern = await self.cache_key_service.build_list_regex_key( - entity=CacheEntity.favorite_movie, - action_regex="get", - user_id=user_id, - page="*", - size="*", - ) - await self.cache_service.delete_by_pattern(pattern) movie_id = favorite_movie_response.movie_id entity_id = (user_id, movie_id) - key = self.cache_key_service.build_item_key( + favorite_movie_status_key = self.cache_key_service.build_item_key( entity=CacheEntity.favorite_movie, entity_id=entity_id, action="get-status", ) - await self.cache_service.delete(key) - - movie_id = create_favorite_movie_data.movie_id - key = self.cache_key_service.build_item_key( + count_favorite_movie_key = self.cache_key_service.build_item_key( entity=CacheEntity.favorite_movie, entity_id=movie_id, action="get-count", ) - await self.cache_service.incr_by( - key, - amount=1, + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.favorite_movie, + user_id=user_id, + ), + self.redis_service.delete(favorite_movie_status_key), + self.redis_service.incr_by( + count_favorite_movie_key, + amount=1, + ), ) return favorite_movie_response @@ -186,40 +184,30 @@ async def delete_favorite_movie_by_id( user_id, favorite_movie_id, ) - pattern = await self.cache_key_service.build_list_regex_key( - entity=CacheEntity.favorite_movie, - action_regex="get", - user_id=user_id, - page="*", - size="*", - ) - await self.cache_service.delete_by_pattern(pattern) movie_id = favorite_movie.movie_id - key = self.cache_key_service.build_item_key( - entity=CacheEntity.favorite_movie, - entity_id=movie_id, - action="get-count", - ) - await self.cache_service.incr_by( - key, - amount=-1, - ) - entity_id = (user_id, movie_id) - key = self.cache_key_service.build_item_key( + favorite_movie_status_key = self.cache_key_service.build_item_key( entity=CacheEntity.favorite_movie, entity_id=entity_id, action="get-status", ) - await self.cache_service.delete(key) - - key = self.cache_key_service.build_item_key( + count_favorite_movie_key = self.cache_key_service.build_item_key( entity=CacheEntity.favorite_movie, - entity_id=favorite_movie_id, - action="get-status", + entity_id=movie_id, + action="get-count", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.favorite_movie, + user_id=user_id, + ), + self.redis_service.delete(favorite_movie_status_key), + self.redis_service.incr_by( + count_favorite_movie_key, + amount=-1, + ), ) - await self.cache_service.delete(key) return favorite_movie async def delete_user_favorite_movie( @@ -231,38 +219,27 @@ async def delete_user_favorite_movie( user_id, movie_id, ) - pattern = await self.cache_key_service.build_list_regex_key( - entity=CacheEntity.favorite_movie, - action_regex="get", - user_id=user_id, - page="*", - size="*", - ) - await self.cache_service.delete_by_pattern(pattern) - - favorite_movie_id = favorite_movie.id - key = self.cache_key_service.build_item_key( - entity=CacheEntity.favorite_movie, - entity_id=movie_id, - action="get-count", - ) - await self.cache_service.incr_by( - key, - amount=-1, - ) entity_id = (user_id, movie_id) - key = self.cache_key_service.build_item_key( + favorite_movie_status_key = self.cache_key_service.build_item_key( entity=CacheEntity.favorite_movie, entity_id=entity_id, action="get-status", ) - await self.cache_service.delete(key) - - key = self.cache_key_service.build_item_key( + count_favorite_movie_key = self.cache_key_service.build_item_key( entity=CacheEntity.favorite_movie, - entity_id=favorite_movie_id, - action="get-status", + entity_id=movie_id, + action="get-count", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.favorite_movie, + user_id=user_id, + ), + self.redis_service.delete(favorite_movie_status_key), + self.redis_service.incr_by( + count_favorite_movie_key, + amount=-1, + ), ) - await self.cache_service.delete(key) return favorite_movie diff --git a/app/cache_services/genre.py b/app/cache_services/genre.py index 4711911..6d0f0d4 100644 --- a/app/cache_services/genre.py +++ b/app/cache_services/genre.py @@ -39,7 +39,11 @@ async def get_genre_by_id(self, genre_id: int) -> GenreResponse: return cast(GenreResponse, cached_genre_response) genre_response = await self.genre_service.get_genre_by_id(genre_id) - await self.redis_service.set(key, genre_response, ttl=24 * 60 * 60) + await self.redis_service.set( + key, + genre_response, + ttl=24 * 60 * 60, + ) return genre_response async def get_all_genres( @@ -58,7 +62,11 @@ async def get_all_genres( return cast(GenreResponseList, cached_genres_response) genres_response = await self.genre_service.get_all_genres(size, page) - await self.redis_service.set(key, genres_response, ttl=24 * 60 * 60) + await self.redis_service.set( + key, + genres_response, + ttl=24 * 60 * 60, + ) return genres_response async def search_genres_by_name( @@ -83,7 +91,11 @@ async def search_genres_by_name( size, page, ) - await self.redis_service.set(key, genres_response, ttl=24 * 60 * 60) + await self.redis_service.set( + key, + genres_response, + ttl=24 * 60 * 60, + ) return genres_response async def create_genre(self, create_data: GenreCreate) -> GenreResponse: diff --git a/app/cache_services/movie.py b/app/cache_services/movie.py index 96c58bf..de3126c 100644 --- a/app/cache_services/movie.py +++ b/app/cache_services/movie.py @@ -1,8 +1,8 @@ +import asyncio from typing import cast -from packages.rabbitmq.constants import Exchange, ExchangeType, Queue -from packages.rabbitmq.utils import create_message - +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from dependencies.annotations.validators import PaginationPageDep, PaginationSizeDep from schemas.movie import ( @@ -22,16 +22,18 @@ class MovieCacheService: def __init__( self, movie_service: MovieService, - cache_service_for_movie: RedisService, - cache_service_for_watch_history: RedisService, + movie_redis_service: RedisService, + watch_history_redis_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.movie_service = movie_service - self.cache_service_for_movie = cache_service_for_movie - self.cache_service_for_watch_history = cache_service_for_watch_history + self.movie_redis_service = movie_redis_service + self.watch_history_redis_service = watch_history_redis_service + self.cache_key_service = cache_key_service async def get_movie_by_id(self, movie_id: int) -> MovieWithGenreResponse: key = RedisService.create_cache_key("movie", movie_id=movie_id) - cached_movie_response = await self.cache_service_for_movie.get( + cached_movie_response = await self.movie_redis_service.get( key, MovieWithGenreResponse, ) @@ -39,7 +41,7 @@ async def get_movie_by_id(self, movie_id: int) -> MovieWithGenreResponse: return cast(MovieWithGenreResponse, cached_movie_response) movie_response = await self.movie_service.get_movie_by_id(movie_id) - await self.cache_service_for_movie.set(key, movie_response, ttl=60) + await self.movie_redis_service.set(key, movie_response, ttl=60) return movie_response async def get_movies( @@ -48,7 +50,7 @@ async def get_movies( page: int = 1, ) -> MovieWithGenreResponseList: key = RedisService.create_cache_key("movies", size=size, page=page) - cached_movies_response = await self.cache_service_for_movie.get( + cached_movies_response = await self.movie_redis_service.get( key, MovieWithGenreResponseList, ) @@ -56,7 +58,7 @@ async def get_movies( return cast(MovieWithGenreResponseList, cached_movies_response) movies_response = await self.movie_service.get_movies(size, page) - await self.cache_service_for_movie.set(key, movies_response, ttl=60) + await self.movie_redis_service.set(key, movies_response, ttl=60) return movies_response async def search_movies_with_filters( @@ -71,7 +73,7 @@ async def search_movies_with_filters( page=page, **movie_filter.model_dump(), ) - cached_movies_response = await self.cache_service_for_movie.get( + cached_movies_response = await self.movie_redis_service.get( key, MovieWithGenreResponseList, ) @@ -83,7 +85,7 @@ async def search_movies_with_filters( size, page, ) - await self.cache_service_for_movie.set(key, movies_response, ttl=1800) + await self.movie_redis_service.set(key, movies_response, ttl=1800) return movies_response async def get_movies_by_genre_id( @@ -98,7 +100,7 @@ async def get_movies_by_genre_id( size=size, page=page, ) - cached_movies_response = await self.cache_service_for_movie.get( + cached_movies_response = await self.movie_redis_service.get( key, MovieResponseList, ) @@ -110,7 +112,7 @@ async def get_movies_by_genre_id( size, page, ) - await self.cache_service_for_movie.set(key, movies_response, ttl=1800) + await self.movie_redis_service.set(key, movies_response, ttl=1800) return movies_response async def watch_movie( @@ -124,20 +126,22 @@ async def watch_movie( ) key = RedisService.create_cache_key("watch_history") pattern = key + "*" - await self.cache_service_for_watch_history.delete_by_pattern(pattern) + await self.watch_history_redis_service.delete_by_pattern(pattern) - rabbitmq_service = self.movie_service.rabbitmq_service - exchange = await rabbitmq_service.declare_exchange( - name=Exchange.app, - type=ExchangeType.direct, - durable=True, + count_watch_history_key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=user_id, + action="get-count", ) - data = {"user_id": user_id} - message = create_message(data) - await rabbitmq_service.publish( - message=message, - routing_key=Queue.update_watch_history_cache_on_watch_movie.value, - exchange=exchange, + await asyncio.gather( + self.watch_history_redis_service.incr_by( + count_watch_history_key, + amount=1, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.watch_history, + user_id=user_id, + ), ) return movie_response @@ -148,7 +152,7 @@ async def create_movie( movie_response = await self.movie_service.create_movie(create_movie_data) key = RedisService.create_cache_key("movie") pattern = key + "*" - await self.cache_service_for_movie.delete_by_pattern(pattern) + await self.movie_redis_service.delete_by_pattern(pattern) return movie_response async def update_movie( @@ -162,7 +166,7 @@ async def update_movie( ) key = RedisService.create_cache_key("movie") pattern = key + "*" - await self.cache_service_for_movie.delete_by_pattern(pattern) + await self.movie_redis_service.delete_by_pattern(pattern) return movie_response async def partial_update_movie( @@ -176,11 +180,11 @@ async def partial_update_movie( ) key = RedisService.create_cache_key("movie") pattern = key + "*" - await self.cache_service_for_movie.delete_by_pattern(pattern) + await self.movie_redis_service.delete_by_pattern(pattern) return movie_response async def delete_movie_by_id(self, movie_id: int) -> None: await self.movie_service.delete_movie_by_id(movie_id) key = RedisService.create_cache_key("movie") pattern = key + "*" - await self.cache_service_for_movie.delete_by_pattern(pattern) + await self.movie_redis_service.delete_by_pattern(pattern) diff --git a/app/cache_services/review.py b/app/cache_services/review.py index 534e611..f123db1 100644 --- a/app/cache_services/review.py +++ b/app/cache_services/review.py @@ -21,11 +21,11 @@ class ReviewCacheService: def __init__( self, review_service: ReviewService, - cache_service: RedisService, + redis_service: RedisService, cache_key_service: CacheKeyService, ) -> None: self.review_service = review_service - self.cache_service = cache_service + self.redis_service = redis_service self.cache_key_service = cache_key_service async def get_reviews( @@ -39,7 +39,7 @@ async def get_reviews( size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -47,7 +47,7 @@ async def get_reviews( return cast(ReviewWithUserResponseList, cached_reviews_response) reviews_response = await self.review_service.get_reviews(size, page) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, ttl=30 * 60, @@ -60,7 +60,7 @@ async def get_review_by_id(self, review_id: int) -> ReviewWithUserResponse: entity_id=review_id, action="get", ) - cached_review_response = await self.cache_service.get( + cached_review_response = await self.redis_service.get( key, ReviewWithUserResponse, ) @@ -68,7 +68,7 @@ async def get_review_by_id(self, review_id: int) -> ReviewWithUserResponse: return cast(ReviewWithUserResponse, cached_review_response) review_response = await self.review_service.get_review_by_id(review_id) - await self.cache_service.set( + await self.redis_service.set( key, review_response, ttl=30 * 60, @@ -84,11 +84,12 @@ async def get_user_reviews( key = await self.cache_key_service.build_list_key( entity=CacheEntity.review, action="get", + version_params={"user_id": user_id}, user_id=user_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithMovieResponseList, ) @@ -100,7 +101,7 @@ async def get_user_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, ttl=30 * 60, @@ -118,7 +119,7 @@ async def get_user_review_about_movie( entity_id=entity_id, action="get", ) - cached_review_response = await self.cache_service.get(key, ReviewResponse) + cached_review_response = await self.redis_service.get(key, ReviewResponse) if cached_review_response is not None: return cast(ReviewResponse, cached_review_response) @@ -126,7 +127,7 @@ async def get_user_review_about_movie( user_id, movie_id, ) - await self.cache_service.set( + await self.redis_service.set( key, review_response, ttl=30 * 60, @@ -142,11 +143,12 @@ async def get_movie_reviews( key = await self.cache_key_service.build_list_key( entity=CacheEntity.review, action="get", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -158,7 +160,7 @@ async def get_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, ttl=30 * 60, @@ -174,11 +176,12 @@ async def get_low_rated_movie_reviews( key = await self.cache_key_service.build_list_key( entity=CacheEntity.review, action="get-low-rated", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -190,7 +193,7 @@ async def get_low_rated_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, ttl=30 * 60, @@ -206,11 +209,12 @@ async def get_top_rated_movie_reviews( key = await self.cache_key_service.build_list_key( entity=CacheEntity.review, action="get-top-rated", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -222,10 +226,10 @@ async def get_top_rated_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -238,11 +242,12 @@ async def get_top_newest_movie_reviews( key = await self.cache_key_service.build_list_key( entity=CacheEntity.review, action="get-top-newest", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -254,7 +259,7 @@ async def get_top_newest_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, ttl=30 * 60, @@ -270,13 +275,14 @@ async def get_top_oldest_movie_reviews( key = await self.cache_key_service.build_list_key( entity=CacheEntity.review, action="get-top-oldest", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) cached_reviews_response = cast( ReviewWithUserResponseList, - await self.cache_service.get(key, ReviewWithUserResponseList), + await self.redis_service.get(key, ReviewWithUserResponseList), ) if cached_reviews_response is not None: return cached_reviews_response @@ -286,7 +292,7 @@ async def get_top_oldest_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, ttl=30 * 60, @@ -302,13 +308,18 @@ async def create_review( user_id, create_review_data, ) - pattern = await self.cache_key_service.build_list_regex_key( - entity=CacheEntity.review, - action_regex="get*", - size="*", - page="*", + movie_id = review_response.movie_id + + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie_id, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + user_id=user_id, + ), ) - await self.cache_service.delete_by_pattern(pattern) return review_response async def update_review( @@ -322,20 +333,29 @@ async def update_review( review_id, update_review_data, ) - pattern = await self.cache_key_service.build_list_regex_key( + review_key = self.cache_key_service.build_item_key( entity=CacheEntity.review, - action_regex="get*", - size="*", - page="*", + entity_id=review_id, + action="get", ) - key = self.cache_key_service.build_item_key( + user_id, movie_id = review_response.user_id, review_response.movie_id + entity_id = (user_id, movie_id) + user_review_about_movie_key = self.cache_key_service.build_item_key( entity=CacheEntity.review, - entity_id=review_id, + entity_id=entity_id, action="get", ) await asyncio.gather( - self.cache_service.delete_by_pattern(pattern), - self.cache_service.delete(key), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie_id, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + user_id=current_user_id, + ), + self.redis_service.delete(review_key), + self.redis_service.delete(user_review_about_movie_key), ) return review_response @@ -350,20 +370,29 @@ async def partial_update_review( review_id, update_review_data, ) - pattern = await self.cache_key_service.build_list_regex_key( + review_key = self.cache_key_service.build_item_key( entity=CacheEntity.review, - action_regex="get*", - size="*", - page="*", + entity_id=review_id, + action="get", ) - key = self.cache_key_service.build_item_key( + user_id, movie_id = review_response.user_id, review_response.movie_id + entity_id = (user_id, movie_id) + user_review_about_movie_key = self.cache_key_service.build_item_key( entity=CacheEntity.review, - entity_id=review_id, + entity_id=entity_id, action="get", ) await asyncio.gather( - self.cache_service.delete_by_pattern(pattern), - self.cache_service.delete(key), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie_id, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + user_id=current_user_id, + ), + self.redis_service.delete(review_key), + self.redis_service.delete(user_review_about_movie_key), ) return review_response @@ -376,20 +405,27 @@ async def delete_review( current_user_id, review_id, ) - pattern = await self.cache_key_service.build_list_regex_key( - entity=CacheEntity.review, - action_regex="get*", - size="*", - page="*", - ) user_id, movie_id = review.user_id, review.movie_id entity_id = (user_id, movie_id) - key = self.cache_key_service.build_item_key( + review_key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=review_id, + action="get", + ) + user_review_about_movie_key = self.cache_key_service.build_item_key( entity=CacheEntity.review, entity_id=entity_id, action="get", ) await asyncio.gather( - self.cache_service.delete_by_pattern(pattern), - self.cache_service.delete(key), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie_id, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + user_id=current_user_id, + ), + self.redis_service.delete(review_key), + self.redis_service.delete(user_review_about_movie_key), ) diff --git a/app/cache_services/watch_history.py b/app/cache_services/watch_history.py index 9a1f785..71f89db 100644 --- a/app/cache_services/watch_history.py +++ b/app/cache_services/watch_history.py @@ -6,7 +6,6 @@ from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.watch_history import ( - WatchHistoryCreate, WatchHistoryWithMovieResponse, WatchHistoryWithMovieResponseList, ) @@ -17,11 +16,11 @@ class WatchHistoryCacheService: def __init__( self, watch_history_service: WatchHistoryService, - cache_service: RedisService, + redis_service: RedisService, cache_key_service: CacheKeyService, ) -> None: self.watch_history_service = watch_history_service - self.cache_service = cache_service + self.redis_service = redis_service self.cache_key_service = cache_key_service async def get_watch_history_by_id( @@ -33,7 +32,7 @@ async def get_watch_history_by_id( entity_id=watch_history_id, action="get", ) - cached_watch_history_response = await self.cache_service.get( + cached_watch_history_response = await self.redis_service.get( key, schema=WatchHistoryWithMovieResponse, ) @@ -43,7 +42,7 @@ async def get_watch_history_by_id( watch_history_response = ( await self.watch_history_service.get_watch_history_by_id(watch_history_id) ) - await self.cache_service.set( + await self.redis_service.set( key, watch_history_response, ttl=5 * 60, @@ -59,11 +58,12 @@ async def get_watch_history_list( key = await self.cache_key_service.build_list_key( entity=CacheEntity.watch_history, action="get", + version_params={"user_id": user_id}, user_id=user_id, size=size, page=page, ) - cached_watch_history_list_response = await self.cache_service.get( + cached_watch_history_list_response = await self.redis_service.get( key, WatchHistoryWithMovieResponseList, ) @@ -76,7 +76,7 @@ async def get_watch_history_list( watch_history_list_response = ( await self.watch_history_service.get_watch_history_list(user_id, size, page) ) - await self.cache_service.set( + await self.redis_service.set( key, watch_history_list_response, ttl=15 * 60, @@ -94,13 +94,14 @@ async def get_watch_history_by_date_range( key = await self.cache_key_service.build_list_key( entity=CacheEntity.watch_history, action="get", + version_params={"user_id": user_id}, user_id=user_id, start_date=start_date, end_date=end_date, size=size, page=page, ) - cached_watch_history_list_response = await self.cache_service.get( + cached_watch_history_list_response = await self.redis_service.get( key, WatchHistoryWithMovieResponseList, ) @@ -119,7 +120,7 @@ async def get_watch_history_by_date_range( page, ) ) - await self.cache_service.set( + await self.redis_service.set( key, watch_history_list_response, ttl=15 * 60, @@ -132,30 +133,28 @@ async def count_user_watch_history(self, user_id: int) -> int: entity_id=user_id, action="get-count", ) - cached_watch_history_response = await self.cache_service.get(key) + cached_watch_history_response = await self.redis_service.get(key) if cached_watch_history_response is not None: return cast(int, cached_watch_history_response) watch_history_response = ( await self.watch_history_service.count_user_watch_history(user_id) ) - await self.cache_service.set( + await self.redis_service.set( key, watch_history_response, ttl=24 * 60 * 60, ) return watch_history_response - async def add_movie_to_watch_history( - self, - user_id: int, - create_watch_history_data: WatchHistoryCreate, - ) -> WatchHistoryWithMovieResponse: ... - async def delete_watch_history_by_id( self, user_id: int, watch_history_id: int, ) -> None: + await self.watch_history_service.delete_watch_history_by_id( + user_id, + watch_history_id, + ) watch_history_count_key = self.cache_key_service.build_item_key( entity=CacheEntity.watch_history, entity_id=user_id, @@ -166,104 +165,34 @@ async def delete_watch_history_by_id( entity_id=watch_history_id, action="get", ) - user_watch_history_pattern_coroutine = ( - self.cache_key_service.build_list_regex_key( - entity=CacheEntity.watch_history, - action_regex="get", - user_id=user_id, - size="*", - page="*", - ) - ) - user_watch_history_range_date_pattern_coroutine = ( - self.cache_key_service.build_list_key( - entity=CacheEntity.watch_history, - action="get", - user_id=user_id, - start_date="*", - end_date="*", - size="*", - page="*", - ) - ) - user_watch_history_pattern, user_watch_history_range_date_pattern = ( - await asyncio.gather( - user_watch_history_pattern_coroutine, - user_watch_history_range_date_pattern_coroutine, - ) - ) - - delete_watch_history_item = ( - self.watch_history_service.delete_watch_history_by_id( - user_id, - watch_history_id, - ) - ) - change_watch_history_count_cache = self.cache_service.incr_by( + change_watch_history_count_cache = self.redis_service.incr_by( key=watch_history_count_key, amount=-1, ) - delete_watch_history_item_cache = self.cache_service.delete(watch_history_key) - delete_watch_history_cache = self.cache_service.delete_by_pattern( - user_watch_history_pattern, - ) - delete_watch_history_range_date_cache = self.cache_service.delete_by_pattern( - user_watch_history_range_date_pattern, - ) + delete_watch_history_item_cache = self.redis_service.delete(watch_history_key) await asyncio.gather( - delete_watch_history_item, change_watch_history_count_cache, delete_watch_history_item_cache, - delete_watch_history_cache, - delete_watch_history_range_date_cache, + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.watch_history, + user_id=user_id, + ), ) async def delete_user_watch_history(self, user_id: int) -> None: await self.watch_history_service.delete_user_watch_history(user_id) - watch_history_count_key = self.cache_key_service.build_item_key( entity=CacheEntity.watch_history, entity_id=user_id, action="get-count", ) - user_watch_history_pattern_coroutine = ( - self.cache_key_service.build_list_regex_key( - entity=CacheEntity.watch_history, - action_regex="get", - user_id=user_id, - size="*", - page="*", - ) - ) - user_watch_history_range_date_pattern_coroutine = ( - self.cache_key_service.build_list_key( - entity=CacheEntity.watch_history, - action="get", - user_id=user_id, - start_date="*", - end_date="*", - size="*", - page="*", - ) - ) - user_watch_history_pattern, user_watch_history_range_date_pattern = ( - await asyncio.gather( - user_watch_history_pattern_coroutine, - user_watch_history_range_date_pattern_coroutine, - ) - ) - - delete_watch_history_count_cache = self.cache_service.delete( + delete_watch_history_count_cache = self.redis_service.delete( key=watch_history_count_key, ) - delete_watch_history_cache = self.cache_service.delete_by_pattern( - user_watch_history_pattern, - ) - delete_watch_history_range_date_cache = self.cache_service.delete_by_pattern( - user_watch_history_range_date_pattern, - ) await asyncio.gather( delete_watch_history_count_cache, - delete_watch_history_cache, - delete_watch_history_range_date_cache, + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.watch_history, + user_id=user_id, + ), ) diff --git a/app/core/celery/tasks.py b/app/core/celery/tasks.py index 10c0da7..eec72ee 100644 --- a/app/core/celery/tasks.py +++ b/app/core/celery/tasks.py @@ -36,7 +36,7 @@ def create_chain_to_notify_inactive_users() -> None: task_chain.delay() -@app.task( +@app.task( # type: ignore[untyped-decorator] name=TaskType.delete_cached_movie_detail_data_by_genre_id.value, ) def delete_cached_movie_detail_data_by_genre(genre_id: int) -> None: diff --git a/app/core/celery/utils.py b/app/core/celery/utils.py index 9882d95..5ff6b4d 100644 --- a/app/core/celery/utils.py +++ b/app/core/celery/utils.py @@ -77,7 +77,7 @@ async def delete_cached_movie_detail_data_by_genre_id(genre_id: int) -> None: get_cache_key_service() as cache_key_service, ): movies = await movie_service.get_movies_by_genre_id(genre_id=genre_id) - movie_ids = [movie.id for movie in movies] + movie_ids = [str(movie.id) for movie in movies.movie_list] movie_ids_regex = "|".join(movie_ids) pattern = cache_key_service.build_item_regex_key( entity=CacheEntity.movie, diff --git a/app/core/rabbitmq/consumers.py b/app/core/rabbitmq/consumers.py index db826b0..67352a1 100644 --- a/app/core/rabbitmq/consumers.py +++ b/app/core/rabbitmq/consumers.py @@ -1,4 +1,3 @@ -import asyncio from collections.abc import Callable, Coroutine from typing import Any @@ -7,71 +6,15 @@ from packages.rabbitmq.utils import create_message, get_message from cache_services import GenreCacheService, MovieCacheService -from core.constants import AnyPydanticType, CacheEntity +from core.constants import AnyPydanticType from core.utils import ( - get_cache_key_service, get_genre_cache_service, get_movie_cache_service, - get_watch_history_redis_service, ) from schemas.genre import GenrePartialUpdate from schemas.movie import MoviePartialUpdate -async def update_watch_history_cache_on_watch_movie(message: IncomingMessage) -> None: - async with ( - message.process(), - get_watch_history_redis_service() as redis_service, - get_cache_key_service() as cache_key_service, - ): - data = get_message(message=message) - user_id = data["user_id"] - watch_history_count_key = cache_key_service.build_item_key( - entity=CacheEntity.watch_history, - entity_id=user_id, - action="get-count", - ) - user_watch_history_pattern_coroutine = cache_key_service.build_list_regex_key( - entity=CacheEntity.watch_history, - action_regex="get", - user_id=user_id, - size="*", - page="*", - ) - user_watch_history_range_date_pattern_coroutine = ( - cache_key_service.build_list_key( - entity=CacheEntity.watch_history, - action="get", - user_id=user_id, - start_date="*", - end_date="*", - size="*", - page="*", - ) - ) - user_watch_history_pattern, user_watch_history_range_date_pattern = ( - await asyncio.gather( - user_watch_history_pattern_coroutine, - user_watch_history_range_date_pattern_coroutine, - ) - ) - - delete_watch_history_count_cache = redis_service.delete( - key=watch_history_count_key, - ) - delete_watch_history_cache = redis_service.delete_by_pattern( - user_watch_history_pattern, - ) - delete_watch_history_range_date_cache = redis_service.delete_by_pattern( - user_watch_history_range_date_pattern, - ) - await asyncio.gather( - delete_watch_history_count_cache, - delete_watch_history_cache, - delete_watch_history_range_date_cache, - ) - - def update_media_factory( schema: AnyPydanticType, service_class: Any, diff --git a/app/core/rabbitmq/startup.py b/app/core/rabbitmq/startup.py index c38911a..4303947 100644 --- a/app/core/rabbitmq/startup.py +++ b/app/core/rabbitmq/startup.py @@ -8,7 +8,6 @@ update_genre_poster_url, update_movie_poster_url, update_movie_source_url, - update_watch_history_cache_on_watch_movie, ) @@ -32,29 +31,6 @@ async def rabbitmq_consumer_queues_startup() -> AsyncGenerator[None]: durable=True, ) - ### - app_exchange = await rabbitmq_service.declare_exchange( - name=Exchange.app, - type=ExchangeType.direct, - durable=True, - ) - queue_update_watch_history_cache_on_watch_movie = ( - await rabbitmq_service.declare_queue( - name=Queue.update_watch_history_cache_on_watch_movie, - durable=True, - ) - ) - await rabbitmq_service.bind( - queue_update_watch_history_cache_on_watch_movie, - app_exchange, - Queue.update_watch_history_cache_on_watch_movie.value, - ) - await rabbitmq_service.consume( - queue_update_watch_history_cache_on_watch_movie, - update_watch_history_cache_on_watch_movie, - ) - #### - await rabbitmq_service.bind( queue_update_genre_poster_url, exchange, diff --git a/app/core/redis/cache_key_service.py b/app/core/redis/cache_key_service.py index 7cf90da..b5b4800 100644 --- a/app/core/redis/cache_key_service.py +++ b/app/core/redis/cache_key_service.py @@ -1,3 +1,4 @@ +from datetime import date from typing import cast from core.config import settings @@ -11,8 +12,12 @@ class CacheKeyService: def __init__(self, redis: RedisService) -> None: self.redis = redis - async def __init_version(self, entity: CacheEntity) -> int: - key = self.__create_version_entity_key(entity) + async def __init_version( + self, + entity: CacheEntity, + **version_params: PrimitiveType, + ) -> int: + key = self.__create_version_entity_key(entity, **version_params) start_value = 1 await self.redis.set( key=key, @@ -20,31 +25,58 @@ async def __init_version(self, entity: CacheEntity) -> int: ) return start_value - async def __get_version(self, entity: CacheEntity) -> int: - key = self.__create_version_entity_key(entity) + async def __get_version( + self, + entity: CacheEntity, + **version_params: PrimitiveType, + ) -> int: + key = self.__create_version_entity_key(entity, **version_params) version = await self.redis.get(key, is_integer=True) return cast(int, version) - async def __update_version(self, entity: CacheEntity) -> None: - key = self.__create_version_entity_key(entity) + async def __update_version( + self, + entity: CacheEntity, + **version_params: PrimitiveType, + ) -> None: + key = self.__create_version_entity_key(entity, **version_params) await self.redis.incr_by(key, amount=1) @classmethod - def __create_version_entity_key(cls, entity: CacheEntity) -> str: - return f"{settings.service_name}:version:{cls.list_field}:{entity.value}" + def __create_version_entity_key( + cls, + entity: CacheEntity, + **version_params: PrimitiveType, + ) -> str: + version = "version" + version_entity_key_list = [ + settings.service_name, + version, + cls.list_field, + entity.value, + ] + for parameter_name, value in version_params.items(): + string_value = str(value) + version_entity_key_list.append(parameter_name) + version_entity_key_list.append(string_value) + + version_entity_key = ":".join(version_entity_key_list) + return version_entity_key async def invalidate_list_keys( self, entity: CacheEntity, + **version_params: PrimitiveType, ) -> None: await self.__update_version( entity=entity, + **version_params, ) @staticmethod def build_item_key( entity: CacheEntity, - entity_id: int | tuple, + entity_id: int | str | tuple[int, ...], action: str = "get", **params: PrimitiveType, ) -> str: @@ -58,7 +90,7 @@ def build_item_key( def build_item_regex_key( cls, entity: CacheEntity, - entity_id_regex: int | str, + entity_id_regex: int | str | tuple[int, ...], action_regex: str = "get", **params: PrimitiveType, ) -> str: @@ -73,11 +105,14 @@ async def build_list_key( self, entity: CacheEntity, action: str = "get", - **params: PrimitiveType, + version_params: dict[str, PrimitiveType] | None = None, + **params: PrimitiveType | date, ) -> str: - version = await self.__get_version(entity) + if version_params is None: + version_params = {} + version = await self.__get_version(entity, **version_params) if version is None: - version = await self.__init_version(entity) + version = await self.__init_version(entity, **version_params) key = f"{settings.service_name}:{entity.value}:{self.list_field}:version:{version}:{action}" # noqa: E501 for parameter_name, value in params.items(): @@ -89,10 +124,12 @@ async def build_list_regex_key( self, entity: CacheEntity, action_regex: str = "get", + version_params: dict[str, PrimitiveType] | None = None, **params: PrimitiveType, ) -> str: return await self.build_list_key( entity=entity, action=action_regex, + version_params=version_params, **params, ) diff --git a/app/core/redis/client.py b/app/core/redis/client.py index 409801d..e4fed4d 100644 --- a/app/core/redis/client.py +++ b/app/core/redis/client.py @@ -54,15 +54,12 @@ async def delete(self, key: str) -> None: await self._redis.delete(key) async def delete_list_of_keys(self, keys: list[str]) -> None: - await self._redis.delete(*keys) + if len(keys) > 0: + await self._redis.delete(*keys) async def delete_by_pattern(self, pattern: str) -> None: - keys_to_delete = [] - async for key in self._redis.scan_iter(match=pattern): - keys_to_delete.append(key) - - if len(keys_to_delete) > 0: - await self.delete_list_of_keys(keys_to_delete) + keys_to_delete = [key async for key in self._redis.scan_iter(match=pattern)] + await self.delete_list_of_keys(keys_to_delete) async def expire(self, key: str, ttl: int = 300) -> None: await self._redis.expire(key, ttl) diff --git a/app/core/utils.py b/app/core/utils.py index e312d83..5bb1c1a 100644 --- a/app/core/utils.py +++ b/app/core/utils.py @@ -128,12 +128,14 @@ async def get_watch_history_redis_service() -> AsyncGenerator[RedisService]: async def get_movie_cache_service() -> AsyncGenerator[MovieCacheService]: async with ( get_movie_service() as movie_service, - get_movie_redis_service() as cache_service_for_movies, - get_watch_history_redis_service() as cache_service_for_watch_history, + get_movie_redis_service() as movie_redis_service, + get_watch_history_redis_service() as watch_history_redis_service, + get_cache_key_service() as cache_key_service, ): movie_cache_service = MovieCacheService( movie_service, - cache_service_for_movies, - cache_service_for_watch_history, + movie_redis_service, + watch_history_redis_service, + cache_key_service, ) yield movie_cache_service diff --git a/app/dependencies/cache_services.py b/app/dependencies/cache_services.py index 53fee54..9852133 100644 --- a/app/dependencies/cache_services.py +++ b/app/dependencies/cache_services.py @@ -80,12 +80,17 @@ async def get_movie_cache_service( RedisService, Depends(get_watch_history_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[MovieCacheService]: try: movie_cache_service = MovieCacheService( movie_service, movie_redis_service, watch_history_redis_service, + cache_key_service, ) yield movie_cache_service finally: From 1ac3b4a7b3359795d54070001508a1c00c1b77ad Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Thu, 16 Jul 2026 21:51:29 +0300 Subject: [PATCH 12/13] Rework movie cache using cache key service. --- app/api/api_v1/movies/list_views.py | 18 ------ app/cache_services/genre.py | 16 +++-- app/cache_services/movie.py | 97 +++++++++++++++++++---------- app/core/celery/tasks.py | 8 +-- app/core/celery/utils.py | 21 ++++--- app/core/redis/service.py | 2 +- app/repositories/movie.py | 4 -- app/schemas/movie.py | 4 +- app/services/movie.py | 6 -- packages/celery/constants.py | 4 +- packages/rabbitmq/utils.py | 2 + 11 files changed, 98 insertions(+), 84 deletions(-) diff --git a/app/api/api_v1/movies/list_views.py b/app/api/api_v1/movies/list_views.py index 6d57e9b..828653a 100644 --- a/app/api/api_v1/movies/list_views.py +++ b/app/api/api_v1/movies/list_views.py @@ -11,7 +11,6 @@ from schemas.movie import ( MovieCreate, MovieFilter, - MovieResponseList, MovieWithGenreResponse, MovieWithGenreResponseList, ) @@ -73,23 +72,6 @@ async def search_movies_with_filters( ) -@router.get( - "/genre/{genre_id}", - response_model=MovieResponseList, - status_code=status.HTTP_200_OK, - dependencies=[ - Depends(check_rate_limit_not_auth), - ], -) -async def get_movies_by_genre_id( - genre_id: int, - movie_cache_service: MovieCacheServiceDep, - size: PaginationSizeDep = 10, - page: PaginationPageDep = 1, -) -> MovieResponseList: - return await movie_cache_service.get_movies_by_genre_id(genre_id, size, page) - - @router.post( "/", response_model=MovieWithGenreResponse, diff --git a/app/cache_services/genre.py b/app/cache_services/genre.py index 6d0f0d4..e91bb0f 100644 --- a/app/cache_services/genre.py +++ b/app/cache_services/genre.py @@ -119,6 +119,11 @@ async def update_genre( self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), self.redis_service.delete(key=key), ) + app.send_task( + args=[genre_id], + name=TaskType.invalidate_movie_detail_cache_by_genre.value, + queue=Queue.app.value, + ) return genre_response async def partial_update_genre( @@ -140,6 +145,11 @@ async def partial_update_genre( self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), self.redis_service.delete(key=key), ) + app.send_task( + args=[genre_id], + name=TaskType.invalidate_movie_detail_cache_by_genre.value, + queue=Queue.app.value, + ) return genre_response async def delete_genre_by_id(self, genre_id: int) -> None: @@ -154,9 +164,3 @@ async def delete_genre_by_id(self, genre_id: int) -> None: self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), self.redis_service.delete(key=key), ) - - app.apply_async( - args=[genre_id], - name=TaskType.delete_cached_movie_detail_data_by_genre_id.value, - queue=Queue.app.value, - ) diff --git a/app/cache_services/movie.py b/app/cache_services/movie.py index de3126c..d5dbb60 100644 --- a/app/cache_services/movie.py +++ b/app/cache_services/movie.py @@ -32,7 +32,11 @@ def __init__( self.cache_key_service = cache_key_service async def get_movie_by_id(self, movie_id: int) -> MovieWithGenreResponse: - key = RedisService.create_cache_key("movie", movie_id=movie_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie_id, + action="get", + ) cached_movie_response = await self.movie_redis_service.get( key, MovieWithGenreResponse, @@ -41,7 +45,11 @@ async def get_movie_by_id(self, movie_id: int) -> MovieWithGenreResponse: return cast(MovieWithGenreResponse, cached_movie_response) movie_response = await self.movie_service.get_movie_by_id(movie_id) - await self.movie_redis_service.set(key, movie_response, ttl=60) + await self.movie_redis_service.set( + key, + movie_response, + ttl=24 * 60 * 60, + ) return movie_response async def get_movies( @@ -49,7 +57,12 @@ async def get_movies( size: int = 10, page: int = 1, ) -> MovieWithGenreResponseList: - key = RedisService.create_cache_key("movies", size=size, page=page) + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.movie, + action="get", + size=size, + page=page, + ) cached_movies_response = await self.movie_redis_service.get( key, MovieWithGenreResponseList, @@ -58,7 +71,11 @@ async def get_movies( return cast(MovieWithGenreResponseList, cached_movies_response) movies_response = await self.movie_service.get_movies(size, page) - await self.movie_redis_service.set(key, movies_response, ttl=60) + await self.movie_redis_service.set( + key, + movies_response, + ttl=24 * 60 * 60, + ) return movies_response async def search_movies_with_filters( @@ -67,8 +84,9 @@ async def search_movies_with_filters( size: PaginationSizeDep = 10, page: PaginationPageDep = 1, ) -> MovieWithGenreResponseList: - key = RedisService.create_cache_key( - "movies", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.movie, + action="get-search", size=size, page=page, **movie_filter.model_dump(), @@ -85,20 +103,21 @@ async def search_movies_with_filters( size, page, ) - await self.movie_redis_service.set(key, movies_response, ttl=1800) + await self.movie_redis_service.set( + key, + movies_response, + ttl=24 * 60 * 60, + ) return movies_response async def get_movies_by_genre_id( self, genre_id: int, - size: int = 10, - page: int = 1, ) -> MovieResponseList: - key = RedisService.create_cache_key( - "movies", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.movie, + action="get-search", genre_id=genre_id, - size=size, - page=page, ) cached_movies_response = await self.movie_redis_service.get( key, @@ -109,10 +128,12 @@ async def get_movies_by_genre_id( movies_response = await self.movie_service.get_movies_by_genre_id( genre_id, - size, - page, ) - await self.movie_redis_service.set(key, movies_response, ttl=1800) + await self.movie_redis_service.set( + key, + movies_response, + ttl=24 * 60 * 60, + ) return movies_response async def watch_movie( @@ -124,10 +145,6 @@ async def watch_movie( user_id, create_watch_history_data, ) - key = RedisService.create_cache_key("watch_history") - pattern = key + "*" - await self.watch_history_redis_service.delete_by_pattern(pattern) - count_watch_history_key = self.cache_key_service.build_item_key( entity=CacheEntity.watch_history, entity_id=user_id, @@ -150,9 +167,7 @@ async def create_movie( create_movie_data: MovieCreate, ) -> MovieWithGenreResponse: movie_response = await self.movie_service.create_movie(create_movie_data) - key = RedisService.create_cache_key("movie") - pattern = key + "*" - await self.movie_redis_service.delete_by_pattern(pattern) + await self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie) return movie_response async def update_movie( @@ -164,9 +179,15 @@ async def update_movie( movie_id, update_movie_data, ) - key = RedisService.create_cache_key("movie") - pattern = key + "*" - await self.movie_redis_service.delete_by_pattern(pattern) + movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), + self.movie_redis_service.delete(movie_key), + ) return movie_response async def partial_update_movie( @@ -178,13 +199,25 @@ async def partial_update_movie( movie_id, update_movie_data, ) - key = RedisService.create_cache_key("movie") - pattern = key + "*" - await self.movie_redis_service.delete_by_pattern(pattern) + movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), + self.movie_redis_service.delete(movie_key), + ) return movie_response async def delete_movie_by_id(self, movie_id: int) -> None: await self.movie_service.delete_movie_by_id(movie_id) - key = RedisService.create_cache_key("movie") - pattern = key + "*" - await self.movie_redis_service.delete_by_pattern(pattern) + movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), + self.movie_redis_service.delete(movie_key), + ) diff --git a/app/core/celery/tasks.py b/app/core/celery/tasks.py index eec72ee..b2cacb7 100644 --- a/app/core/celery/tasks.py +++ b/app/core/celery/tasks.py @@ -4,8 +4,8 @@ from .celery_app import app from .utils import ( - delete_cached_movie_detail_data_by_genre_id, get_data_to_send_inactive_users_movie_selection, + invalidate_movie_detail_cache_by_genre_id, ) @@ -37,9 +37,9 @@ def create_chain_to_notify_inactive_users() -> None: @app.task( # type: ignore[untyped-decorator] - name=TaskType.delete_cached_movie_detail_data_by_genre_id.value, + name=TaskType.invalidate_movie_detail_cache_by_genre.value, ) -def delete_cached_movie_detail_data_by_genre(genre_id: int) -> None: +def invalidate_movie_detail_cache_by_genre(genre_id: int) -> None: sync_run_coroutine_function( - delete_cached_movie_detail_data_by_genre_id(genre_id), + invalidate_movie_detail_cache_by_genre_id(genre_id), ) diff --git a/app/core/celery/utils.py b/app/core/celery/utils.py index 5ff6b4d..4e5bde8 100644 --- a/app/core/celery/utils.py +++ b/app/core/celery/utils.py @@ -70,18 +70,21 @@ async def get_data_to_send_inactive_users_movie_selection() -> ( ) -async def delete_cached_movie_detail_data_by_genre_id(genre_id: int) -> None: +async def invalidate_movie_detail_cache_by_genre_id(genre_id: int) -> None: + await rabbitmq_connection_startup() async with ( get_movie_service() as movie_service, get_movie_redis_service() as movie_redis_service, get_cache_key_service() as cache_key_service, ): movies = await movie_service.get_movies_by_genre_id(genre_id=genre_id) - movie_ids = [str(movie.id) for movie in movies.movie_list] - movie_ids_regex = "|".join(movie_ids) - pattern = cache_key_service.build_item_regex_key( - entity=CacheEntity.movie, - entity_id_regex=movie_ids_regex, - action_regex="get", - ) - await movie_redis_service.delete_by_pattern(pattern) + invalidate_movie_keys = [] + for movie in movies.movie_list: + movie_key = cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie.id, + action="get", + ) + invalidate_movie_keys.append(movie_key) + + await movie_redis_service.delete_list_of_keys(invalidate_movie_keys) diff --git a/app/core/redis/service.py b/app/core/redis/service.py index 0d1869d..da5964a 100644 --- a/app/core/redis/service.py +++ b/app/core/redis/service.py @@ -81,7 +81,7 @@ async def delete(self, key: str) -> None: await self.redis.delete(key) async def delete_list_of_keys(self, keys: list[str]) -> None: - await self.redis.delete(*keys) + await self.redis.delete_list_of_keys(keys) async def delete_by_pattern(self, pattern: str) -> None: await self.redis.delete_by_pattern(pattern) diff --git a/app/repositories/movie.py b/app/repositories/movie.py index 0437d8b..4ec83e7 100644 --- a/app/repositories/movie.py +++ b/app/repositories/movie.py @@ -97,14 +97,10 @@ async def search_movies_with_filters( # noqa: C901 async def get_movies_by_genre_id( self, genre_id: int, - size: int = 10, - page: int = 1, ) -> list[Movie]: stmt = ( select(Movie) .where(Movie.genre_id == genre_id) - .limit(size) - .offset(size * (page - 1)) ) result = await self.session.execute(stmt) return list(result.scalars().all()) diff --git a/app/schemas/movie.py b/app/schemas/movie.py index 1ff20b3..dc1b069 100644 --- a/app/schemas/movie.py +++ b/app/schemas/movie.py @@ -70,8 +70,8 @@ class MovieResponseList(BaseModel): """ movie_list: list[MovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None class MovieWithGenreResponse(MovieResponse): diff --git a/app/services/movie.py b/app/services/movie.py index 8f44f63..2447df5 100644 --- a/app/services/movie.py +++ b/app/services/movie.py @@ -106,8 +106,6 @@ async def search_movies_with_filters( async def get_movies_by_genre_id( self, genre_id: int, - size: int = 10, - page: int = 1, ) -> MovieResponseList: if not await self.genre_repository.genre_id_exists(genre_id): raise MovieIdNotFoundError(genre_id) @@ -116,14 +114,10 @@ async def get_movies_by_genre_id( MovieResponse.model_validate(movie) for movie in await self.movie_repository.get_movies_by_genre_id( genre_id, - size, - page, ) ] return MovieResponseList( movie_list=movies, - size=size, - page=page, ) async def create_movie( diff --git a/packages/celery/constants.py b/packages/celery/constants.py index a3db525..86c75ff 100644 --- a/packages/celery/constants.py +++ b/packages/celery/constants.py @@ -25,6 +25,6 @@ class TaskType(StrEnum): ) send_inactive_users_email = "notification-service.email.send-inactive-users-email" - delete_cached_movie_detail_data_by_genre_id = ( - "movie-catalog.cache.delete-cached-movie-detail-data" + invalidate_movie_detail_cache_by_genre = ( + "movie-catalog.cache.invalidate-movie-detail-cache-by-genre" ) diff --git a/packages/rabbitmq/utils.py b/packages/rabbitmq/utils.py index 18055f0..5143039 100644 --- a/packages/rabbitmq/utils.py +++ b/packages/rabbitmq/utils.py @@ -52,6 +52,8 @@ def create_queue_name( @asynccontextmanager async def get_channel() -> AsyncGenerator[AbstractChannel]: + # if connection.RABBIT_MQ_CONNECTION is None: + # await rabbitmq_connection_startup() assert connection.RABBIT_MQ_CONNECTION is not None async with cast( AbstractChannel, From eff5264002b3b616e4d82485a62b0dfa9b8218fa Mon Sep 17 00:00:00 2001 From: Nikolay Shirokov Date: Fri, 17 Jul 2026 17:06:08 +0300 Subject: [PATCH 13/13] Rework user cache using cache key service. --- app/cache_services/user.py | 122 +++++++++++++++++++++++------ app/core/celery/tasks.py | 10 +++ app/core/celery/utils.py | 19 +++++ app/dependencies/cache_services.py | 10 ++- app/repositories/movie.py | 12 +-- app/schemas/favorite_movie.py | 8 +- app/schemas/genre.py | 4 +- app/schemas/movie.py | 4 +- app/schemas/review.py | 12 +-- app/schemas/user.py | 4 +- app/schemas/watch_history.py | 8 +- app/services/movie.py | 11 +++ packages/celery/constants.py | 4 +- 13 files changed, 175 insertions(+), 53 deletions(-) diff --git a/app/cache_services/user.py b/app/cache_services/user.py index 548c2ff..80b921a 100644 --- a/app/cache_services/user.py +++ b/app/cache_services/user.py @@ -1,5 +1,11 @@ +import asyncio from typing import cast +from packages.celery.constants import Queue, TaskType + +from core.celery.celery_app import app +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.user import ( UserCreate, @@ -15,39 +21,66 @@ class UserCacheService: def __init__( self, user_service: UserService, - cache_service: RedisService, + redis_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.user_service = user_service - self.cache_service = cache_service + self.redis_service = redis_service + self.cache_key_service = cache_key_service async def get_user_by_id(self, user_id: int) -> UserResponse: - key = RedisService.create_cache_key("user", user_id=user_id) - cached_user_response = await self.cache_service.get(key, UserResponse) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=user_id, + action="get", + ) + cached_user_response = await self.redis_service.get(key, UserResponse) if cached_user_response is not None: return cast(UserResponse, cached_user_response) user_response = await self.user_service.get_user_by_id(user_id) - await self.cache_service.set(key, user_response) + await self.redis_service.set( + key, + user_response, + ttl=24 * 60 * 60, + ) return user_response async def get_user_by_login(self, login: str) -> UserResponse: - key = RedisService.create_cache_key("user", login=login) - cached_user_response = await self.cache_service.get(key, UserResponse) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=login, + action="get-by-login", + ) + cached_user_response = await self.redis_service.get(key, UserResponse) if cached_user_response is not None: return cast(UserResponse, cached_user_response) user_response = await self.user_service.get_user_by_login(login) - await self.cache_service.set(key, user_response) + await self.redis_service.set( + key, + user_response, + ttl=24 * 60 * 60, + ) return user_response async def get_all_users(self, size: int = 10, page: int = 1) -> UserResponseList: - key = RedisService.create_cache_key("users", size=size, page=page) - cached_users_response = await self.cache_service.get(key, UserResponseList) + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.user, + action="get", + size=size, + page=page, + ) + cached_users_response = await self.redis_service.get(key, UserResponseList) if cached_users_response is not None: return cast(UserResponseList, cached_users_response) users_response = await self.user_service.get_all_users(size, page) - await self.cache_service.set(key, users_response) + await self.redis_service.set( + key, + users_response, + ttl=12 * 60 * 60, + ) return users_response async def create_user( @@ -55,9 +88,7 @@ async def create_user( user_create_data: UserCreate, ) -> UserResponse: user_response = await self.user_service.create_user(user_create_data) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + await self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user) return user_response async def update_user( @@ -66,9 +97,20 @@ async def update_user( update_data: UserUpdate, ) -> UserResponse: user_response = await self.user_service.update_user(user_id, update_data) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + user_key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=user_id, + action="get", + ) + await asyncio.gather( + self.redis_service.delete(user_key), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user), + ) + app.send_task( + args=[user_id], + name=TaskType.invalidate_reviews_cache_on_update_user.value, + queue=Queue.app.value, + ) return user_response async def partial_update_user( @@ -80,19 +122,47 @@ async def partial_update_user( user_id, update_data, ) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + user_key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=user_id, + action="get", + ) + await asyncio.gather( + self.redis_service.delete(user_key), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user), + ) + app.send_task( + args=[user_id], + name=TaskType.invalidate_reviews_cache_on_update_user.value, + queue=Queue.app.value, + ) return user_response async def delete_user_by_id(self, user_id: int) -> None: await self.user_service.delete_user_by_id(user_id) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + user_key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=user_id, + action="get", + ) + await asyncio.gather( + self.redis_service.delete(user_key), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user), + ) + app.send_task( + args=[user_id], + name=TaskType.invalidate_reviews_cache_on_update_user.value, + queue=Queue.app.value, + ) async def delete_user_by_login(self, login: str) -> None: await self.user_service.delete_user_by_login(login) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + user_key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=login, + action="get-by-login", + ) + await asyncio.gather( + self.redis_service.delete(user_key), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user), + ) diff --git a/app/core/celery/tasks.py b/app/core/celery/tasks.py index b2cacb7..b1e7830 100644 --- a/app/core/celery/tasks.py +++ b/app/core/celery/tasks.py @@ -6,6 +6,7 @@ from .utils import ( get_data_to_send_inactive_users_movie_selection, invalidate_movie_detail_cache_by_genre_id, + invalidate_reviews_cache_on_update_user, ) @@ -43,3 +44,12 @@ def invalidate_movie_detail_cache_by_genre(genre_id: int) -> None: sync_run_coroutine_function( invalidate_movie_detail_cache_by_genre_id(genre_id), ) + + +@app.task( # type: ignore[untyped-decorator] + name=TaskType.invalidate_reviews_cache_on_update_user.value, +) +def invalidate_movie_reviews_cache_on_update_user(user_id: int) -> None: + sync_run_coroutine_function( + invalidate_reviews_cache_on_update_user(user_id), + ) diff --git a/app/core/celery/utils.py b/app/core/celery/utils.py index 4e5bde8..73ab3e7 100644 --- a/app/core/celery/utils.py +++ b/app/core/celery/utils.py @@ -1,3 +1,4 @@ +import asyncio from asyncio import gather from packages.rabbitmq.connection import rabbitmq_connection_startup @@ -88,3 +89,21 @@ async def invalidate_movie_detail_cache_by_genre_id(genre_id: int) -> None: invalidate_movie_keys.append(movie_key) await movie_redis_service.delete_list_of_keys(invalidate_movie_keys) + + +async def invalidate_reviews_cache_on_update_user(user_id: int) -> None: + await rabbitmq_connection_startup() + async with ( + get_movie_service() as movie_service, + get_cache_key_service() as cache_key_service, + ): + movies = await movie_service.get_movies_reviewed_by_user(user_id=user_id) + invalidate_review_coroutines = [] + for movie in movies.movie_list: + coroutine = cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie.id, + ) + invalidate_review_coroutines.append(coroutine) + + await asyncio.gather(*invalidate_review_coroutines) diff --git a/app/dependencies/cache_services.py b/app/dependencies/cache_services.py index 9852133..251ed21 100644 --- a/app/dependencies/cache_services.py +++ b/app/dependencies/cache_services.py @@ -189,9 +189,17 @@ async def get_user_cache_service( RedisService, Depends(get_user_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[UserCacheService]: try: - user_cache_service = UserCacheService(user_service, user_redis_service) + user_cache_service = UserCacheService( + user_service, + user_redis_service, + cache_key_service, + ) yield user_cache_service finally: """ diff --git a/app/repositories/movie.py b/app/repositories/movie.py index 4ec83e7..bbc8150 100644 --- a/app/repositories/movie.py +++ b/app/repositories/movie.py @@ -4,7 +4,7 @@ from core.constants import SortMonotony, SortType from dependencies.annotations.validators import PaginationPageDep, PaginationSizeDep -from models import Movie +from models import Movie, Review from schemas.movie import MovieCreate, MovieFilter, MoviePartialUpdate, MovieUpdate @@ -49,6 +49,11 @@ async def get_movies( result = await self.session.execute(stmt) return list(result.scalars().all()) + async def get_movies_reviewed_by_user(self, user_id: int) -> list[Movie]: + stmt = select(Movie).join(Review).where(Review.user_id == user_id) + result = await self.session.execute(stmt) + return list(result.scalars().all()) + async def search_movies_with_filters( # noqa: C901 self, movie_filter: MovieFilter, @@ -98,10 +103,7 @@ async def get_movies_by_genre_id( self, genre_id: int, ) -> list[Movie]: - stmt = ( - select(Movie) - .where(Movie.genre_id == genre_id) - ) + stmt = select(Movie).where(Movie.genre_id == genre_id) result = await self.session.execute(stmt) return list(result.scalars().all()) diff --git a/app/schemas/favorite_movie.py b/app/schemas/favorite_movie.py index 55009a9..78c11e3 100644 --- a/app/schemas/favorite_movie.py +++ b/app/schemas/favorite_movie.py @@ -36,8 +36,8 @@ class FavoriteMovieResponseList(BaseModel): """ favorite_movie_list: list[FavoriteMovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None class FavoriteMovieWithMovieResponse(FavoriteMovieResponse): @@ -55,5 +55,5 @@ class FavoriteMovieWithMovieResponseList(BaseModel): """ favorite_movie_list: list[FavoriteMovieWithMovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/schemas/genre.py b/app/schemas/genre.py index 805c4d9..7bf4bb7 100644 --- a/app/schemas/genre.py +++ b/app/schemas/genre.py @@ -52,5 +52,5 @@ class GenreResponseList(BaseModel): """ genre_list: list[GenreResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/schemas/movie.py b/app/schemas/movie.py index dc1b069..b3d9754 100644 --- a/app/schemas/movie.py +++ b/app/schemas/movie.py @@ -88,8 +88,8 @@ class MovieWithGenreResponseList(BaseModel): """ movie_list: list[MovieWithGenreResponse] - size: int - page: int + size: int | None = None + page: int | None = None class MovieFilter(BaseModel): diff --git a/app/schemas/review.py b/app/schemas/review.py index 2d9e426..0248179 100644 --- a/app/schemas/review.py +++ b/app/schemas/review.py @@ -59,8 +59,8 @@ class ReviewResponseList(BaseModel): """ review_list: list[ReviewResponse] - size: int - page: int + size: int | None = None + page: int | None = None class ReviewWithUserResponse(ReviewResponse): @@ -77,8 +77,8 @@ class ReviewWithUserResponseList(BaseModel): """ review_list: list[ReviewWithUserResponse] - size: int - page: int + size: int | None = None + page: int | None = None class ReviewWithMovieResponse(ReviewResponse): @@ -95,5 +95,5 @@ class ReviewWithMovieResponseList(BaseModel): """ review_list: list[ReviewWithMovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/schemas/user.py b/app/schemas/user.py index ce2320b..cb97cd4 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -77,5 +77,5 @@ class UserResponseList(BaseModel): """ user_list: list[UserResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/schemas/watch_history.py b/app/schemas/watch_history.py index 20b06f3..d5614ed 100644 --- a/app/schemas/watch_history.py +++ b/app/schemas/watch_history.py @@ -37,8 +37,8 @@ class WatchHistoryResponseList(BaseModel): """ watch_history_list: list[WatchHistoryResponse] - size: int - page: int + size: int | None = None + page: int | None = None class WatchHistoryWithMovieResponse(WatchHistoryResponse): @@ -55,5 +55,5 @@ class WatchHistoryWithMovieResponseList(BaseModel): """ watch_history_list: list[WatchHistoryWithMovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/services/movie.py b/app/services/movie.py index 2447df5..c7045d0 100644 --- a/app/services/movie.py +++ b/app/services/movie.py @@ -83,6 +83,17 @@ async def get_movies( page=page, ) + async def get_movies_reviewed_by_user(self, user_id: int) -> MovieResponseList: + movies = [ + MovieResponse.model_validate(movie) + for movie in await self.movie_repository.get_movies_reviewed_by_user( + user_id, + ) + ] + return MovieResponseList( + movie_list=movies, + ) + async def search_movies_with_filters( self, movie_filter: MovieFilter, diff --git a/packages/celery/constants.py b/packages/celery/constants.py index 86c75ff..e47898c 100644 --- a/packages/celery/constants.py +++ b/packages/celery/constants.py @@ -24,7 +24,9 @@ class TaskType(StrEnum): "movie-catalog.mailing-list.get-data-to-send-inactive-users-email" ) send_inactive_users_email = "notification-service.email.send-inactive-users-email" - invalidate_movie_detail_cache_by_genre = ( "movie-catalog.cache.invalidate-movie-detail-cache-by-genre" ) + invalidate_reviews_cache_on_update_user = ( + "movie-catalog.cache.invalidate-reviews-cache-on-update-user" + )