Skip to content

Commit

Permalink
feat!: Original media url cache added
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Redis dependency
  • Loading branch information
Alirezaja1384 committed Sep 27, 2024
1 parent 5c4e3dd commit 62a3f4d
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 11 deletions.
4 changes: 3 additions & 1 deletion api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ APP_NAME=MicroMedia

JWT_DECODE_ALGORITHMS=HS256,RS256
JWT_DECODE_KEY=SOME_KEY
#JWT_DECODE_KEY=file: jwt_pub.pem
# JWT_DECODE_KEY=file: jwt_pub.pem
JWT_AUDIENCE=
JWT_ISSUER=

Expand All @@ -22,3 +22,5 @@ IMGPROXY_HOST=http://localhost:8080
IMGPROXY_KEY=SOME_HEX_KEY
IMGPROXY_SALT=SOME_HEX_SALT
IMGPROXY_RESIZE_ENLARGE=True

REDIS_URL=redis://localhost:6379/0
27 changes: 21 additions & 6 deletions api/micro_media/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.authentication import AuthenticationMiddleware

from redis.asyncio.client import Redis
from auth_utils import APIKeyAuthBackend, AuthBackendsWrapper, JWTAuthBackend
from fastapi_pagination import add_pagination

from micro_media.schemas import JWTUser
from micro_media.routers import router as base_router
from micro_media.auth import validate_api_keys, get_api_key_user
from micro_media.exception_handlers import register_exception_handlers
from micro_media.utils.cache import AsyncRedisCache
from micro_media.settings import (
DEBUG,
APP_NAME,
Expand All @@ -17,13 +20,30 @@
JWT_AUDIENCE,
JWT_ISSUER,
CORS_ALLOWED_ORIGINS,
REDIS_URL,
REDIS_PREFIX,
)


@asynccontextmanager
async def lifespan(app: FastAPI):
validate_api_keys()

redis = Redis.from_url(REDIS_URL, decode_responses=True)
await redis.ping()
AsyncRedisCache.init(redis=redis, prefix=REDIS_PREFIX + "caches:")

yield

await redis.aclose()


app = FastAPI(
title=APP_NAME,
docs_url="/docs",
redoc_url="/redoc",
debug=DEBUG,
lifespan=lifespan,
)

app.include_router(base_router)
Expand Down Expand Up @@ -52,8 +72,3 @@

add_pagination(app)
register_exception_handlers(app=app)


@app.on_event("startup")
async def startup():
validate_api_keys()
9 changes: 8 additions & 1 deletion api/micro_media/routers/v1/public/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from micro_media.utils import truthy_or_404
from micro_media.utils.sqlalchemy import get_one
from micro_media.utils.cache import AsyncRedisCache
from micro_media.models import get_session, Media, MediaType
from micro_media.storage import STORAGE_CONTEXT as SC
from micro_media.media import MEDIA_CONTEXT as MC, IMGProxyThumbnailManager
Expand All @@ -22,7 +23,13 @@
)


async def _get_original_link(media: Media, expires_in: int):
@AsyncRedisCache.aredis_cache(
key_generator=lambda media, expires_in: f"original_link:{media.id}",
cache_deserializer=str,
cache_serializer=str,
ttl=60,
)
async def _get_original_link(media: Media, expires_in: int) -> str:
storage_manager = SC.get_manager(storage_id=media.storage_id)

file_link = await storage_manager.generate_file_link(
Expand Down
3 changes: 3 additions & 0 deletions api/micro_media/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@
IMGPROXY_RESIZE_ENLARGE = cast(
bool, config("IMGPROXY_RESIZE_ENLARGE", default=True)
)

REDIS_URL = cast(str, config("REDIS_URL"))
REDIS_PREFIX = cast(str, config("REDIS_PREFIX", default="micro_media:"))
55 changes: 55 additions & 0 deletions api/micro_media/utils/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import asyncio
from functools import wraps
from typing import Awaitable, Callable, TypeVar
from typing_extensions import ParamSpec

from redis.asyncio.client import Redis


P = ParamSpec("P")
T = TypeVar("T")


class AsyncRedisCache:
prefix: str
redis: Redis

@classmethod
def init(cls, redis: Redis, prefix: str) -> None:
cls.redis = redis
cls.prefix = prefix

@classmethod
def aredis_cache(
cls,
key_generator: Callable[P, str],
cache_serializer: Callable[[T], str],
cache_deserializer: Callable[[str], T],
ttl: int,
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
def decorator(
func: Callable[P, Awaitable[T]]
) -> Callable[P, Awaitable[T]]:
lock = asyncio.Lock()

@wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
cache_key = cls.prefix + key_generator(*args, **kwargs)

async with lock:
cache_value = await cls.redis.get(cache_key)
if cache_value is not None:
if isinstance(cache_value, bytes):
cache_value = cache_value.decode()

return cache_deserializer(cache_value)

res = await func(*args, **kwargs)
await cls.redis.setex(
cache_key, ttl, cache_serializer(res)
)
return res

return wrapper

return decorator
42 changes: 41 additions & 1 deletion api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/prod.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ IMGPROXY_HOST=https://imgproxy.blah.blah
IMGPROXY_KEY=SOME_HEX_KEY
IMGPROXY_SALT=SOME_HEX_SALT
IMGPROXY_RESIZE_ENLARGE=True

REDIS_URL=redis://redis:6379/0
2 changes: 2 additions & 0 deletions api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ python-multipart = "^0.0.7"
pyjwt = { extras = ["crypto"], version = "^2.8.0" }
pillow = "^10.2.0"
fastapi-auth-utils = "^1.1.2"
cachetools = "^5.5.0"
redis = "^5.0.8"


[tool.poetry.group.dev.dependencies]
Expand Down
8 changes: 6 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.8'

services:
api:
restart: always
Expand Down Expand Up @@ -41,6 +39,12 @@ services:
env_file:
- ./imgproxy/prod.env

redis:
image: redis:7.2-alpine
restart: always
networks:
- default

networks:
default:
driver: bridge

0 comments on commit 62a3f4d

Please sign in to comment.