From 2b4905f8fdc26e81adf3c5cd35f1ab7ebf5490b0 Mon Sep 17 00:00:00 2001 From: herbygitea Date: Wed, 15 Apr 2026 23:00:32 +0000 Subject: [PATCH] Create pbs-api-db-connection-fix.md via n8n --- Tech/Sessions/pbs-api-db-connection-fix.md | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 Tech/Sessions/pbs-api-db-connection-fix.md diff --git a/Tech/Sessions/pbs-api-db-connection-fix.md b/Tech/Sessions/pbs-api-db-connection-fix.md new file mode 100644 index 0000000..1b93f7e --- /dev/null +++ b/Tech/Sessions/pbs-api-db-connection-fix.md @@ -0,0 +1,94 @@ +--- +project: pbs-api-db-connection-fix +type: session-notes +status: active +path: Tech/Sessions +tags: + - pbs + - pbs-api + - flask + - mysql + - sqlalchemy + - docker +created: 2026-04-15 +updated: 2026-04-15 +--- + +# PBS API — DB Connection Pool Fix + +## Problem + +The pbs-api container was periodically appearing unavailable (content +hub inaccessible). The container was not crashing — it was being +marked unhealthy by Docker, causing Traefik to pull it from rotation. + +Root cause: SQLAlchemy connection pool holding idle MySQL connections +that MySQL had already closed server-side. On next use, the dead +connection produces a broken pipe error. If this occurs during a +healthcheck window, the health endpoint fails 3x and Traefik stops +routing traffic. + +Confirmed via logs: +- `pymysql.err.OperationalError: (2006, "MySQL server has gone away +(BrokenPipeError(32, 'Broken pipe'))")` + +Container state confirmed clean — exit code 0, no OOM kill, restart +policy `unless-stopped`. + +## Fix + +Update `get_engine()` in the database module to add connection pool +resilience for MySQL connections. + +### Change: `get_engine()` function + +Locate the `get_engine()` function (currently uses bare +`create_engine(url, echo=False)` for all DB types) and replace with +the following: + +```python +def get_engine() -> Engine: + global _engine + if _engine is None: + url = get_database_url() + if url.startswith("sqlite"): + from pathlib import Path + db_path = Path(url.replace("sqlite:///", "")) + db_path.parent.mkdir(parents=True, exist_ok=True) + _engine = create_engine(url, echo=False) + else: + _engine = create_engine( + url, + echo=False, + pool_pre_ping=True, # test connection before use, +transparently reconnects dead ones + pool_recycle=1800, # proactively recycle +connections every 30 min + ) + return _engine +``` + +### Why this works + +- `pool_pre_ping=True` — issues a lightweight `SELECT 1` before each +query. If the connection is dead, SQLAlchemy silently replaces it. +This is the self-healing mechanism. +- `pool_recycle=1800` — proactively replaces connections older than 30 +minutes, before MySQL's `wait_timeout` closes them server-side. +- SQLite path is unchanged — pooling does not apply. + +## Outcome + +With `pool_pre_ping` in place, dead connections are detected and +replaced transparently before they can cause a request failure or +health check miss. No container restart required. + +## Notes + +- No changes needed to the `/api/health` endpoint — it is a simple 200 +and does not touch the DB, which is correct. +- Docker's `unless-stopped` restart policy does not auto-restart on +healthcheck failure — this is by design and does not need to change +given the root cause is now addressed. +- Autoheal (docker-autoheal) was considered and deferred — not needed +with pool_pre_ping in place. \ No newline at end of file