Skip to content

Commit fcd260d

Browse files
Merge pull request #88 from spiceoogway/fix/threaded-connection-pool
fix: replace SimpleConnectionPool with ThreadedConnectionPool
2 parents f663284 + b34a1a5 commit fcd260d

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
@@ -246,17 +246,42 @@ def __init__(self):
246246
self._bets: Dict[str, dict] = {}
247247
self._positions: Dict[str, Dict[str, dict]] = {}
248248

249+
def close_pool(self):
250+
"""Close all connections in the pool.
251+
252+
Called during application shutdown to release database connections
253+
cleanly instead of leaving them to be garbage-collected.
254+
"""
255+
if self._pool is not None:
256+
try:
257+
self._pool.closeall()
258+
print("Connection pool closed")
259+
except Exception as e:
260+
print(f"Warning: error closing connection pool: {e}")
261+
249262
def _init_pool(self):
250-
"""Initialize the connection pool.
263+
"""Initialize a thread-safe connection pool.
264+
265+
Uses ThreadedConnectionPool instead of SimpleConnectionPool because
266+
FastAPI handles concurrent requests across multiple threads —
267+
SimpleConnectionPool is NOT thread-safe and can hand the same
268+
connection to two threads simultaneously, causing data corruption
269+
and 'connection already in use' errors under load.
270+
271+
Pool size is configurable via environment variables:
272+
DB_POOL_MIN – minimum connections kept open (default: 2)
273+
DB_POOL_MAX – maximum connections allowed (default: 10)
251274
252275
Configures TCP keepalives so Railway's Postgres proxy doesn't silently
253276
drop idle connections, and sets a statement timeout as a safety net
254277
against queries that hang forever.
255278
"""
256279
parsed = urlparse(self.database_url)
257-
self._pool = pool.SimpleConnectionPool(
258-
minconn=1,
259-
maxconn=10,
280+
min_conn = int(os.getenv("DB_POOL_MIN", "2"))
281+
max_conn = int(os.getenv("DB_POOL_MAX", "10"))
282+
self._pool = pool.ThreadedConnectionPool(
283+
minconn=min_conn,
284+
maxconn=max_conn,
260285
host=parsed.hostname,
261286
port=parsed.port or 5432,
262287
user=parsed.username,
@@ -270,7 +295,7 @@ def _init_pool(self):
270295
keepalives_count=3, # Give up after 3 missed keepalives
271296
options='-c statement_timeout=30000', # 30s query timeout
272297
)
273-
print("Connection pool initialized (min=1, max=10, keepalives=on, statement_timeout=30s)")
298+
print(f"ThreadedConnectionPool initialized (min={min_conn}, max={max_conn}, keepalives=on, statement_timeout=30s)")
274299

275300
def _get_conn(self):
276301
"""Get a database connection from the pool.
@@ -1657,7 +1682,8 @@ async def lifespan(app: FastAPI):
16571682
user_count = len(db.users)
16581683
print(f"MoltMarkets API started with {market_count} markets, {user_count} users")
16591684
yield
1660-
# Shutdown
1685+
# Shutdown: close the connection pool cleanly
1686+
db.close_pool()
16611687
print("MoltMarkets API shutting down")
16621688

16631689

0 commit comments

Comments
 (0)