Merge pull request #153 from grillazz/152-implement-acyncpg-coon-pool-poc

152 implement acyncpg coon pool poc
This commit is contained in:
Jakub Miazek 2024-05-11 16:34:39 +02:00 committed by GitHub
commit 5ad75cb55a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 103 additions and 21 deletions

View File

@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.exceptions import ResponseValidationError
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -21,7 +22,6 @@ async def create_multi_stuff(
db_session.add_all(stuff_instances) db_session.add_all(stuff_instances)
await db_session.commit() await db_session.commit()
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
# logger.exception(ex)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex) status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex ) from ex
@ -42,11 +42,56 @@ async def create_stuff(
@router.get("/{name}", response_model=StuffResponse) @router.get("/{name}", response_model=StuffResponse)
async def find_stuff( async def find_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
result = await Stuff.find(db_session, name)
if not result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Stuff with name {name} not found.",
)
return result
@router.get("/pool/{name}", response_model=StuffResponse)
async def find_stuff_pool(
request: Request,
name: str, name: str,
db_session: AsyncSession = Depends(get_db), db_session: AsyncSession = Depends(get_db),
): ):
return await Stuff.find(db_session, name) """
Asynchronous function to find a specific 'Stuff' object in the database using a connection pool.
This function compiles an SQL statement to find a 'Stuff' object by its name, executes the statement
using a connection from the application's connection pool, and returns the result as a dictionary.
If the 'Stuff' object is not found, it raises an HTTPException with a 404 status code.
If an SQLAlchemyError occurs during the execution of the SQL statement, it raises an HTTPException
with a 422 status code.
Args:
request (Request): The incoming request. Used to access the application's connection pool.
name (str): The name of the 'Stuff' object to find.
db_session (AsyncSession): The database session. Used to compile the SQL statement.
Returns:
dict: The found 'Stuff' object as a dictionary.
Raises:
HTTPException: If the 'Stuff' object is not found or an SQLAlchemyError occurs.
"""
try:
stmt = await Stuff.find(db_session, name, compile_sql=True)
result = await request.app.postgres_pool.fetchrow(str(stmt))
result = dict(result)
except SQLAlchemyError as ex:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
if not result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Stuff with name {name} not found.",
)
return result
@router.delete("/{name}") @router.delete("/{name}")

View File

@ -70,5 +70,29 @@ class Settings(BaseSettings):
path=self.POSTGRES_DB, path=self.POSTGRES_DB,
) )
@computed_field
@property
def postgres_url(self) -> PostgresDsn:
"""
This is a computed field that generates a PostgresDsn URL
The URL is built using the MultiHostUrl.build method, which takes the following parameters:
- scheme: The scheme of the URL. In this case, it is "postgres".
- username: The username for the Postgres database, retrieved from the POSTGRES_USER environment variable.
- password: The password for the Postgres database, retrieved from the POSTGRES_PASSWORD environment variable.
- host: The host of the Postgres database, retrieved from the POSTGRES_HOST environment variable.
- path: The path of the Postgres database, retrieved from the POSTGRES_DB environment variable.
Returns:
PostgresDsn: The constructed PostgresDsn URL.
"""
return MultiHostUrl.build(
scheme="postgres",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD,
host=self.POSTGRES_HOST,
path=self.POSTGRES_DB,
)
settings = Settings() settings = Settings()

View File

@ -1,3 +1,4 @@
import asyncpg
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends from fastapi import FastAPI, Depends
@ -7,6 +8,7 @@ from fastapi_cache.backends.redis import RedisBackend
from app.api.nonsense import router as nonsense_router from app.api.nonsense import router as nonsense_router
from app.api.shakespeare import router as shakespeare_router from app.api.shakespeare import router as shakespeare_router
from app.api.stuff import router as stuff_router from app.api.stuff import router as stuff_router
from app.config import settings as global_settings
from app.utils.logging import AppLogger from app.utils.logging import AppLogger
from app.api.user import router as user_router from app.api.user import router as user_router
from app.api.health import router as health_router from app.api.health import router as health_router
@ -21,15 +23,26 @@ async def lifespan(_app: FastAPI):
# Load the redis connection # Load the redis connection
_app.redis = await get_redis() _app.redis = await get_redis()
_postgres_dsn = global_settings.postgres_url.unicode_string()
try: try:
# Initialize the cache with the redis connection # Initialize the cache with the redis connection
redis_cache = await get_cache() redis_cache = await get_cache()
FastAPICache.init(RedisBackend(redis_cache), prefix="fastapi-cache") FastAPICache.init(RedisBackend(redis_cache), prefix="fastapi-cache")
logger.info(FastAPICache.get_cache_status_header()) logger.info(FastAPICache.get_cache_status_header())
# Initialize the postgres connection pool
_app.postgres_pool = await asyncpg.create_pool(
dsn=_postgres_dsn,
min_size=5,
max_size=20,
)
logger.info(f"Postgres pool created: {_app.postgres_pool.get_idle_size()=}")
yield yield
finally: finally:
# close redis connection and release the resources # close redis connection and release the resources
await _app.redis.close() await _app.redis.close()
# close postgres connection pool and release the resources
await _app.postgres_pool.close()
app = FastAPI(title="Stuff And Nonsense API", version="0.6", lifespan=lifespan) app = FastAPI(title="Stuff And Nonsense API", version="0.6", lifespan=lifespan)

View File

@ -9,6 +9,20 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship, joinedload
from app.models.base import Base from app.models.base import Base
from app.models.nonsense import Nonsense from app.models.nonsense import Nonsense
from functools import wraps
def compile_sql_or_scalar(func):
@wraps(func)
async def wrapper(cls, db_session, name, compile_sql=False, *args, **kwargs):
stmt = await func(cls, db_session, name, *args, **kwargs)
if compile_sql:
return stmt.compile(compile_kwargs={"literal_binds": True})
result = await db_session.execute(stmt)
return result.scalars().first()
return wrapper
class Stuff(Base): class Stuff(Base):
__tablename__ = "stuff" __tablename__ = "stuff"
@ -24,23 +38,10 @@ class Stuff(Base):
) )
@classmethod @classmethod
async def find(cls, db_session: AsyncSession, name: str): @compile_sql_or_scalar
""" async def find(cls, db_session: AsyncSession, name: str, compile_sql=False):
:param db_session:
:param name:
:return:
"""
stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name) stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name)
result = await db_session.execute(stmt) return stmt
instance = result.scalars().first()
if instance is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"Not found": f"There is no record for name: {name}"},
)
else:
return instance
class StuffFullOfNonsense(Base): class StuffFullOfNonsense(Base):

View File

@ -1,4 +1,3 @@
import pytest import pytest
from fastapi import status from fastapi import status
from httpx import AsyncClient from httpx import AsyncClient