Files
Autoparts-DB/pos/tenant_db.py
consultoria-as 0e549e7746 fix: connection pool exhaustion + cross_ref column name
- tenant_db.py: add rollback() before returning conn to pool to prevent
  'idle in transaction (aborted)' state that exhausts the pool
- tenant_db.py: increase pool maxconn from 10 to 20 for better concurrency
- inventory_vehicle_compat.py: fix column name cross_ref_number ->
  cross_reference_number to match actual schema
2026-05-01 02:25:58 +00:00

108 lines
3.2 KiB
Python

# /home/Autopartes/pos/tenant_db.py
"""Tenant DB connection manager with pooling.
Uses psycopg2.pool.ThreadedConnectionPool for both master and tenant DBs.
Connections are returned to the pool on .close() via a thin wrapper —
this keeps the rest of the codebase unchanged.
"""
import psycopg2
from psycopg2 import pool
from config import MASTER_DB_URL, TENANT_DB_URL_TEMPLATE
# ─── Pools ─────────────────────────────────────
_master_pool = None
_tenant_pools = {}
def _get_master_pool():
"""Lazy-initialize master DB connection pool."""
global _master_pool
if _master_pool is None:
_master_pool = pool.ThreadedConnectionPool(
minconn=2, maxconn=20, dsn=MASTER_DB_URL
)
return _master_pool
def _get_tenant_pool(db_name):
"""Lazy-initialize tenant DB connection pool by db_name."""
global _tenant_pools
if db_name not in _tenant_pools:
dsn = TENANT_DB_URL_TEMPLATE.format(db_name=db_name)
_tenant_pools[db_name] = pool.ThreadedConnectionPool(
minconn=2, maxconn=20, dsn=dsn
)
return _tenant_pools[db_name]
class _PooledConnection:
"""Thin wrapper that delegates all attribute access to the real
psycopg2 connection, but returns it to the pool on .close().
"""
__slots__ = ('_conn', '_pool')
def __init__(self, conn, db_pool):
self._conn = conn
self._pool = db_pool
def __getattr__(self, name):
return getattr(self._conn, name)
def close(self):
try:
# Rollback any aborted transaction before returning to pool.
# Without this, failed transactions leave connections in
# 'idle in transaction (aborted)' state, eventually exhausting
# the pool.
if self._conn:
try:
self._conn.rollback()
except Exception:
pass
self._pool.putconn(self._conn)
except Exception:
# If pool is already closed, fall back to real close
self._conn.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
# ─── Public API ────────────────────────────────
def get_master_conn():
"""Get a pooled connection to the master DB."""
p = _get_master_pool()
return _PooledConnection(p.getconn(), p)
def get_tenant_conn(tenant_id):
"""Get a pooled connection to a tenant's DB."""
master = get_master_conn()
cur = master.cursor()
cur.execute(
"SELECT db_name FROM tenants WHERE id = %s AND is_active = true",
(tenant_id,)
)
row = cur.fetchone()
cur.close()
master.close()
if not row:
raise ValueError(f"Tenant {tenant_id} not found or inactive")
db_name = row[0]
p = _get_tenant_pool(db_name)
return _PooledConnection(p.getconn(), p)
def get_tenant_conn_by_dbname(db_name):
"""Get a pooled connection to a tenant DB directly by name."""
p = _get_tenant_pool(db_name)
return _PooledConnection(p.getconn(), p)