@@ -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