Skip to content

Commit 040f634

Browse files
chriscarrollsmithgithub-actionsbhtaborrafizamankhanclaude
authored
Resolve merge conflicts for Propagate main to stripe (#235)
* chore: release v1.1.8 [skip ci] Automatically generated by python-semantic-release * fix: engine too many connections (#231) * fix: engine too many connections Fix create_engine() connection leaks per request/call * refactor: use lru cached engine Use lru cached engine * add pool_pre_ping and dispose() * fix: address review feedback on engine cache - Route rate_limit.py through the shared get_engine() cache instead of its own module-level singleton, so no second unmanaged pool survives clear_engine_cache() or goes stale when the connection URL changes. - Guard the engine cache with a threading.Lock: sync dependencies run in FastAPI's threadpool, so concurrent first requests could race in get_engine() and orphan an undisposed engine, and clear_engine_cache() could hit a dict-mutated-during-iteration error. - Remove the clear_engine_cache() side effect from tear_down_db(); test fixtures that relied on it now call it explicitly in their teardown. - Make engine-cache tests isolation-safe via an engine_cache fixture so cleanup runs even when an assertion fails mid-test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Rafiuzzaman Khan <rafiuzzamank@gmail.com> Co-authored-by: chriscarrollsmith <chriscarrollsmith@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: github-actions <actions@users.noreply.github.com> Co-authored-by: Biruk Haileye Tabor <biruk.haileye@gmail.com> Co-authored-by: Rafiuzzaman Khan <rafiuzzamank@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Christopher Carroll Smith <chriscarrollsmith@users.noreply.github.com>
1 parent d64803e commit 040f634

13 files changed

Lines changed: 211 additions & 72 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ BASE_URL=http://localhost:8000
77
# Database connection mode (0 = direct, 1 = pooled)
88
USE_POOL=0
99

10+
# SQLAlchemy pool (per process; keep small when using PgBouncer)
11+
# DB_POOL_SIZE=5
12+
# DB_MAX_OVERFLOW=5
13+
1014
# Shared database settings
1115
DB_HOST=127.0.0.1
1216
DB_SSLMODE=prefer

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
<!-- version list -->
99

10+
## v1.1.8 (2026-07-14)
11+
12+
### Bug Fixes
13+
14+
- **ci**: Keep Test workflow push triggers unfiltered
15+
([#229](https://github.com/Promptly-Technologies-LLC/fastapi-jinja2-postgres-webapp/pull/229),
16+
[`2a88198`](https://github.com/Promptly-Technologies-LLC/fastapi-jinja2-postgres-webapp/commit/2a88198586079c45761afc69995a84586c76899d))
17+
18+
### Code Style
19+
20+
- **footer**: Improve site footer layout and typography
21+
([#226](https://github.com/Promptly-Technologies-LLC/fastapi-jinja2-postgres-webapp/pull/226),
22+
[`85b5fe9`](https://github.com/Promptly-Technologies-LLC/fastapi-jinja2-postgres-webapp/commit/85b5fe92b266ae7459631c336275c82eeba3a610))
23+
24+
### Documentation
25+
26+
- Document stripe branch and add propagate/CI support
27+
([#229](https://github.com/Promptly-Technologies-LLC/fastapi-jinja2-postgres-webapp/pull/229),
28+
[`2a88198`](https://github.com/Promptly-Technologies-LLC/fastapi-jinja2-postgres-webapp/commit/2a88198586079c45761afc69995a84586c76899d))
29+
30+
1031
## v1.1.7 (2026-07-06)
1132

1233
### Bug Fixes

main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from routers.app import billing as billing_router
5151
from utils.app.credentials import validate_billing_environment
5252
from utils.app.billing import resolve_billing_nav_href, should_resolve_billing_nav
53-
from utils.core.db import set_up_db
53+
from utils.core.db import set_up_db, clear_engine_cache
5454

5555
logger = logging.getLogger("uvicorn.error")
5656
logger.setLevel(logging.DEBUG)
@@ -62,6 +62,7 @@ async def lifespan(app: FastAPI):
6262
validate_billing_environment()
6363
set_up_db()
6464
yield
65+
clear_engine_cache()
6566

6667

6768
# Initialize the FastAPI app

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fastapi-jinja2-postgres-webapp"
3-
version = "1.1.7"
3+
version = "1.1.8"
44
description = "A template webapp with a pure-Python FastAPI backend, frontend templating with Jinja2, and a Postgres database to power user auth"
55
readme = "README.md"
66
package-mode = false

tests/browser/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from playwright.sync_api import Browser, Page
99
from tests.browser.db_helpers import browser_db_env
1010
from utils.core.db import (
11+
clear_engine_cache,
1112
ensure_database_exists,
1213
get_connection_url,
1314
set_up_db,
@@ -160,6 +161,7 @@ def live_server(browser_env):
160161
proc.wait(timeout=5)
161162
with _temporary_env(browser_env):
162163
tear_down_db()
164+
clear_engine_cache()
163165

164166

165167
@pytest.fixture(scope="session")
@@ -173,3 +175,4 @@ def live_server_csrf(browser_csrf_env):
173175
proc.wait(timeout=5)
174176
with _temporary_env(browser_csrf_env):
175177
tear_down_db()
178+
clear_engine_cache()

tests/browser/db_helpers.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
from datetime import timedelta
99

1010
from dotenv import load_dotenv
11-
from sqlmodel import Session, create_engine, select
11+
from sqlmodel import Session, select
1212

1313
from utils.core.auth import get_password_hash
14-
from utils.core.db import create_default_roles, get_connection_url
14+
from utils.core.db import create_default_roles, get_engine
1515
from utils.core.models import (
1616
Account,
1717
Invitation,
@@ -39,8 +39,7 @@ def browser_db_session():
3939
saved = {key: os.environ.get(key) for key in env}
4040
os.environ.update(env)
4141
try:
42-
engine = create_engine(get_connection_url())
43-
with Session(engine) as session:
42+
with Session(get_engine()) as session:
4443
yield session
4544
finally:
4645
for key, value in saved.items():
@@ -168,8 +167,7 @@ def browser_csrf_db_session():
168167
saved = {key: os.environ.get(key) for key in env}
169168
os.environ.update(env)
170169
try:
171-
engine = create_engine(get_connection_url())
172-
with Session(engine) as session:
170+
with Session(get_engine()) as session:
173171
yield session
174172
finally:
175173
for key, value in saved.items():

tests/conftest.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from typing import Generator, cast
44

55
pytest_plugins = ["tests.frontend.fixtures"]
6-
from sqlmodel import create_engine, Session, select
6+
from sqlmodel import Session, select
77
from fastapi.testclient import TestClient
88
from dotenv import load_dotenv
99
from utils.core.db import (
10+
clear_engine_cache,
1011
get_connection_url,
12+
get_engine,
1113
tear_down_db,
1214
set_up_db,
1315
create_default_roles,
@@ -85,15 +87,19 @@ def engine(env_vars):
8587
Create a new SQLModel engine for the test database.
8688
Use PostgreSQL for testing to match production environment.
8789
"""
88-
# Use PostgreSQL for testing to match production environment
90+
# Use PostgreSQL for testing to match production environment. get_engine()
91+
# returns the same cached pool the app itself uses via get_session(), so
92+
# TestClient requests and direct test queries share one pool instead of
93+
# opening a second one for the same database.
8994
ensure_database_exists(get_connection_url())
90-
engine = create_engine(get_connection_url())
95+
engine = get_engine()
9196
set_up_db(drop=True)
9297

9398
yield engine
9499

95100
# Clean up after tests
96101
tear_down_db()
102+
clear_engine_cache()
97103

98104

99105
@pytest.fixture

tests/utils/test_db.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from sqlalchemy import Engine
44
from utils.core.db import (
55
get_connection_url,
6+
get_engine,
7+
clear_engine_cache,
68
assign_permissions_to_role,
79
create_default_roles,
810
create_permissions,
@@ -147,6 +149,84 @@ def test_get_connection_url_missing_pool_vars(monkeypatch):
147149
get_connection_url()
148150

149151

152+
# --- Engine cache ---
153+
154+
155+
@pytest.fixture
156+
def engine_cache():
157+
"""Ensure engine-cache tests start clean and never leak cached engines,
158+
even when an assertion fails mid-test."""
159+
clear_engine_cache()
160+
yield
161+
clear_engine_cache()
162+
163+
164+
def _direct_db_env(monkeypatch, *, name: str = "testdb", password: str = "testpass"):
165+
for var in (
166+
"USE_POOL",
167+
"DB_HOST",
168+
"DB_PORT",
169+
"DB_NAME",
170+
"DB_USER",
171+
"DB_PASSWORD",
172+
"DB_POOL_PORT",
173+
"DB_POOL_NAME",
174+
"DB_APPUSER",
175+
"DB_APPUSER_PASSWORD",
176+
"DB_POOL_SIZE",
177+
"DB_MAX_OVERFLOW",
178+
):
179+
monkeypatch.delenv(var, raising=False)
180+
monkeypatch.setenv("DB_HOST", "localhost")
181+
monkeypatch.setenv("DB_PORT", "5432")
182+
monkeypatch.setenv("DB_NAME", name)
183+
monkeypatch.setenv("DB_USER", "testuser")
184+
monkeypatch.setenv("DB_PASSWORD", password)
185+
186+
187+
def test_get_engine_reuses_same_instance(engine_cache, monkeypatch):
188+
_direct_db_env(monkeypatch)
189+
assert get_engine() is get_engine()
190+
191+
192+
def test_get_engine_different_urls_get_different_engines(engine_cache, monkeypatch):
193+
_direct_db_env(monkeypatch, name="db_a")
194+
engine_a = get_engine()
195+
_direct_db_env(monkeypatch, name="db_b")
196+
engine_b = get_engine()
197+
assert engine_a is not engine_b
198+
199+
200+
def test_get_engine_not_keyed_by_masked_str_password(engine_cache, monkeypatch):
201+
"""str(URL) masks passwords; cache must still separate credentials."""
202+
_direct_db_env(monkeypatch, password="secretA")
203+
engine_a = get_engine()
204+
assert "***" in str(get_connection_url())
205+
_direct_db_env(monkeypatch, password="secretB")
206+
engine_b = get_engine()
207+
assert engine_a is not engine_b
208+
209+
210+
def test_clear_engine_cache_disposes_and_creates_new(engine_cache, monkeypatch):
211+
_direct_db_env(monkeypatch)
212+
first = get_engine()
213+
clear_engine_cache()
214+
second = get_engine()
215+
assert first is not second
216+
217+
218+
def test_get_engine_applies_pool_settings(engine_cache, monkeypatch):
219+
_direct_db_env(monkeypatch)
220+
monkeypatch.setenv("DB_POOL_SIZE", "3")
221+
monkeypatch.setenv("DB_MAX_OVERFLOW", "2")
222+
engine = get_engine()
223+
assert engine.pool.size() == 3
224+
# No public accessors for these; private attrs are stable in practice but
225+
# may need updating on a SQLAlchemy major upgrade.
226+
assert engine.pool._max_overflow == 2
227+
assert engine.pool._pre_ping is True
228+
229+
150230
# --- Permission and Role Tests ---
151231

152232

utils/core/auth.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from fastapi.templating import Jinja2Templates
1414
from fastapi import Cookie
1515
from starlette.responses import Response
16-
from utils.core.db import create_engine, get_connection_url
16+
from utils.core.db import get_engine
1717
from utils.core.models import (
1818
AccountRecoveryToken,
1919
EmailVerificationToken,
@@ -339,8 +339,7 @@ def send_reset_email_task(email: str) -> None:
339339
FastAPI background tasks should not reuse request-scoped resources from
340340
`yield` dependencies, because cleanup may run before the task executes.
341341
"""
342-
engine = create_engine(get_connection_url())
343-
with Session(engine) as session:
342+
with Session(get_engine()) as session:
344343
send_reset_email(email, session)
345344

346345

0 commit comments

Comments
 (0)