Add redis cache

This commit is contained in:
2022-02-09 01:03:54 +03:00
parent baae2c850b
commit 19fe00335e
7 changed files with 265 additions and 69 deletions

View File

@@ -3,7 +3,7 @@ from typing import cast
from app.models import User, Language
async def update_user_allowed_langs(user: User, new_allowed_langs: list[str]):
async def update_user_allowed_langs(user: User, new_allowed_langs: list[str]) -> bool:
user_allowed_langs = cast(list[Language], user.allowed_langs)
exists_langs = set(lang.code for lang in user_allowed_langs)
@@ -16,9 +16,15 @@ async def update_user_allowed_langs(user: User, new_allowed_langs: list[str]):
langs = await Language.objects.filter(code__in=all_process_langs).all()
updated = False
for lang in langs:
if lang.code in to_delete:
await user.allowed_langs.remove(lang)
updated = True
if lang.code in to_add:
await user.allowed_langs.add(lang)
updated = True
return updated

View File

@@ -0,0 +1,157 @@
from typing import Optional, Union
from fastapi import HTTPException, status
import aioredis
import orjson
from app.models import User
from app.serializers import UserCreateOrUpdate, UserDetail, UserUpdate
from app.services.allowed_langs_updater import update_user_allowed_langs
class UsersDataManager:
@classmethod
async def _get_user_from_db(cls, user_id: int) -> Optional[User]:
return await User.objects.select_related("allowed_langs").get_or_none(
user_id=user_id
)
@classmethod
def _get_cache_key(cls, user_id: int) -> str:
return f"user_{user_id}"
@classmethod
async def _get_user_from_cache(
cls, user_id: int, redis: aioredis.Redis
) -> Optional[UserDetail]:
try:
key = cls._get_cache_key(user_id)
data = await redis.get(key)
if data is None:
return None
return UserDetail.parse_obj(orjson.loads(data))
except aioredis.RedisError:
return None
@classmethod
async def _cache_user(cls, user: User, redis: aioredis.Redis) -> bool:
try:
key = cls._get_cache_key(user.id)
data = orjson.dumps(user.dict())
await redis.set(key, data)
return True
except aioredis.RedisError:
return False
@classmethod
async def get_user(
cls, user_id: int, redis: aioredis.Redis
) -> Optional[UserDetail]:
if cached_user := await cls._get_user_from_cache(user_id, redis):
return cached_user
user = await cls._get_user_from_db(user_id)
if not user:
return None
await cls._cache_user(user, redis)
return user # type: ignore
@classmethod
def _is_has_data_to_update(cls, new_user: UserUpdate) -> bool:
data_dict = new_user.dict()
update_data = {}
for key in data_dict:
if data_dict[key] is not None:
update_data[key] = data_dict[key]
return bool(update_data)
@classmethod
async def _create(cls, data: UserCreateOrUpdate):
data_dict = data.dict()
allowed_langs = data_dict.pop("allowed_langs", None) or ["ru", "be", "uk"]
user_obj = await User.objects.select_related("allowed_langs").create(
**data_dict
)
await update_user_allowed_langs(user_obj, allowed_langs)
return user_obj
@classmethod
async def _update(
cls, user_id: int, update_data: dict, redis: aioredis.Redis
) -> User:
user_obj = await cls._get_user_from_db(user_id)
assert user_obj is not None
if allowed_langs := update_data.pop("allowed_langs", None):
await update_user_allowed_langs(user_obj, allowed_langs)
if update_data:
user_obj.update_from_dict(update_data)
await user_obj.update()
await cls._cache_user(user_obj, redis)
return user_obj
@classmethod
async def create_or_update_user(
cls, data: UserCreateOrUpdate, redis: aioredis.Redis
):
user = await cls.get_user(data.user_id, redis)
if user is None:
new_user = await cls._create(data)
await cls._cache_user(new_user, redis)
return new_user
if not cls._is_need_update(user, data):
return user
return await cls._update(user.user_id, data.dict(), redis)
@classmethod
def _is_need_update(
cls, old_user: UserDetail, new_user: Union[UserUpdate, UserCreateOrUpdate]
) -> bool:
old_data = old_user.dict()
new_data = new_user.dict()
allowed_langs = new_data.pop("allowed_lang", None)
for key in new_data:
if new_data[key] != old_data[key]:
return True
if allowed_langs and set(allowed_langs) != set(
[lang.code for lang in old_user.allowed_langs]
):
return True
return False
@classmethod
async def update_user(
cls, user_id: int, user_data: UserUpdate, redis: aioredis.Redis
) -> Union[UserDetail, User]:
user = await cls.get_user(user_id, redis)
if user is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
if not cls._is_has_data_to_update(user_data):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
if not cls._is_need_update(user, user_data):
return user
return await cls._update(user.user_id, user_data.dict(), redis)

View File

@@ -1,5 +1,6 @@
from fastapi import APIRouter, HTTPException, status, Depends
from fastapi import APIRouter, HTTPException, status, Depends, Request
import aioredis
from fastapi_pagination import Page, Params
from fastapi_pagination.ext.ormar import paginate
@@ -12,10 +13,7 @@ from app.serializers import (
CreateLanguage,
LanguageDetail,
)
from app.services import update_user_allowed_langs
# TODO: add redis cache
from app.services.users_data_manager import UsersDataManager
users_router = APIRouter(
@@ -29,10 +27,9 @@ async def get_users():
@users_router.get("/{user_id}", response_model=UserDetail)
async def get_user(user_id: int):
user_data = await User.objects.select_related("allowed_langs").get_or_none(
user_id=user_id
)
async def get_user(request: Request, user_id: int):
redis: aioredis.Redis = request.app.state.redis
user_data = await UsersDataManager.get_user(user_id, redis)
if user_data is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -41,62 +38,15 @@ async def get_user(user_id: int):
@users_router.post("/", response_model=UserDetail)
async def create_or_update_user(data: UserCreateOrUpdate):
data_dict = data.dict()
user_data = await User.objects.select_related("allowed_langs").get_or_none(
user_id=data_dict["user_id"]
)
allowed_langs = data_dict.pop("allowed_langs")
if user_data is None:
user_data = await User.objects.select_related("allowed_langs").create(
**data_dict
)
if allowed_langs is None:
allowed_langs = ["ru", "be", "uk"]
else:
data_dict.pop("user_id")
user_data.update_from_dict(data_dict)
if allowed_langs:
await update_user_allowed_langs(user_data, allowed_langs)
return user_data
async def create_or_update_user(request: Request, data: UserCreateOrUpdate):
redis: aioredis.Redis = request.app.state.redis
return await UsersDataManager.create_or_update_user(data, redis)
@users_router.patch("/{user_id}", response_model=UserDetail)
async def update_user(user_id: int, data: UserUpdate):
user_data = await User.objects.select_related("allowed_langs").get_or_none(
user_id=user_id
)
if user_data is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
data_dict = data.dict()
update_data = {}
for key in data_dict:
if data_dict[key] is not None:
update_data[key] = data_dict[key]
if not update_data:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
allowed_langs = update_data.pop("allowed_langs", None)
if update_data:
user_data.update_from_dict(update_data)
await user_data.update()
if not allowed_langs:
return user_data
await update_user_allowed_langs(user_data, allowed_langs)
return user_data
async def update_user(request: Request, user_id: int, data: UserUpdate):
redis: aioredis.Redis = request.app.state.redis
return await UsersDataManager.update_user(user_id, data, redis)
languages_router = APIRouter(

View File

@@ -1,8 +1,10 @@
from fastapi import FastAPI
import aioredis
from fastapi_pagination import add_pagination
from app.views import users_router, languages_router, healthcheck_router
from core.config import env_config
from core.db import database
@@ -15,6 +17,13 @@ def start_app() -> FastAPI:
app.state.database = database
app.state.redis = aioredis.Redis(
host=env_config.REDIS_HOST,
port=env_config.REDIS_PORT,
db=env_config.REDIS_DB,
password=env_config.REDIS_PASSWORD,
)
add_pagination(app)
@app.on_event("startup")

View File

@@ -1,3 +1,5 @@
from typing import Optional
from pydantic import BaseSettings
@@ -10,5 +12,10 @@ class EnvConfig(BaseSettings):
POSTGRES_PORT: int
POSTGRES_DB: str
REDIS_HOST: str
REDIS_PORT: int
REDIS_DB: int
REDIS_PASSWORD: Optional[str]
env_config = EnvConfig()