Skip to content

Give each request its own database session - #51

Merged
lewisjared merged 4 commits into
mainfrom
fix/close-request-database-session
Aug 14, 2026
Merged

Give each request its own database session#51
lewisjared merged 4 commits into
mainfrom
fix/close-request-database-session

Conversation

@lewisjared

@lewisjared lewisjared commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Fixes connections left idle in a transaction.

get_database_session yielded database.session with nothing after the yield. FastAPI runs generator teardown after each response, but there was no teardown, so the transaction SQLAlchemy opens on first use never ended. Each request left a connection idle in a transaction until the server or the database timed it out. The CNPG timeout is a backstop, not the cure.

Closing that session on the way out is not enough on its own. Database owns a single long-lived Session and every request shares it, so a teardown that closes it can end a transaction another in-flight request is still using. All of the routes here are async def, so two requests really can be in flight at once.

  • Uses Database.session_scope from climate-ref 0.17.2, so each request gets its own session. The engine and its connection pool stay shared, so only the session is private to the request.
  • Passes that session to the Reader, so a request reads through one session throughout rather than mixing the request session with the shared one.
  • Caches the Database per url and read-only flag. _get_database_dependency called Database.from_config fresh on every request, so each read built an engine and a session and abandoned both. main.py overrides the dependency with a singleton, but nothing else does, so the uncached path is live in tests and in any app built without that override.
  • Raises the climate-ref floor to 0.17.2, which is where session_scope and the Reader session argument land.

The dependency does not call Database.close(). The Database is shared, so disposing the engine after every response would tear down the pool the next request needs. session_scope closes the request session and returns its connection to the pool, which is all that is needed.

Adds tests covering the reuse of the Database, sessions being per request rather than shared, the session closing on teardown, and a real request leaving no connection checked out of the pool.

The session dependency yielded the shared session without a teardown, so the
transaction SQLAlchemy opens on first use was never ended. Connections sat idle
in a transaction until the server or the database timed them out.

Closes the session on the way out of the dependency, and caches the Database per
url so a request does not build an engine it then abandons.
@netlify

netlify Bot commented Aug 14, 2026

Copy link
Copy Markdown

Deploy Preview for climate-ref canceled.

Name Link
🔨 Latest commit 2246a7f
🔍 Latest deploy log https://app.netlify.com/projects/climate-ref/deploys/6a7e9510987491000819298c

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Database dependency lifecycle

Layer / File(s) Summary
Cache instances and manage request sessions
backend/pyproject.toml, backend/src/ref_backend/api/deps.py, changelog/51.fix.md
The dependency cache reuses Database instances by URL and read-only mode. Each request uses a session_scope() session, passes it to Reader, and closes it after use.
Wire and validate the dependency lifecycle
backend/src/ref_backend/main.py, backend/tests/conftest.py, backend/tests/test_api/test_deps.py
Application and test wiring use the dependency factory. Tests cover instance reuse, session isolation, transaction closure, and connection pool cleanup.

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant DatabaseDependency
  participant Database
  participant Reader
  Request->>DatabaseDependency: Resolve database by URL and read-only mode
  DatabaseDependency->>Database: Open session_scope
  DatabaseDependency->>Reader: Provide request-scoped session
  Reader-->>Request: Return diagnostic result
  Database-->>DatabaseDependency: Close session and transaction
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: each request receives its own database session.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/close-request-database-session

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.

The dependency now caches, so the startup override in main.py was a second
mechanism for the same job. The startup singleton is built through the
dependency instead, and the override is gone.

Under USE_TEST_DATA the database now follows the test config. Previously only
the config was swapped, so the app served the production database.
The session dependency handed out `Database.session`, which is long lived and shared
process-wide. Closing it at the end of a request would end a transaction another
in-flight request was still using.

- Uses `Database.session_scope` from climate-ref 0.17.2, so each request gets its own
  session on the shared engine and pool.
- Passes that session to the `Reader`, so a request reads through one session throughout.
- Raises the climate-ref floor to 0.17.2, which is where `session_scope` lands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ed9e844-5757-4dd3-afba-48952432e519

📥 Commits

Reviewing files that changed from the base of the PR and between a7b67b0 and 2246a7f.

⛔ Files ignored due to path filters (1)
  • backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • backend/pyproject.toml
  • backend/src/ref_backend/api/deps.py
  • backend/src/ref_backend/main.py
  • backend/tests/conftest.py
  • backend/tests/test_api/test_deps.py
  • changelog/51.fix.md

Comment thread changelog/51.fix.md
@@ -0,0 +1,4 @@
Gives each request its own database session, so a request no longer leaves a connection idle in a transaction.
The session dependency handed out `Database.session`, which is long lived and shared process-wide, and never closed it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hyphenate “long-lived”.

Change “long lived” to “long-lived” in this changelog entry.

Proposed fix
-The session dependency handed out `Database.session`, which is long lived and shared process-wide, and never closed it.
+The session dependency handed out `Database.session`, which is long-lived and shared process-wide, and never closed it.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The session dependency handed out `Database.session`, which is long lived and shared process-wide, and never closed it.
The session dependency handed out `Database.session`, which is long-lived and shared process-wide, and never closed it.
🧰 Tools
🪛 LanguageTool

[misspelling] ~2-~2: This word is normally spelled with a hyphen.
Context: ...handed out Database.session, which is long lived and shared process-wide, and never clos...

(EN_COMPOUNDS_LONG_LIVED)

Source: Linters/SAST tools

@lewisjared lewisjared changed the title Close the request database session Give each request its own database session Aug 14, 2026
@lewisjared
lewisjared merged commit 176ebb2 into main Aug 14, 2026
9 checks passed
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.

1 participant