fix: replace manual db.close() calls with middleware-based connection management (#71) #97
No reviewers
Labels
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/major-domo-database#97
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "issue/71-refactor-manual-db-close-calls-to-middleware-based"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #71
Summary
Adds
db_connection_middlewaretoapp/main.pyand removes all 177 manualdb.close()calls from 26 router files.What changed
app/main.py: Addeddb_connection_middleware— opens a connection at the start of each HTTP request (db.connect(reuse_if_open=True)), closes it in afinallyblock after the response is fully sent. This ensures FK lazy-loads during FastAPI response serialization use a live connection.db.close()lines (177 total). The middleware now owns the connection lifecycle for every request.Why
With
PooledPostgresqlDatabase, manualdb.close()beforereturnreturned the connection to the pool before FastAPI serialized the response. Any lazy FK access duringmodel_to_dict(recurse=True)afterdb.close()would race against concurrent requests. The middleware defers close until after the full response is written.The
handle_db_errorsdecorator'sdb.close()call in its exception handler is left intact — it is harmless with the middleware (double-close is a no-op onPooledPostgresqlDatabasewhenis_closed()guards the middleware close).Files changed
app/main.py(+13 lines)app/routers_v3/*.py— 21 filesapp/routers_v3/stratplay/*.py— 5 filesOther observations
handle_db_errorsindependencies.pycatchesExceptiongenerically, which means it also catchesHTTPExceptionraised by endpoints and converts them to 500 responses. This is a pre-existing bug out of scope for this PR.# db.close()onstandings.py:30was left as-is (already inactive).Note on related PRs
PR #87 also adds
db_connection_middlewarewith the same implementation, paired withautoconnect=Falseandtimeout=5changes indb_engine.py. This PR is complementary — if #87 merges first, the middleware addition here will conflict (trivially resolvable) but thedb.close()removals will still apply cleanly.AI Code Review
Files Reviewed
app/main.py(modified — middleware added)app/routers_v3/custom_commands.py(modified)app/routers_v3/help_commands.py(modified)app/routers_v3/awards.py,battingstats.py,current.py,decisions.py,divisions.py,draftdata.py,draftlist.py,draftpicks.py,fieldingstats.py,injuries.py,keepers.py,managers.py,pitchingstats.py,results.py,sbaplayers.py,schedules.py,standings.py,stratgame.py,transactions.py(modified)app/routers_v3/stratplay/batting.py,crud.py,fielding.py,pitching.py,plays.py(modified)Findings
Correctness
CRITICAL — Empty
finally:blocks cause SyntaxError at import 🚨custom_commands.pyandhelp_commands.pyuse atry/except/finallypattern wheredb.close()was the only statement in thefinally:clause. After removal, thosefinally:blocks have an empty body, which Python rejects with:The API will not start — both files fail at import time. Affected locations:
custom_commands.py(12 endpoints):get_custom_commands,create_custom_command_endpoint,update_custom_command_endpoint,patch_custom_command,delete_custom_command_endpoint,get_creators,create_creator_endpoint,get_custom_command_stats,get_custom_command_by_name_endpoint,execute_custom_command,get_command_names_for_autocomplete,get_custom_command.help_commands.py(10 endpoints):get_help_commands,create_help_command_endpoint,update_help_command_endpoint,restore_help_command_endpoint,delete_help_command_endpoint,get_help_command_stats,get_help_command_by_name_endpoint,increment_view_count,get_help_names_for_autocomplete,get_help_command.Fix: Remove the
finally:clause entirely (sincedb.close()is now handled by the middleware), or addpassif the structure must be preserved:All other files only removed
db.close()from plainif/elseor top-level branches — those are syntactically correct.Security
Style & Conventions
app/main.py):from .db_engine import dbis placed inside the middleware function body. Functionally correct (Python caches module imports), but top-level import is the project convention. Non-blocking.Suggestions
finally:clause entirely over addingpass— cleaner, and the middleware already handles connection lifecycle.db.close()removals apply cleanly.Verdict: REQUEST_CHANGES
Two files (
custom_commands.pyandhelp_commands.py) will produceSyntaxError/IndentationErrorat import time due to emptyfinally:blocks, crashing the API on startup. All 22 affectedfinally:blocks need thefinally:clause removed (orpassadded). The middleware implementation inmain.pyand all otherdb.close()removals are correct.Automated review by Claude PR Reviewer
Review Feedback Addressed
Commit
ac2c592fixes the emptyfinally:blocks identified in the review.Root cause: After removing
db.close()fromfinally:clauses, those blocks were left empty, causingIndentationErrorat import time (Python requires a non-empty body afterfinally:).Fix applied: Removed the
finally:clause entirely from all 22 affected endpoints:custom_commands.py— 12 endpoints (get_custom_commands,create_custom_command_endpoint,update_custom_command_endpoint,patch_custom_command,delete_custom_command_endpoint,get_creators,create_creator_endpoint,get_custom_command_stats,get_custom_command_by_name_endpoint,execute_custom_command,get_command_names_for_autocomplete,get_custom_command)help_commands.py— 10 endpoints (get_help_commands,create_help_command_endpoint,update_help_command_endpoint,restore_help_command_endpoint,delete_help_command_endpoint,get_help_command_stats,get_help_command_by_name_endpoint,increment_view_count,get_help_names_for_autocomplete,get_help_command)The
finally:clauses are no longer needed since connection lifecycle is handled by the middleware inmain.py.AI Code Review
Files Reviewed
app/main.py(modified — middleware added)app/routers_v3/custom_commands.py(modified — 12 emptyfinally:blocks removed)app/routers_v3/help_commands.py(modified — 10 emptyfinally:blocks removed)app/routers_v3/awards.py,battingstats.py,current.py,decisions.py,divisions.py,draftdata.py,draftlist.py,draftpicks.py,fieldingstats.py,injuries.py,keepers.py,managers.py,pitchingstats.py,results.py,sbaplayers.py,schedules.py,standings.py,stratgame.py,transactions.py(modified — standalonedb.close()removals)app/routers_v3/stratplay/batting.py,crud.py,fielding.py,pitching.py,plays.py(modified — standalonedb.close()removals)Findings
Correctness
db.connect(reuse_if_open=True)at request start +finally: db.close()withis_closed()guard correctly defers connection lifecycle until after the full response is written. This eliminates the race condition where earlydb.close()returned the connection to the pool before FastAPI serialized FK lazy-loads.custom_commands.py(12 endpoints) andhelp_commands.py(10 endpoints) previously had emptyfinally:clauses afterdb.close()removal → would causeIndentationErrorat import. Those empty blocks are correctly removed in commitac2c592.db.close()removals across 26 router files are complete. Three files inmain.py's includes (players.py,teams.py,views.py) are absent from the diff — consistent with those files having no manualdb.close()calls.handle_db_errorsdecorator retains itsdb.close()call — correctly noted as harmless with theis_closed()guard in middleware.Security
from .db_engine import dbinside the middleware function is intentional (avoids circular imports at startup) and poses no security concern.Style & Conventions
Suggestions
/health,/ready). If connection exhaustion under load becomes a concern, a path-prefix check could skip those, but this is well outside the scope of this PR.from .db_engine import dbinside the middleware body works but is an unusual placement. A module-level import would be more idiomatic if circular imports aren't a concern.Verdict: APPROVED
The prior blocking issue (empty
finally:clauses causingIndentationErroron import) is fixed inac2c592. The middleware implementation is correct, the 177 removals are complete, and the connection lifecycle is now properly managed. Ready to merge, pending trivial conflict resolution with PR #87 if that lands first.Automated review by Claude PR Reviewer
AI Code Review
Files Reviewed
app/main.py(modified — middleware added)app/routers_v3/awards.py(modified)app/routers_v3/battingstats.py(modified)app/routers_v3/current.py(modified)app/routers_v3/custom_commands.py(modified)app/routers_v3/decisions.py(modified)app/routers_v3/divisions.py(modified)app/routers_v3/draftdata.py(modified)app/routers_v3/draftlist.py(modified)app/routers_v3/draftpicks.py(modified)app/routers_v3/fieldingstats.py(modified)app/routers_v3/help_commands.py(modified)app/routers_v3/injuries.py(modified)app/routers_v3/keepers.py(modified)app/routers_v3/managers.py(modified)app/routers_v3/pitchingstats.py(modified)app/routers_v3/results.py(modified)app/routers_v3/sbaplayers.py(modified)app/routers_v3/schedules.py(modified)app/routers_v3/standings.py(modified)app/routers_v3/stratgame.py(modified)app/routers_v3/stratplay/batting.py(modified)app/routers_v3/stratplay/crud.py(modified)app/routers_v3/stratplay/fielding.py(modified)app/routers_v3/stratplay/pitching.py(modified)app/routers_v3/stratplay/plays.py(modified)app/routers_v3/transactions.py(modified)app/dependencies.py(read for context — unchanged)app/db_engine.py(read for context — unchanged)Findings
Correctness
Middleware implementation is correct.
db.connect(reuse_if_open=True)beforecall_nextanddb.close()guarded byis_closed()infinallyis the right pattern forPooledPostgresqlDatabase. The close fires afterawait call_next(request)returns, which in FastAPI means after the full response is streamed — this correctly defers connection release past all FK lazy-loads duringmodel_to_dict(recurse=True).Double-close safety is properly handled. The
handle_db_errorsdecorator independencies.pycallsdb.close()in its ownfinallyblock when an exception occurs. WithPooledPostgresqlDatabaseand theis_closed()guard in the middleware'sfinally, this is a harmless no-op — the decorator closes the connection on error, and the middleware'sif not db.is_closed()skips the redundant close. Correct.db_engine.pystill hasautoconnect=True. This means connections may auto-open outside middleware scope (e.g., during startup or background tasks). The middleware usesreuse_if_open=Truewhich handles this gracefully. No regression here, but worth noting that PR #87 pairs this withautoconnect=Falsefor tighter lifecycle control — if #87 merges after this PR, no additional changes to the router files will be needed.db.close()removals are clean across all 26 files. Verified that:db.close()beforereturnstatements are removed.db.close()beforeraise HTTPException(...)are removed.custom_commands.pyandhelp_commands.pyhadfinally: db.close()blocks (a slightly different pattern) — these were correctly removed as completefinallyblocks, leaving no emptyfinally:stubs. No accidental logic was dropped.standings.pyline 30's commented-out# db.close()is intentionally left as-is.One unguarded
db.close()path remains inhandle_db_errors. The exception handler independencies.pycallsdb.close()unconditionally in its own finally, without anis_closed()guard. The PR body describes this as "harmless" due to the middleware'sis_closed()guard, which is correct forPooledPostgresqlDatabase— the pool handles repeated closes gracefully. No action required.Security
No issues found. No new inputs, no auth changes, no credentials introduced. The middleware touches only the connection lifecycle.
Style & Conventions
The middleware import (
from .db_engine import db) is deferred inside the function body rather than placed at the top ofmain.py. This matches the project's existing pattern independencies.pywherefrom .db_engine import dbis also imported inside the decorator wrapper to avoid circular imports. Correct and consistent.No empty finally blocks were left behind. All removals are surgical — only
db.close()lines removed, surrounding logic preserved exactly.Suggestions
db.close()removals here are compatible either way, but resolving the middleware addition conflict will be simpler if one PR is merged cleanly before the other.handle_db_errorspre-existing bug noted in the PR body (catchingHTTPExceptionand converting to 500) is a real issue worth a follow-up issue if one doesn't already exist.Verdict: APPROVED
The middleware implementation is correct and directly addresses the race condition described in issue #71. All 177
db.close()removals across 26 files are clean — no empty finally blocks, no dropped logic, no missing removals. Theis_closed()guard in the middleware correctly makes the decorator's existingdb.close()a safe no-op. This is a straightforward, well-scoped reliability fix and is ready to merge.Automated review by Claude PR Reviewer
Checkout
From your project repository, check out a new branch and test the changes.