ops: wait for the database on startup (no crash-loop on a slow DB)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 7s
CI & Build / Build & push image (push) Successful in 33s

New thoughtsync.dbwait polls the DB (SELECT 1) up to 60×1s before startup,
logging each attempt, so a briefly slow/unready database no longer crash-
loops the container. Wired as `python -m thoughtsync.dbwait &&` ahead of
`alembic upgrade head` in the image CMD and the dev compose command; exits
non-zero after the window so a restart policy can take over. Prod compose
gains restart: unless-stopped as the complementary piece.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-20 20:10:45 -04:00
co-authored by Claude Opus 4.8
parent 08258f81d9
commit fe12859ba2
4 changed files with 61 additions and 4 deletions
+54
View File
@@ -0,0 +1,54 @@
from __future__ import annotations
import asyncio
import sys
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from .config import Config
ATTEMPTS = 60
DELAY_SECONDS = 1.0
async def _probe(url: str) -> None:
engine = create_async_engine(url)
try:
async with engine.connect() as conn:
await conn.execute(text("SELECT 1"))
finally:
await engine.dispose()
async def wait_for_db(attempts: int = ATTEMPTS, delay: float = DELAY_SECONDS) -> bool:
"""Poll the database until it accepts a connection, up to `attempts` tries.
Startup runs `alembic upgrade head` as the container's first DB touch, so a
database that is a moment slow to accept connections would otherwise crash the
container. This gives it a short window to come up, verifying once per `delay`
seconds, instead of failing on the first missed connection.
"""
url = Config.DATABASE_URL
for attempt in range(1, attempts + 1):
try:
await _probe(url)
print(f"[dbwait] database ready (attempt {attempt}/{attempts})", flush=True)
return True
except Exception as exc:
print(
f"[dbwait] not ready (attempt {attempt}/{attempts}): {exc.__class__.__name__}: {exc}",
flush=True,
)
if attempt < attempts:
await asyncio.sleep(delay)
print(f"[dbwait] database unreachable after {attempts} attempts; giving up", flush=True)
return False
def main() -> int:
return 0 if asyncio.run(wait_for_db()) else 1
if __name__ == "__main__":
sys.exit(main())