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/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_key_service.py b/app/core/redis/cache_key_service.py new file mode 100644 index 0000000..70aac49 --- /dev/null +++ b/app/core/redis/cache_key_service.py @@ -0,0 +1,71 @@ +from typing import cast + +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 __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 + await self.redis.set( + key=key, + value=start_value, + ) + return start_value + + @classmethod + def __create_version_entity_key(cls, entity: str) -> str: + return f"{settings.service_name}:version:{cls.list_field}:{entity}" + + async def invalidate_list_keys( + self, + entity: str, + ) -> None: + await self.__update_version( + entity=entity, + ) + + @staticmethod + def build_item_key( + entity: str, + entity_id: int, + action: str = "get", + **params: PrimitiveType, + ) -> str: + 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 = "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 + + 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, ) 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) +