Skip to content

Commit f90a8cf

Browse files
fix(perf): migra actions/xray al pool + test de aislamiento de claims + log en lifespan
- ActionRepository._connect y XrayConfig._connect migrados a get_pool().connection() (eliminado psycopg.connect directo por petición; row_factory/vector los gestiona el pool) - test_pooled_connection_does_not_leak_claims: test de integración con pool max_size=1 que verifica que set_config transaction-local no se filtra entre checkouts - asgi.py lifespan: except Exception:pass → logging.warning con el exc (no swallow silencioso) Claude-Session: https://claude.ai/code/session_0198KfgRWvAM8BhiVz24uTok
1 parent 4f5be92 commit f90a8cf

4 files changed

Lines changed: 35 additions & 8 deletions

File tree

asgi.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
async def lifespan(app: FastAPI):
2121
try:
2222
get_pool() # pre-calienta el pool; si la BD no está, no tumbar el arranque
23-
except Exception:
24-
pass
23+
except Exception as exc:
24+
import logging
25+
logging.getLogger(__name__).warning("pool pre-warm failed; lazy init on first request: %s", exc)
2526
yield
2627
close_pool()
2728

src/actions/repository.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from typing import Any, Dict, List, Optional
22

33
import psycopg
4-
from psycopg.rows import dict_row
54
from psycopg.types.json import Json
65

76
from src.config import DATABASE_URL
7+
from src.db.pool import get_pool
88

99

1010
class ActionRepository:
@@ -16,8 +16,8 @@ def __init__(self, db_url: str = DATABASE_URL):
1616
raise RuntimeError("DATABASE_URL must be configured for Mnemo persistence")
1717
self.db_url = db_url
1818

19-
def _connect(self) -> psycopg.Connection:
20-
return psycopg.connect(self.db_url, row_factory=dict_row)
19+
def _connect(self):
20+
return get_pool().connection()
2121

2222
def _set_claims(self, conn: psycopg.Connection, user_id: str) -> None:
2323
with conn.cursor() as cur:

src/xray/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
from typing import Any, Dict, Optional
2222

2323
import psycopg
24-
from psycopg.rows import dict_row
2524
from cryptography.fernet import InvalidToken
2625

2726
from src.config import DATABASE_URL
27+
from src.db.pool import get_pool
2828
from src.jira.crypto import decrypt_token, encrypt_token
2929

3030
_CLOUD_BASE = "https://xray.cloud.getxray.app"
@@ -43,8 +43,8 @@ def __init__(self, db_url: str = DATABASE_URL) -> None:
4343
# Private helpers
4444
# ------------------------------------------------------------------
4545

46-
def _connect(self) -> psycopg.Connection:
47-
return psycopg.connect(self.db_url, row_factory=dict_row)
46+
def _connect(self):
47+
return get_pool().connection()
4848

4949
def _decrypt(self, enc: str) -> str:
5050
try:

tests/test_db_pool.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import os
2+
3+
import pytest
4+
from dotenv import load_dotenv
5+
6+
load_dotenv()
7+
18
import src.db.pool as poolmod
29

310

@@ -24,3 +31,22 @@ class Conn: row_factory = None
2431
from psycopg.rows import dict_row
2532
assert c.row_factory is dict_row
2633
assert calls["vector"] == 1
34+
35+
36+
@pytest.mark.integration
37+
def test_pooled_connection_does_not_leak_claims():
38+
url = os.getenv("DATABASE_URL")
39+
if not url:
40+
pytest.skip("DATABASE_URL not configured")
41+
from psycopg_pool import ConnectionPool
42+
from psycopg.rows import dict_row
43+
pool = ConnectionPool(conninfo=url, min_size=1, max_size=1, open=True, kwargs={"row_factory": dict_row})
44+
try:
45+
with pool.connection() as c1:
46+
c1.execute("select set_config('request.jwt.claim.sub', 'user-A', true)")
47+
# is_local=true → scoped to this transaction; commit on block exit clears it
48+
with pool.connection() as c2: # same physical connection (max_size=1)
49+
row = c2.execute("select current_setting('request.jwt.claim.sub', true) as v").fetchone()
50+
assert row["v"] in (None, ""), f"claim leaked across checkouts: {row['v']!r}"
51+
finally:
52+
pool.close()

0 commit comments

Comments
 (0)