diff --git a/src/cartsnitch_api/cache.py b/src/cartsnitch_api/cache.py index a7fdc81..069e71a 100644 --- a/src/cartsnitch_api/cache.py +++ b/src/cartsnitch_api/cache.py @@ -1,26 +1,51 @@ """Redis/DragonflyDB caching helpers.""" +import redis.asyncio as redis + from cartsnitch_api.config import settings class CacheClient: - """Stub for Redis/DragonflyDB caching. + """Redis/DragonflyDB caching with connection pooling. Will be used for expensive queries: price trends, product comparisons. Cache invalidation via Redis pub/sub events from other services. """ def __init__(self) -> None: - self.url = settings.redis_url + self._pool: redis.ConnectionPool | None = None + self._client: redis.Redis | None = None + + async def initialize(self) -> None: + """Initialize the Redis connection pool.""" + self._pool = redis.ConnectionPool.from_url( + settings.redis_url, + max_connections=20, + decode_responses=True, + ) + self._client = redis.Redis(connection_pool=self._pool) + + async def close(self) -> None: + """Close the Redis connection pool.""" + if self._client: + await self._client.aclose() + if self._pool: + await self._pool.aclose() async def get(self, key: str) -> str | None: - # TODO: implement with redis-py async - return None + if not self._client: + return None + return await self._client.get(key) async def set(self, key: str, value: str, ttl_seconds: int = 300) -> None: - # TODO: implement with redis-py async - pass + if not self._client: + return + await self._client.set(key, value, ex=ttl_seconds) async def delete(self, key: str) -> None: - # TODO: implement with redis-py async - pass + if not self._client: + return + await self._client.delete(key) + + +cache_client = CacheClient() diff --git a/src/cartsnitch_api/database.py b/src/cartsnitch_api/database.py index 324c5bf..3c6043c 100644 --- a/src/cartsnitch_api/database.py +++ b/src/cartsnitch_api/database.py @@ -6,7 +6,14 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_asyn from cartsnitch_api.config import settings -engine = create_async_engine(settings.database_url, echo=False) +engine = create_async_engine( + settings.database_url, + echo=False, + pool_size=10, + max_overflow=20, + pool_pre_ping=True, + pool_recycle=3600, +) async_session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) @@ -14,3 +21,8 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]: """FastAPI dependency that yields an async DB session.""" async with async_session_factory() as session: yield session + + +async def dispose_engine() -> None: + """Dispose the database engine, closing all pooled connections.""" + await engine.dispose() diff --git a/src/cartsnitch_api/main.py b/src/cartsnitch_api/main.py index 6db5a0c..555ffd9 100644 --- a/src/cartsnitch_api/main.py +++ b/src/cartsnitch_api/main.py @@ -5,6 +5,8 @@ from contextlib import asynccontextmanager from fastapi import APIRouter, FastAPI from cartsnitch_api.auth.routes import router as auth_router +from cartsnitch_api.cache import cache_client +from cartsnitch_api.database import dispose_engine from cartsnitch_api.middleware.cors import add_cors_middleware from cartsnitch_api.middleware.error_handler import add_error_handlers, add_error_monitor_middleware from cartsnitch_api.middleware.rate_limit import add_rate_limit_middleware @@ -23,9 +25,10 @@ from cartsnitch_api.routes.user import router as user_router @asynccontextmanager async def lifespan(app: FastAPI): - # TODO: initialize DB session pool, Redis connection, service clients + await cache_client.initialize() yield - # TODO: cleanup connections + await cache_client.close() + await dispose_engine() def create_app() -> FastAPI: