FASE 7b: DB Performance — Pooling, Stock Summary, N+1 fix
Cambios implementados: 1. Connection pooling (tenant_db.py): - psycopg2.pool.ThreadedConnectionPool para master y tenants - Wrapper _PooledConnection que devuelve al pool en .close() - Cero cambios en blueprints (backward compatible) 2. Tabla inventory_stock_summary + triggers (v3.2): - O(1) stock lookup en vez de SUM() sobre historial completo - Trigger AFTER INSERT en inventory_operations recalcula stock - Poblada inicialmente en ambos tenants - Refactor en 6 archivos de servicios para usar la nueva tabla 3. Fix N+1 en process_sale (pos_engine.py): - Precarga retail_price en bulk query FOR UPDATE - Elimina SELECT individual por item en loop 4. Índices críticos: - idx_parts_name_part + pattern_ops (master) - idx_inv_ops_inventory_branch_created (tenants) - idx_wi_part_stock_positive (master, ya existía desde Fase 1) Tests: 73/73 pasando (compat + fase3 + fase5 + fase6) Migración: v3.2_db_performance.sql
This commit is contained in:
@@ -1,20 +1,85 @@
|
||||
# /home/Autopartes/pos/tenant_db.py
|
||||
"""Tenant DB connection manager. Gets a psycopg2 connection for a specific tenant."""
|
||||
"""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=10, 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=10, 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:
|
||||
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 connection to nexus_master DB."""
|
||||
return psycopg2.connect(MASTER_DB_URL)
|
||||
"""Get a pooled connection to the master DB."""
|
||||
p = _get_master_pool()
|
||||
return _PooledConnection(p.getconn(), p)
|
||||
|
||||
|
||||
def get_tenant_conn(tenant_id):
|
||||
"""Get connection to a tenant's DB by looking up db_name in nexus_master."""
|
||||
"""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,))
|
||||
cur.execute(
|
||||
"SELECT db_name FROM tenants WHERE id = %s AND is_active = true",
|
||||
(tenant_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
master.close()
|
||||
@@ -23,9 +88,11 @@ def get_tenant_conn(tenant_id):
|
||||
raise ValueError(f"Tenant {tenant_id} not found or inactive")
|
||||
|
||||
db_name = row[0]
|
||||
return psycopg2.connect(TENANT_DB_URL_TEMPLATE.format(db_name=db_name))
|
||||
p = _get_tenant_pool(db_name)
|
||||
return _PooledConnection(p.getconn(), p)
|
||||
|
||||
|
||||
def get_tenant_conn_by_dbname(db_name):
|
||||
"""Get connection to a tenant DB directly by name."""
|
||||
return psycopg2.connect(TENANT_DB_URL_TEMPLATE.format(db_name=db_name))
|
||||
"""Get a pooled connection to a tenant DB directly by name."""
|
||||
p = _get_tenant_pool(db_name)
|
||||
return _PooledConnection(p.getconn(), p)
|
||||
|
||||
Reference in New Issue
Block a user