From 43eca19d91902e1fd0b026a00dd8af59265b4737 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 5 Feb 2024 03:40:12 +0300 Subject: [PATCH] =?UTF-8?q?REDIS=20=D0=BA=D1=8D=D1=88=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5,=20=D0=BD=D0=B0=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=BB=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastfood/repository/menu.py | 3 +- fastfood/repository/redis.py | 55 ++++++++++++++++++--- fastfood/service/dish.py | 96 ++++++++++++++++++++++++++++++------ fastfood/service/menu.py | 49 ++++++++++++++---- fastfood/service/submenu.py | 84 ++++++++++++++++++++++++------- 5 files changed, 234 insertions(+), 53 deletions(-) diff --git a/fastfood/repository/menu.py b/fastfood/repository/menu.py index b7dad66..a7937a8 100644 --- a/fastfood/repository/menu.py +++ b/fastfood/repository/menu.py @@ -60,8 +60,7 @@ class MenuRepository: updated_menu = await self.db.execute(qr) return updated_menu.scalar_one() - async def delete_menu_item(self, menu_id: UUID) -> int: + async def delete_menu_item(self, menu_id: UUID): query = delete(Menu).where(Menu.id == menu_id) await self.db.execute(query) await self.db.commit() - return 200 diff --git a/fastfood/repository/redis.py b/fastfood/repository/redis.py index 037be4d..a560efd 100644 --- a/fastfood/repository/redis.py +++ b/fastfood/repository/redis.py @@ -1,3 +1,4 @@ +import pickle from typing import Any import redis.asyncio as redis # type: ignore @@ -6,19 +7,59 @@ from fastapi import BackgroundTasks, Depends from fastfood.dbase import get_redis_pool +def get_key(level: str, **kwargs) -> str: + match level: + case 'menus': + return 'MENUS' + case 'menu': + return f"{kwargs.get('menu_id')}" + case 'submenus': + return f"{kwargs.get('menu_id')}:SUBMENUS" + case 'submenu': + return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}" + case 'dishes': + return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:DISHES" + case 'dish': + return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:{kwargs.get('dish_id')}" + + return 'abracadabra' + + class RedisRepository: def __init__( self, - redis_pool: redis.Redis = Depends(get_redis_pool), + pool: redis.Redis = Depends(get_redis_pool), ) -> None: - self.redis_pool = redis_pool + self.pool = pool self.ttl = 1800 async def get(self, key: str) -> Any | None: - + data = await self.pool.get(key) + if data is not None: + return pickle.loads(data) return None - async def set( - self, key: str, value: Any, background_tasks: BackgroundTasks - ) -> None: - pass + async def set(self, key: str, value: Any, bg_task: BackgroundTasks) -> None: + data = pickle.dumps(value) + bg_task.add_task(self._set_cache, key, data) + + async def _set_cache(self, key: str, data: Any) -> None: + await self.pool.setex(key, self.ttl, data) + + async def delete(self, key: str, bg_task: BackgroundTasks) -> None: + bg_task.add_task(self._delete_cache, key) + + async def _delete_cache(self, key: str) -> None: + await self.pool.delete(key) + + async def clear_cache(self, pattern: str, bg_task: BackgroundTasks) -> None: + keys = [key async for key in self.pool.scan_iter(pattern)] + if keys: + bg_task.add_task(self._clear_keys, keys) + + async def _clear_keys(self, keys: list[str]) -> None: + await self.pool.delete(*keys) + + async def invalidate(self, key: str, bg_task: BackgroundTasks) -> None: + await self.clear_cache(f'{key}*', bg_task) + await self.clear_cache(f'{get_key("menus")}*', bg_task) diff --git a/fastfood/service/dish.py b/fastfood/service/dish.py index 675f805..5b921ed 100644 --- a/fastfood/service/dish.py +++ b/fastfood/service/dish.py @@ -5,7 +5,7 @@ from fastapi import BackgroundTasks, Depends from fastfood.dbase import get_async_redis_client from fastfood.repository.dish import DishRepository -from fastfood.repository.redis import RedisRepository +from fastfood.repository.redis import RedisRepository, get_key from fastfood.schemas import Dish, Dish_db, DishBase @@ -17,16 +17,32 @@ class DishService: background_tasks: BackgroundTasks = None, ) -> None: self.dish_repo = dish_repo - self.cache_client = RedisRepository(redis_client) - self.background_tasks = background_tasks + self.cache = RedisRepository(redis_client) + self.bg_tasks = background_tasks + self.key = get_key async def read_dishes(self, menu_id: UUID, submenu_id: UUID) -> list[Dish]: + cached_dishes = await self.cache.get( + self.key('dishes', menu_id=str(menu_id), submenu_id=str(submenu_id)) + ) + if cached_dishes is not None: + return cached_dishes + data = await self.dish_repo.get_dishes(menu_id, submenu_id) response = [] for row in data: dish = row.__dict__ dish['price'] = str(dish['price']) response.append(Dish(**dish)) + await self.cache.set( + self.key( + 'dishes', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + ), + response, + self.bg_tasks, + ) return response async def create_dish( @@ -35,34 +51,79 @@ class DishService: submenu_id: UUID, dish_data: DishBase, ) -> Dish: - dish = Dish_db(**dish_data.model_dump()) + dish_db = Dish_db(**dish_data.model_dump()) data = await self.dish_repo.create_dish_item( menu_id, submenu_id, - dish, + dish_db, ) - response = data.__dict__ - response['price'] = str(response['price']) - return Dish(**response) + dish = data.__dict__ + dish['price'] = str(dish['price']) + dish = Dish(**dish) + await self.cache.set( + self.key('dish', menu_id=str(menu_id), submenu_id=str(submenu_id)), + dish, + self.bg_tasks, + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + + return dish async def read_dish( self, menu_id: UUID, submenu_id: UUID, dish_id: UUID ) -> Dish | None: + cached_dish = await self.cache.get( + self.key( + 'dish', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + dish_id=str(dish_id), + ) + ) + if cached_dish is not None: + return cached_dish + data = await self.dish_repo.get_dish_item(menu_id, submenu_id, dish_id) if data is None: return None - response = data.__dict__ - response['price'] = str(response['price']) - return Dish(**response) + dish = data.__dict__ + dish['price'] = str(dish['price']) + dish = Dish(**dish) + await self.cache.set( + self.key( + 'dish', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + dish_id=str(dish_id), + ), + dish, + self.bg_tasks, + ) + return dish async def update_dish( self, menu_id: UUID, submenu_id: UUID, dish_id, dish_data: DishBase ) -> Dish: - dish = Dish_db(**dish_data.model_dump()) - data = await self.dish_repo.update_dish_item(menu_id, submenu_id, dish_id, dish) - response = data.__dict__ - response['price'] = str(response['price']) - return Dish(**response) + dish_db = Dish_db(**dish_data.model_dump()) + data = await self.dish_repo.update_dish_item( + menu_id, submenu_id, dish_id, dish_db + ) + dish = data.__dict__ + dish['price'] = str(dish['price']) + dish = Dish(**dish) + await self.cache.set( + self.key( + 'dish', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + dish_id=str(dish_id), + ), + dish, + self.bg_tasks, + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + + return dish async def del_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID) -> int: response = await self.dish_repo.delete_dish_item( @@ -70,4 +131,7 @@ class DishService: submenu_id, dish_id, ) + await self.cache.delete(key=str(menu_id), bg_task=self.bg_tasks) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + return response diff --git a/fastfood/service/menu.py b/fastfood/service/menu.py index e743afa..48147e6 100644 --- a/fastfood/service/menu.py +++ b/fastfood/service/menu.py @@ -5,7 +5,7 @@ from fastapi import BackgroundTasks, Depends from fastfood.dbase import get_async_redis_client from fastfood.repository.menu import MenuRepository -from fastfood.repository.redis import RedisRepository +from fastfood.repository.redis import RedisRepository, get_key from fastfood.schemas import MenuBase, MenuRead @@ -17,10 +17,15 @@ class MenuService: background_tasks: BackgroundTasks = None, ) -> None: self.menu_repo = menu_repo - self.cache_client = RedisRepository(redis_client) - self.background_tasks = background_tasks + self.cache = RedisRepository(redis_client) + self.key = get_key + self.bg_tasks = background_tasks async def read_menus(self) -> list[MenuRead]: + cached_menus = await self.cache.get(self.key('menus')) + if cached_menus is not None: + return cached_menus + data = await self.menu_repo.get_menus() menus = [] for r in data: @@ -34,6 +39,8 @@ class MenuService: menu['dishes_count'] = dishes_conter menu = MenuRead(**menu) menus.append(menu) + + await self.cache.set(self.key('menus'), menus, self.bg_tasks) return menus async def create_menu(self, menu_data: MenuBase) -> MenuRead: @@ -46,10 +53,23 @@ class MenuService: dishes_conter += len(sub.dishes) menu['submenus_count'] = len(menu.pop('submenus')) menu['dishes_count'] = dishes_conter - - return MenuRead(**menu) + await self.cache.set( + key=get_key('menu', menu_id=str(menu.get('id'))), + value=menu, + bg_task=self.bg_tasks, + ) + menu = MenuRead(**menu) + await self.cache.set( + self.key('menu', menu_id=str(menu.id)), menu, self.bg_tasks + ) + await self.cache.invalidate(key=str(menu.id), bg_task=self.bg_tasks) + return menu async def read_menu(self, menu_id: UUID) -> MenuRead | None: + cached_menu = await self.cache.get(self.key('menu', menu_id=str(menu_id))) + if cached_menu is not None: + return cached_menu + data = await self.menu_repo.get_menu_item(menu_id) if data is None: return None @@ -61,8 +81,11 @@ class MenuService: dishes_conter += len(sub.dishes) menu['submenus_count'] = len(menu.pop('submenus')) menu['dishes_count'] = dishes_conter - - return MenuRead(**menu) + menu = MenuRead(**menu) + await self.cache.set( + self.key('menu', menu_id=str(menu.id)), menu, self.bg_tasks + ) + return menu async def update_menu(self, menu_id: UUID, menu_data) -> MenuRead: data = await self.menu_repo.update_menu_item(menu_id, menu_data) @@ -74,9 +97,15 @@ class MenuService: dishes_conter += len(sub.dishes) menu['submenus_count'] = len(menu.pop('submenus')) menu['dishes_count'] = dishes_conter + menu = MenuRead(**menu) + await self.cache.set( + self.key('menu', menu_id=str(menu.id)), menu, self.bg_tasks + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + return menu - return MenuRead(**menu) - - async def del_menu(self, menu_id: UUID) -> int: + async def del_menu(self, menu_id: UUID): data = await self.menu_repo.delete_menu_item(menu_id) + await self.cache.delete(key=str(menu_id), bg_task=self.bg_tasks) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) return data diff --git a/fastfood/service/submenu.py b/fastfood/service/submenu.py index ad08b47..db23343 100644 --- a/fastfood/service/submenu.py +++ b/fastfood/service/submenu.py @@ -4,7 +4,7 @@ import redis.asyncio as redis # type: ignore from fastapi import BackgroundTasks, Depends from fastfood.dbase import get_async_redis_client -from fastfood.repository.redis import RedisRepository +from fastfood.repository.redis import RedisRepository, get_key from fastfood.repository.submenu import SubMenuRepository from fastfood.schemas import MenuBase, SubMenuRead @@ -18,10 +18,17 @@ class SubmenuService: ) -> None: self.submenu_repo = submenu_repo - self.cache_client = RedisRepository(redis_client) - self.background_tasks = background_tasks + self.cache = RedisRepository(redis_client) + self.bg_tasks = background_tasks + self.key = get_key async def read_submenus(self, menu_id: UUID) -> list[SubMenuRead]: + cached_submenus = await self.cache.get( + self.key('submenus', menu_id=str(menu_id)) + ) + if cached_submenus is not None: + return cached_submenus + data = await self.submenu_repo.get_submenus(menu_id=menu_id) submenus = [] for r in data: @@ -31,6 +38,10 @@ class SubmenuService: submenu['dishes_count'] = len(subq.dishes) submenu = SubMenuRead(**submenu) submenus.append(submenu) + + await self.cache.set( + self.key('submenus', menu_id=str(menu_id)), submenus, self.bg_tasks + ) return submenus async def create_submenu( @@ -40,20 +51,40 @@ class SubmenuService: menu_id, submenu_data, ) - menu = data.__dict__ - menu = {k: v for k, v in menu.items() if not k.startswith('_')} - menu['dishes_count'] = len(menu.pop('dishes')) - menu = SubMenuRead(**menu) - return menu + submenu = data.__dict__ + submenu = {k: v for k, v in submenu.items() if not k.startswith('_')} + submenu['dishes_count'] = len(submenu.pop('dishes')) + submenu = SubMenuRead(**submenu) + await self.cache.set( + self.key('submenu', menu_id=str(menu_id)), submenu, self.bg_tasks + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + + return submenu async def read_menu(self, menu_id: UUID, submenu_id: UUID) -> SubMenuRead | None: + cached_submenu = await self.cache.get( + self.key( + 'submenu', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + ) + ) + if cached_submenu is not None: + return cached_submenu + data = await self.submenu_repo.get_submenu_item(menu_id, submenu_id) if data is None: return None - menu = data.__dict__ - menu = {k: v for k, v in menu.items() if not k.startswith('_')} - menu['dishes_count'] = len(menu.pop('dishes')) - menu = SubMenuRead(**menu) + submenu = data.__dict__ + submenu = {k: v for k, v in submenu.items() if not k.startswith('_')} + submenu['dishes_count'] = len(submenu.pop('dishes')) + menu = SubMenuRead(**submenu) + await self.cache.set( + self.key('submenu', menu_id=str(menu_id), submenu_id=str(submenu_id)), + submenu, + self.bg_tasks, + ) return menu async def update_submenu( @@ -62,11 +93,28 @@ class SubmenuService: data = await self.submenu_repo.update_submenu_item( menu_id, submenu_id, submenu_data ) - menu = data.__dict__ - menu = {k: v for k, v in menu.items() if not k.startswith('_')} - menu['dishes_count'] = len(menu.pop('dishes')) - menu = SubMenuRead(**menu) - return menu + submenu = data.__dict__ + submenu = {k: v for k, v in submenu.items() if not k.startswith('_')} + submenu['dishes_count'] = len(submenu.pop('dishes')) + submenu = SubMenuRead(**submenu) + await self.cache.set( + self.key('submenu', menu_id=str(menu_id), submenu_id=str(submenu_id)), + submenu, + self.bg_tasks, + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + + return submenu async def del_menu(self, menu_id: UUID, submenu_id: UUID) -> int: - return await self.submenu_repo.delete_submenu_item(menu_id, submenu_id) + code = await self.submenu_repo.delete_submenu_item(menu_id, submenu_id) + await self.cache.delete( + key=self.key( + 'submenu', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + ), + bg_task=self.bg_tasks, + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + return code