mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-08-04 15:44:09 +00:00
fix: pool exhaustion and lock timeout surface as 500 instead of structured 503 (#295)
This commit is contained in:
+54
-5
@@ -39,7 +39,11 @@ from fastapi_toolsets.db import (
|
||||
m2m_set,
|
||||
wait_for_row_change,
|
||||
)
|
||||
from fastapi_toolsets.exceptions import NotFoundError
|
||||
from fastapi_toolsets.exceptions import (
|
||||
LockTimeoutError,
|
||||
NotFoundError,
|
||||
PoolExhaustedError,
|
||||
)
|
||||
from fastapi_toolsets.pytest import create_db_session
|
||||
|
||||
from .conftest import DATABASE_URL, Base, Post, Role, RoleCrud, Tag, User, UserCrud
|
||||
@@ -399,15 +403,13 @@ class TestAdvisoryLock:
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_timeout_raises_when_contended(self, session_maker):
|
||||
"""timeout= raises when the lock cannot be acquired within the interval."""
|
||||
from sqlalchemy.exc import DBAPIError
|
||||
|
||||
"""timeout= raises LockTimeoutError when the lock cannot be acquired."""
|
||||
async with session_maker() as holder:
|
||||
async with holder.begin():
|
||||
async with advisory_lock(holder, 1006):
|
||||
async with session_maker() as contender:
|
||||
async with contender.begin():
|
||||
with pytest.raises(DBAPIError):
|
||||
with pytest.raises(LockTimeoutError):
|
||||
async with advisory_lock(
|
||||
contender, 1006, timeout="10ms"
|
||||
):
|
||||
@@ -746,6 +748,53 @@ class TestM2MAdd:
|
||||
assert loaded.tags[0].name == "locked_tag"
|
||||
|
||||
|
||||
class TestDbErrors:
|
||||
"""Tests for structured error handling in db utilities."""
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pool_exhausted_on_get_db_raises_pool_exhausted_error(self):
|
||||
"""PoolExhaustedError is raised when the connection pool is exhausted on get_db."""
|
||||
engine = create_async_engine(
|
||||
DATABASE_URL, pool_size=1, max_overflow=0, pool_timeout=0.1
|
||||
)
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
get_db = create_db_dependency(session_factory)
|
||||
|
||||
try:
|
||||
async with session_factory() as holder:
|
||||
await holder.connection() # check out the single available connection
|
||||
with pytest.raises(PoolExhaustedError):
|
||||
async for _ in get_db():
|
||||
pass
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pool_exhausted_on_lock_tables_raises_pool_exhausted_error(self):
|
||||
"""PoolExhaustedError is raised when the connection pool is exhausted on lock_tables."""
|
||||
engine = create_async_engine(
|
||||
DATABASE_URL, pool_size=1, max_overflow=0, pool_timeout=0.1
|
||||
)
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
try:
|
||||
async with session_factory() as holder:
|
||||
await holder.connection() # check out the single available connection
|
||||
with pytest.raises(PoolExhaustedError):
|
||||
async with lock_tables(session_factory, [Role]) as _:
|
||||
pass
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_lock_timeout_raises_lock_timeout_error(self, session_maker):
|
||||
"""LockTimeoutError is raised when a table lock cannot be acquired within timeout."""
|
||||
async with lock_tables(session_maker, [Role]) as _:
|
||||
with pytest.raises(LockTimeoutError):
|
||||
async with lock_tables(session_maker, [Role], timeout="100ms") as _:
|
||||
pass
|
||||
|
||||
|
||||
class _LocalBase(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ from fastapi_toolsets.exceptions import (
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
InvalidOrderFieldError,
|
||||
LockTimeoutError,
|
||||
NotFoundError,
|
||||
PoolExhaustedError,
|
||||
UnauthorizedError,
|
||||
generate_error_responses,
|
||||
init_exceptions_handlers,
|
||||
@@ -216,6 +218,70 @@ class TestApiExceptionGuard:
|
||||
assert err.api_error.code == 404
|
||||
|
||||
|
||||
class TestDbExceptions:
|
||||
"""Tests for database-related exception classes."""
|
||||
|
||||
def test_pool_exhausted_error_attributes(self):
|
||||
"""PoolExhaustedError has 503 status and DB-503-POOL error code."""
|
||||
error = PoolExhaustedError()
|
||||
assert error.api_error.code == 503
|
||||
assert error.api_error.err_code == "DB-503-POOL"
|
||||
assert error.api_error.msg == "Service Unavailable"
|
||||
|
||||
def test_pool_exhausted_error_with_detail(self):
|
||||
"""PoolExhaustedError accepts a detail string that overrides msg."""
|
||||
error = PoolExhaustedError("pool full")
|
||||
assert error.api_error.msg == "pool full"
|
||||
assert PoolExhaustedError.api_error.msg == "Service Unavailable"
|
||||
|
||||
def test_lock_timeout_error_attributes(self):
|
||||
"""LockTimeoutError has 503 status and DB-503-LOCK error code."""
|
||||
error = LockTimeoutError()
|
||||
assert error.api_error.code == 503
|
||||
assert error.api_error.err_code == "DB-503-LOCK"
|
||||
assert error.api_error.msg == "Service Unavailable"
|
||||
|
||||
def test_lock_timeout_error_with_detail(self):
|
||||
"""LockTimeoutError accepts a detail string that overrides msg."""
|
||||
error = LockTimeoutError("contended")
|
||||
assert error.api_error.msg == "contended"
|
||||
assert LockTimeoutError.api_error.msg == "Service Unavailable"
|
||||
|
||||
def test_pool_exhausted_handled_as_503(self):
|
||||
"""init_exceptions_handlers turns PoolExhaustedError into a 503 response."""
|
||||
from fastapi import FastAPI
|
||||
from fastapi_toolsets.exceptions import init_exceptions_handlers
|
||||
|
||||
app = FastAPI()
|
||||
init_exceptions_handlers(app)
|
||||
|
||||
@app.get("/db")
|
||||
async def endpoint():
|
||||
raise PoolExhaustedError()
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/db")
|
||||
assert response.status_code == 503
|
||||
assert response.json()["error_code"] == "DB-503-POOL"
|
||||
|
||||
def test_lock_timeout_handled_as_503(self):
|
||||
"""init_exceptions_handlers turns LockTimeoutError into a 503 response."""
|
||||
from fastapi import FastAPI
|
||||
from fastapi_toolsets.exceptions import init_exceptions_handlers
|
||||
|
||||
app = FastAPI()
|
||||
init_exceptions_handlers(app)
|
||||
|
||||
@app.get("/lock")
|
||||
async def endpoint():
|
||||
raise LockTimeoutError()
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/lock")
|
||||
assert response.status_code == 503
|
||||
assert response.json()["error_code"] == "DB-503-LOCK"
|
||||
|
||||
|
||||
class TestBuiltInExceptions:
|
||||
"""Tests for built-in exception classes."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user