Give each request its own database session - #51
Conversation
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.
✅ Deploy Preview for climate-ref canceled.
|
📝 WalkthroughWalkthroughChangesDatabase dependency lifecycle
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
backend/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
backend/pyproject.tomlbackend/src/ref_backend/api/deps.pybackend/src/ref_backend/main.pybackend/tests/conftest.pybackend/tests/test_api/test_deps.pychangelog/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. | |||
There was a problem hiding this comment.
📐 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.
| 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
Fixes connections left idle in a transaction.
get_database_sessionyieldeddatabase.sessionwith nothing after theyield. 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.
Databaseowns a single long-livedSessionand 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 areasync def, so two requests really can be in flight at once.Database.session_scopefrom 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.Reader, so a request reads through one session throughout rather than mixing the request session with the shared one.Databaseper url and read-only flag._get_database_dependencycalledDatabase.from_configfresh on every request, so each read built an engine and a session and abandoned both.main.pyoverrides 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.session_scopeand theReadersession argument land.The dependency does not call
Database.close(). TheDatabaseis shared, so disposing the engine after every response would tear down the pool the next request needs.session_scopecloses 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.