пул соединений psycopg2 (min=1 max=3), db.put_conn вместо close, v1.20

This commit is contained in:
2026-06-17 18:23:41 +04:00
parent dfb9a4218b
commit 2615e781ce
3 changed files with 51 additions and 35 deletions
+4 -4
View File
@@ -118,7 +118,7 @@ class ContractsApp:
contract_id = cur.fetchone()[0] contract_id = cur.fetchone()[0]
conn.commit() conn.commit()
cur.close() cur.close()
conn.close() db.put_conn(conn)
for f in files: for f in files:
if not f.filename: if not f.filename:
@@ -209,11 +209,11 @@ class ContractsApp:
if not doc_id: if not doc_id:
conn.rollback() conn.rollback()
cur.close() cur.close()
conn.close() db.put_conn(conn)
return jsonify({"error": "Не удалось сохранить файл в БД"}), 500 return jsonify({"error": "Не удалось сохранить файл в БД"}), 500
cur.close() cur.close()
conn.close() db.put_conn(conn)
_log("assembly_done", f"cid={contract_id}") _log("assembly_done", f"cid={contract_id}")
except Exception as e: except Exception as e:
_log("assembly_error", str(e)) _log("assembly_error", str(e))
@@ -221,7 +221,7 @@ class ContractsApp:
except: pass except: pass
try: cur.close() try: cur.close()
except: pass except: pass
try: conn.close() try: db.put_conn(conn)
except: pass except: pass
return jsonify({"error": f"Сборка: {e}"}), 500 return jsonify({"error": f"Сборка: {e}"}), 500
finally: finally:
+46 -30
View File
@@ -1,27 +1,38 @@
""" """
db.py — Транспортный слой к базе данных. db.py — Транспортный слой к базе данных (с пулом соединений).
Только connect() и query(). Никакой бизнес-логики, никакого DDL. Пул psycopg2.pool.ThreadedConnectionPool (min=1, max=3).
DDL → schema.py. Бизнес-запросы → upload.py, extractor.py, api.py. Все query/execute забирают соединение из пула и возвращают обратно.
Функции:
_pg_connect(dbname) — сырое подключение к ЛЮБОЙ базе (используется /test createdb)
connect() — подключение к ЦЕЛЕВОЙ базе (из DB_NAME)
query(sql, params) — выполнить SELECT → (result, error)
execute(sql, params) — выполнить INSERT/UPDATE/DELETE → (rowcount, error)
query_one(sql, params) — как query, но возвращает одну строку или None
""" """
import os import os
from psycopg2 import pool as _pgpool
import psycopg2 import psycopg2
_pool = None
def _get_pool():
"""Ленивая инициализация пула."""
global _pool
if _pool is None:
_pool = _pgpool.ThreadedConnectionPool(
minconn=1,
maxconn=3,
host=os.getenv("DB_HOST"),
port=os.getenv("DB_PORT", 5432),
dbname=os.getenv("DB_NAME"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASS"),
sslmode=os.getenv("DB_SSLMODE", "disable"),
)
return _pool
def _pg_connect(dbname): def _pg_connect(dbname):
""" """
Сырое подключение к указанной базе данных. Сырое подключение к ЛЮБОЙ базе (для /test createdb).
Используется: НЕ из пула — для создания БД нужна отдельная сессия.
- connect() для целевой БД
- test_routes.py для /test createdb (подключение к 'postgres')
""" """
return psycopg2.connect( return psycopg2.connect(
host=os.getenv("DB_HOST"), host=os.getenv("DB_HOST"),
@@ -35,20 +46,26 @@ def _pg_connect(dbname):
def connect(): def connect():
""" """
Подключение к ЦЕЛЕВОЙ базе данных (DB_NAME из переменных окружения). Подключение к ЦЕЛЕВОЙ базе из пула.
Возвращает (connection, None) при успехе или (None, error) при ошибке. Возвращает (connection, None) или (None, error).
""" """
try: try:
return _pg_connect(os.getenv("DB_NAME")), None return _get_pool().getconn(), None
except Exception as e: except Exception as e:
return None, str(e) return None, str(e)
def put_conn(conn):
"""Вернуть соединение в пул."""
try:
_get_pool().putconn(conn)
except Exception:
pass
def query(sql_text, params=None): def query(sql_text, params=None):
""" """
Выполнить SQL-запрос к целевой БД. SELECT → (result, None) или (None, error).
Возвращает (result, None) или (None, error).
result = {"columns": [...], "rows": [[...], ...]} result = {"columns": [...], "rows": [[...], ...]}
""" """
conn, err = connect() conn, err = connect()
@@ -60,17 +77,16 @@ def query(sql_text, params=None):
rows = cur.fetchall() rows = cur.fetchall()
cols = [desc[0] for desc in cur.description] if cur.description else [] cols = [desc[0] for desc in cur.description] if cur.description else []
cur.close() cur.close()
conn.close()
return {"columns": cols, "rows": [list(r) for r in rows]}, None return {"columns": cols, "rows": [list(r) for r in rows]}, None
except Exception as e: except Exception as e:
conn.close()
return None, str(e) return None, str(e)
finally:
put_conn(conn)
def execute(sql_text, params=None): def execute(sql_text, params=None):
""" """
Выполнить INSERT/UPDATE/DELETE к целевой БД. INSERT/UPDATE/DELETE → (rowcount, None) или (None, error).
Возвращает (rowcount, None) или (None, error).
""" """
conn, err = connect() conn, err = connect()
if err: if err:
@@ -81,23 +97,23 @@ def execute(sql_text, params=None):
conn.commit() conn.commit()
rowcount = cur.rowcount rowcount = cur.rowcount
cur.close() cur.close()
conn.close()
return rowcount, None return rowcount, None
except Exception as e: except Exception as e:
conn.rollback() try: conn.rollback()
conn.close() except: pass
return None, str(e) return None, str(e)
finally:
put_conn(conn)
def query_one(sql_text, params=None): def query_one(sql_text, params=None):
""" """
Выполнить SELECT и вернуть ПЕРВУЮ строку или None. SELECT одной строки → (row_dict, None) или (None, error).
Возвращает (row_dict, None) или (None, error).
""" """
result, err = query(sql_text, params) result, err = query(sql_text, params)
if err: if err:
return None, err return None, err
rows = result["rows"] rows = result["rows"]
if not rows: if not rows:
return None, None # нет строк — не ошибка return None, None
return dict(zip(result["columns"], rows[0])), None return dict(zip(result["columns"], rows[0])), None
+1 -1
View File
@@ -59,7 +59,7 @@
<body> <body>
<div class="topbar"> <div class="topbar">
<img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes"> <img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes">
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.19</span></span> <span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.20</span></span>
</div> </div>
<div class="content"> <div class="content">