Skip to content

Fix: Thread-safe DB connection manager with pooling (#212)#252

Open
muhammadtihame wants to merge 2 commits into
AOSSIE-Org:mainfrom
muhammadtihame:fix-thread-safe-db
Open

Fix: Thread-safe DB connection manager with pooling (#212)#252
muhammadtihame wants to merge 2 commits into
AOSSIE-Org:mainfrom
muhammadtihame:fix-thread-safe-db

Conversation

@muhammadtihame

@muhammadtihame muhammadtihame commented Jan 26, 2026

Copy link
Copy Markdown

Summary

This PR implements thread-safe database connection management with connection pooling and proper resource cleanup.

Changes

  • Added backend/app/database/core.py (async SQLAlchemy engine + session manager)
  • Added tests/test_db_pool.py to verify concurrency and rollback behavior
  • Added docs/DATABASE_CONNECTION.md documentation
  • Updated backend/app/core/config/settings.py for database configuration
  • Updated pyproject.toml and backend/requirements.txt to include new dependencies

Verification

Run the following tests:

pytest tests/test_db_pool.py


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Added optional environment-configurable database URL and asynchronous database connectivity with pooled connections for improved performance and reliability.

* **Documentation**
  * Added a guide covering database connection setup, pooling behavior, and best practices.

* **Tests**
  * Added tests for connection pool sizing, concurrent session handling, and rollback/cleanup on errors.

* **Chores**
  * Added async DB runtime and test dependencies.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitai

coderabbitai Bot commented Jan 26, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds async database support via SQLAlchemy/asyncpg: new optional database_url setting, an async engine/session module with pooling and a get_db() generator, updated dependencies, documentation, and tests for pool behavior and session lifecycle.

Changes

Cohort / File(s) Summary
Configuration
backend/app/core/config/settings.py
Added optional database_url: Optional[str] = None to Settings.
Async DB core
backend/app/database/core.py
New async engine (create_async_engine) and async_session_maker created from DATABASE_URL; engine may be None with a warning; added async def get_db() -> AsyncGenerator[AsyncSession, None] that raises if engine uninitialized, yields sessions, and handles rollback/closure on errors.
Project deps
pyproject.toml, backend/requirements.txt
Added sqlalchemy and asyncpg to project dependencies; added asyncpg==0.29.0 and pytest-asyncio==0.23.5 to backend requirements.
Documentation
docs/DATABASE_CONNECTION.md
New doc describing AsyncIO SQLAlchemy setup, pool parameters, DATABASE_URL usage, get_db DI pattern, and testing considerations.
Tests
tests/test_db_pool.py
New async tests mocking engine/session: asserts pool size/timeout, simulates 50 concurrent session acquisitions, and verifies rollback+close on error.

Sequence Diagram(s)

sequenceDiagram
    participant Client as FastAPI Route
    participant GetDb as get_db()
    participant Maker as async_session_maker
    participant Pool as Connection Pool
    participant Session as AsyncSession

    Client->>GetDb: request session
    alt Engine initialized
        GetDb->>Maker: create session
        Maker->>Pool: acquire connection
        Pool-->>Maker: provide connection
        Maker-->>Session: return AsyncSession
        GetDb-->>Client: yield session
        Client->>Session: execute queries
        alt Success
            Client->>GetDb: return normally
            GetDb->>Session: close session
            Session->>Pool: release connection
        else Exception
            Client->>GetDb: exception raised
            GetDb->>Session: rollback
            GetDb->>Session: close session
            Session->>Pool: release connection
        end
    else Engine not initialized
        GetDb-->>Client: raise RuntimeError
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I nibble bytes and hop through queues,

Connections pooled, a cozy fuse.
Sessions yield and roll back light,
Async fields gleam in rabbit sight.
Hooray — the DB sleeps snug at night!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix: Thread-safe DB connection manager with pooling (#212)' directly and accurately describes the main change: implementing a thread-safe database connection manager with connection pooling, which is the primary objective of the PR across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@backend/app/database/core.py`:
- Around line 46-57: Remove the redundant explicit close in the async session
context manager: when using the async with async_session_maker() as session
block you should not call await session.close() in the finally; delete that call
so the context manager handles cleanup. Also replace logger.error(...) with
logger.exception(...) in the except block to include the traceback, while
preserving await session.rollback() and re-raising the exception; reference
async_session_maker, session, logger.error -> logger.exception, session.rollback
to locate the changes.

In `@tests/test_db_pool.py`:
- Around line 7-9: The test currently patches app.core.config.settings at module
scope but imports engine and get_db after the patch context, which is fragile
because engine is created at import time and module caching prevents
reinitialization; update tests/test_db_pool.py to apply the patch while
importing/reloading the database module so engine is created with the mocked
configuration—use a pytest fixture that applies patch.dict or patch to set
DATABASE_URL (or patch app.core.config.settings), then
importlib.reload(app.database.core) inside that fixture and yield db_core.engine
and db_core.get_db (or move the import of engine/get_db inside the with
patch(...) block) so engine initialization sees the mocked value.
- Around line 44-45: The async context manager mock is incomplete: instead of
assigning __aexit__.return_value = None make both __aenter__ and __aexit__
awaitable (e.g., replace the current assignments on mock_session with AsyncMock
usage: set mock_session.__aenter__ = AsyncMock(return_value=mock_session) and
mock_session.__aexit__ = AsyncMock(return_value=None)), and apply the same
change to the other async context-manager mock later in the file so both
enter/exit are proper awaitables.
🧹 Nitpick comments (1)
tests/test_db_pool.py (1)

49-52: Unused loop variable session — rename to _session.

Per the static analysis hint, the loop variable is unused within the body. Renaming to _session signals intentional non-use.

-            async for session in get_db():
+            async for _session in get_db():

The same applies to line 80.

Comment thread backend/app/database/core.py Outdated
Comment thread tests/test_db_pool.py Outdated
Comment thread tests/test_db_pool.py Outdated
@muhammadtihame

Copy link
Copy Markdown
Author

Fixes #212

@muhammadtihame

Copy link
Copy Markdown
Author

Addressed CodeRabbit feedback:

  • Removed redundant session.close()
  • Switched to logger.exception for better tracing
  • Refactored tests to use importlib.reload with fixture
  • Fixed async mock aexit using AsyncMock

@Shevilll Shevilll left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peer Review: Fix: Thread-safe DB connection manager with pooling

Hello @muhammadtihame! Thank you for this massive and extremely important improvement to the database layer. Transitioning the backend to use asynchronous SQLAlchemy connection pooling via asyncpg is a huge win for concurrent performance and scalability, especially when handling high API traffic compared to the old direct postgrest calls.

I've reviewed the code in detail and have a few constructive suggestions to share:

1. 🧹 PEP 8 Style and Readability

In backend/app/database/core.py, both engine and async_session_maker are initialized using trailing ternary if/else statements wrapped around long, multiline function calls:

engine = create_async_engine(
    DATABASE_URL,
    echo=False,
    pool_size=20,
    ...
) if DATABASE_URL else None

While syntactically valid in Python, putting a multiline block inside a ternary condition makes the flow harder to parse and violates PEP 8's readability guidelines. It's much cleaner to initialize these using standard if blocks:

engine = None
async_session_maker = None

if DATABASE_URL:
    engine = create_async_engine(
        DATABASE_URL,
        echo=False,
        pool_size=20,        # Maintain 20 open connections
        max_overflow=10,     # Allow 10 extra during spikes
        pool_timeout=30,     # Wait 30s for a connection before raising timeout
        pool_pre_ping=True,  # Check connection health before handing it out
    )
    
    async_session_maker = async_sessionmaker(
        engine,
        class_=AsyncSession,
        expire_on_commit=False,
        autocommit=False,
        autoflush=False,
    )

This makes the conditional initialization of both the engine and session factory explicit, clean, and extremely easy to read.

2. 🛡️ Robust Async Session Cleanup

In the get_db generator:

    async with async_session_maker() as session:
        try:
            yield session
        except Exception:
            logger.exception("Database session error")
            await session.rollback()
            raise

This is a robust and safe pattern!
Just as a small technical detail to note: if a request is cancelled or the connection is severed, FastAPI throws a GeneratorExit (which inherits from BaseException rather than Exception) into the generator at the yield statement.
Because except Exception only catches standard exceptions, GeneratorExit will propagate past this block. However, the outer async with block will still catch it and execute its __aexit__ method, which automatically closes the SQLAlchemy session (performing a safe rollback of any active transactions). Thus, your implementation is completely safe even during task cancellations!

3. 🧪 Commendation on Tests

The test suite you added in tests/test_db_pool.py is absolutely phenomenal! Testing database connection pools and concurrency with multiple async tasks is historically very difficult to mock correctly, but your mock set up (simulating 50 parallel requests and verifying rollback and cleanup counts) is exceptionally clean, robust, and elegant.

Outstanding work on this! Once the stylistic ternary block is cleaned up, this is ready to go.

@Shevilll Shevilll left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peer Review: Thread-safe DB connection manager with pooling

Thank you for putting together this implementation of the database connection pooling using SQLAlchemy and asyncpg. It is a solid foundation, but there are a few architectural, thread-safety, and operational issues that need to be addressed before this is ready for production.


1. Severe Log Pollution via Global Exception Catching in get_db()

In backend/app/database/core.py, the dependency catches all exceptions that propagate through the yield:

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    ...
    async with async_session_maker() as session:
        try:
            yield session
        except Exception:
            logger.exception("Database session error")
            await session.rollback()
            raise

Issue: Any exception raised inside the FastAPI endpoint or downstream handlers (including standard validation errors like RequestValidationError / HTTP 422, or standard not-found exceptions like HTTPException / HTTP 404) bubbles back through the generator. By catching all Exception types and logging them with logger.exception("Database session error"), standard client-side errors will produce scary server-side traceback logs. This will pollute production logging systems and trigger false-alarm alerts.
Fix: Limit the logging to actual database-level exceptions (e.g., sqlalchemy.exc.SQLAlchemyError), or perform the rollback silently (or at DEBUG/INFO level) without logging general application exceptions as database failures:

        try:
            yield session
        except sqlalchemy.exc.SQLAlchemyError as e:
            logger.exception("Database session database-level error")
            await session.rollback()
            raise
        except Exception:
            # Rollback for safety, but do not log as a DB system error
            await session.rollback()
            raise

2. Standard PostgreSQL URIs Will Crash Async Engine Initialization

create_async_engine is invoked with DATABASE_URL directly:

engine = create_async_engine(
    DATABASE_URL,
    ...
) if DATABASE_URL else None

Issue: Standard connection strings (e.g., from Supabase or PostgreSQL environments) start with postgresql:// or postgres://. SQLAlchemy's create_async_engine requires an async dialect (like postgresql+asyncpg://). If a standard URL is passed, it will fail to initialize with InvalidRequestError: The asyncpg dialect is not installed or similar.
Fix: Implement automatic scheme conversion in the settings layer or right before creating the engine:

if DATABASE_URL:
    if DATABASE_URL.startswith("postgresql://"):
        DATABASE_URL = DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://", 1)
    elif DATABASE_URL.startswith("postgres://"):
        DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql+asyncpg://", 1)

3. Module-Level Engine Creation Anti-Pattern

Issue: Creating the engine and sessionmaker at the module-import level (backend/app/database/core.py) means any configuration or parse error will crash the application during import. This makes running CLI scripts, setup/migration scripts, or testing suites that do not use the database highly brittle.
Fix: Wrap the engine and sessionmaker in a lazy-initialization block, or initialize them as part of a FastAPI lifespan event handler.


4. Brittle, Stateful Unit Testing via importlib.reload

In tests/test_db_pool.py:

@pytest.fixture
def mock_db_module():
    with patch("app.core.config.settings") as mock_settings:
        mock_settings.database_url = "postgresql+asyncpg://user:password@localhost:5432/testdb"
        import app.database.core
        importlib.reload(app.database.core)
        yield app.database.core

Issue: Using importlib.reload during tests is a major anti-pattern. It can lead to state leakages, where other test modules that imported app.database.core retain references to the old engine or get confused by partial reloads.
Fix: Cleanly decouple settings injection from the connection manager, allowing settings to be passed or resolved dynamically rather than relying on module reloads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants