""" Datenbank-Anbindung für die WDM-Performance-Anwendung. Verwendet einen Thread-sicheren Connection-Pool. Beim Start wird mehrfach versucht die Verbindung aufzubauen, da Postgres im Docker-Setup u.U. noch nicht bereit ist, wenn der App-Container startet. """ import os import time import logging from contextlib import contextmanager import psycopg2 import psycopg2.extras from psycopg2.pool import ThreadedConnectionPool log = logging.getLogger("wdm-performance") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") DB_HOST = os.environ.get("DB_HOST", "db") DB_PORT = os.environ.get("DB_PORT", "5432") DB_NAME = os.environ.get("DB_NAME", "wdm") DB_USER = os.environ.get("DB_USER", "wdm") DB_PASSWORD = os.environ.get("DB_PASSWORD", "wdm") _pool = None def init_pool(max_versuche=30, wartezeit_sekunden=2): """Baut den Connection-Pool auf. Versucht es mehrfach, falls die DB beim Containerstart noch nicht erreichbar ist (kein harter Absturz).""" global _pool for versuch in range(1, max_versuche + 1): try: _pool = ThreadedConnectionPool( minconn=1, maxconn=10, host=DB_HOST, port=DB_PORT, dbname=DB_NAME, user=DB_USER, password=DB_PASSWORD, connect_timeout=5, ) log.info("Datenbankverbindung zu %s:%s/%s hergestellt.", DB_HOST, DB_PORT, DB_NAME) return except psycopg2.OperationalError as exc: if versuch == max_versuche: log.error("Konnte nach %d Versuchen keine DB-Verbindung herstellen: %s", max_versuche, exc) raise log.warning("DB noch nicht erreichbar (Versuch %d/%d), warte %ds ...", versuch, max_versuche, wartezeit_sekunden) time.sleep(wartezeit_sekunden) @contextmanager def get_conn(): """Liefert eine Connection aus dem Pool. Bei Fehlern wird die Connection verworfen statt sie kaputt zurückzugeben (Stabilität bei Langzeitbetrieb).""" if _pool is None: init_pool() conn = _pool.getconn() try: yield conn conn.commit() except Exception: conn.rollback() raise finally: _pool.putconn(conn) @contextmanager def get_cursor(dict_cursor=True): with get_conn() as conn: cursor_factory = psycopg2.extras.RealDictCursor if dict_cursor else None cur = conn.cursor(cursor_factory=cursor_factory) try: yield cur finally: cur.close() def migrate(): """Leichte, idempotente Schema-Migrationen für bereits laufende Installationen. `init.sql` wird nur beim allerersten Start (leeres Datenvolume) ausgeführt - bestehende Installationen bekommen neue Spalten stattdessen hier beim App-Start nachgerüstet.""" with get_cursor() as cur: cur.execute('ALTER TABLE "Auftraege" ADD COLUMN IF NOT EXISTS "artikel" text') log.info("Schema-Migration geprüft/angewendet.")