From 7532e20d1e7c961ea82d6fef96781e90ae54524a Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 10 Jul 2026 17:32:20 +0200 Subject: [PATCH] fix: worker waits for schema before claiming; npm ci for reproducible builds Co-Authored-By: Claude Opus 4.8 (1M context) --- web/Dockerfile | 2 +- worker/lyra_worker/db.py | 32 ++++++++++++++++++++++++++++++++ worker/lyra_worker/main.py | 4 ++-- worker/tests/test_db.py | 11 +++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 worker/tests/test_db.py diff --git a/web/Dockerfile b/web/Dockerfile index 7e4782a..132e72a 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -4,7 +4,7 @@ RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* COPY package.json package-lock.json* ./ # Copy schema before install so package.json's postinstall (prisma generate) can run COPY prisma ./prisma -RUN npm install +RUN npm ci COPY . . RUN npx prisma generate && npm run build diff --git a/worker/lyra_worker/db.py b/worker/lyra_worker/db.py index 8c89282..e3e882f 100644 --- a/worker/lyra_worker/db.py +++ b/worker/lyra_worker/db.py @@ -1,4 +1,5 @@ import os +import time import psycopg @@ -6,3 +7,34 @@ import psycopg def connect() -> psycopg.Connection: dsn = os.environ["DATABASE_URL"] return psycopg.connect(dsn) + + +def wait_for_db(timeout: float = 60.0, interval: float = 2.0) -> psycopg.Connection: + """Block until the database is up and the `Job` table exists. + + Repeatedly opens a connection and probes for the `Job` table's presence. + Tolerates the DB not being reachable yet (`psycopg.OperationalError`) and + migrations not having run yet (`psycopg.errors.UndefinedTable`), retrying + with a fixed `interval` until `timeout` elapses. Returns the open, usable + connection on success; re-raises the last error on timeout. + """ + deadline = time.monotonic() + timeout + printed_waiting = False + last_error: Exception | None = None + while time.monotonic() < deadline: + conn: psycopg.Connection | None = None + try: + conn = connect() + with conn.cursor() as cur: + cur.execute('SELECT 1 FROM "Job" LIMIT 0') + return conn + except (psycopg.OperationalError, psycopg.errors.UndefinedTable) as exc: + last_error = exc + if conn is not None: + conn.close() + if not printed_waiting: + print("worker: waiting for database schema...", flush=True) + printed_waiting = True + time.sleep(interval) + assert last_error is not None + raise last_error diff --git a/worker/lyra_worker/main.py b/worker/lyra_worker/main.py index bbfe538..19f897b 100644 --- a/worker/lyra_worker/main.py +++ b/worker/lyra_worker/main.py @@ -1,7 +1,7 @@ import time from lyra_worker.claim import claim_next -from lyra_worker.db import connect +from lyra_worker.db import wait_for_db from lyra_worker.pipeline import run_pipeline IDLE_SLEEP = 2.0 @@ -9,7 +9,7 @@ STAGE_DELAY = 1.0 # visible progress in the UI def run_forever() -> None: - conn = connect() + conn = wait_for_db() print("worker: waiting for jobs", flush=True) try: while True: diff --git a/worker/tests/test_db.py b/worker/tests/test_db.py new file mode 100644 index 0000000..7c2f788 --- /dev/null +++ b/worker/tests/test_db.py @@ -0,0 +1,11 @@ +from lyra_worker.db import wait_for_db + + +def test_wait_for_db_returns_usable_connection_when_schema_ready(conn): + # `conn` fixture guarantees the DB is up and the schema is migrated. + result = wait_for_db(timeout=5) + try: + with result.cursor() as cur: + cur.execute('SELECT 1 FROM "Job" LIMIT 0') + finally: + result.close()