Skip to content

Commit b34a1a5

Browse files
committed
fix: replace SimpleConnectionPool with ThreadedConnectionPool (#60)
SimpleConnectionPool is NOT thread-safe. FastAPI serves concurrent requests across multiple threads, so two requests can receive the same connection simultaneously — causing 'connection already in use' errors, data corruption, and intermittent failures under load. Changes: - Replace SimpleConnectionPool with ThreadedConnectionPool (psycopg2's built-in thread-safe pool that uses a threading.Lock internally) - Add DB_POOL_MIN / DB_POOL_MAX env vars for pool size tuning (defaults: 2/10) - Add close_pool() method and call it during app shutdown (lifespan) - All existing code paths already use try/finally for connection return Closes #60
1 parent 4541dc4 commit b34a1a5

1 file changed

Lines changed: 32 additions & 6 deletions

File tree

api.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,17 +235,42 @@ def __init__(self):
235235
self._bets: Dict[str, dict] = {}
236236
self._positions: Dict[str, Dict[str, dict]] = {}
237237

238+
def close_pool(self):
239+
"""Close all connections in the pool.
240+
241+
Called during application shutdown to release database connections
242+
cleanly instead of leaving them to be garbage-collected.
243+
"""
244+
if self._pool is not None:
245+
try:
246+
self._pool.closeall()
247+
print("Connection pool closed")
248+
except Exception as e:
249+
print(f"Warning: error closing connection pool: {e}")
250+
238251
def _init_pool(self):
239-
"""Initialize the connection pool.
252+
"""Initialize a thread-safe connection pool.
253+
254+
Uses ThreadedConnectionPool instead of SimpleConnectionPool because
255+
FastAPI handles concurrent requests across multiple threads —
256+
SimpleConnectionPool is NOT thread-safe and can hand the same
257+
connection to two threads simultaneously, causing data corruption
258+
and 'connection already in use' errors under load.
259+
260+
Pool size is configurable via environment variables:
261+
DB_POOL_MIN – minimum connections kept open (default: 2)
262+
DB_POOL_MAX – maximum connections allowed (default: 10)
240263
241264
Configures TCP keepalives so Railway's Postgres proxy doesn't silently
242265
drop idle connections, and sets a statement timeout as a safety net
243266
against queries that hang forever.
244267
"""
245268
parsed = urlparse(self.database_url)
246-
self._pool = pool.SimpleConnectionPool(
247-
minconn=1,
248-
maxconn=10,
269+
min_conn = int(os.getenv("DB_POOL_MIN", "2"))
270+
max_conn = int(os.getenv("DB_POOL_MAX", "10"))
271+
self._pool = pool.ThreadedConnectionPool(
272+
minconn=min_conn,
273+
maxconn=max_conn,
249274
host=parsed.hostname,
250275
port=parsed.port or 5432,
251276
user=parsed.username,
@@ -259,7 +284,7 @@ def _init_pool(self):
259284
keepalives_count=3, # Give up after 3 missed keepalives
260285
options='-c statement_timeout=30000', # 30s query timeout
261286
)
262-
print("Connection pool initialized (min=1, max=10, keepalives=on, statement_timeout=30s)")
287+
print(f"ThreadedConnectionPool initialized (min={min_conn}, max={max_conn}, keepalives=on, statement_timeout=30s)")
263288

264289
def _get_conn(self):
265290
"""Get a database connection from the pool.
@@ -1646,7 +1671,8 @@ async def lifespan(app: FastAPI):
16461671
user_count = len(db.users)
16471672
print(f"MoltMarkets API started with {market_count} markets, {user_count} users")
16481673
yield
1649-
# Shutdown
1674+
# Shutdown: close the connection pool cleanly
1675+
db.close_pool()
16501676
print("MoltMarkets API shutting down")
16511677

16521678

0 commit comments

Comments
 (0)