Fix worker race condition - single uvicorn worker + tolerate duplicates
Build and push images / build-backend (push) Successful in 5s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 5s

This commit is contained in:
2026-06-06 15:09:22 +01:00
parent 5a57e84e80
commit bfb3daba05
2 changed files with 41 additions and 23 deletions
+3 -2
View File
@@ -11,5 +11,6 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Tables are created at runtime by SQLAlchemy in app/main.py lifespan
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
# Single worker avoids race condition during DB initialization.
# For a personal app this is fine; async handles concurrent requests well.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+38 -21
View File
@@ -10,21 +10,27 @@ from app.api import auth, activities, routes, health, records, upload
async def init_db():
"""Create tables then seed admin, with retries for slow DB startup."""
for attempt in range(10):
"""Create tables then seed admin, with retries for slow DB startup.
Multiple uvicorn workers may race here on first start. We tolerate
duplicate table errors since they just mean another worker got there first.
"""
for attempt in range(15):
try:
# Step 1: create all tables (separate connection so it commits cleanly)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
break
except Exception as e:
if attempt == 9:
msg = str(e).lower()
if "already exists" in msg or "duplicate" in msg or "pg_type_typname" in msg:
print("Tables already created by another worker - skipping")
break
if attempt == 14:
raise
print(f"DB not ready yet (attempt {attempt + 1}/10): {e}")
await asyncio.sleep(3)
print(f"DB not ready yet (attempt {attempt + 1}/15): {e}")
await asyncio.sleep(2)
# Step 2: try to enable TimescaleDB hypertable (separate connection,
# failure here is non-fatal - falls back to plain Postgres)
# Try TimescaleDB hypertable (non-fatal)
try:
async with engine.begin() as conn:
await conn.execute(text(
@@ -34,24 +40,35 @@ async def init_db():
except Exception as e:
print(f"TimescaleDB hypertable skipped: {e}")
# Step 3: seed admin user
# Seed admin user (only if password is configured)
if not settings.admin_password:
print("ADMIN_PASSWORD not set - skipping admin user seed")
return
from sqlalchemy import select
from app.models.user import User
from app.core.security import hash_password
async with AsyncSessionLocal() as db:
result = await db.execute(
select(User).where(User.username == settings.admin_username)
)
if not result.scalar_one_or_none():
admin = User(
username=settings.admin_username,
hashed_password=hash_password(settings.admin_password),
is_admin=True,
try:
async with AsyncSessionLocal() as db:
result = await db.execute(
select(User).where(User.username == settings.admin_username)
)
db.add(admin)
await db.commit()
print(f"Admin user '{settings.admin_username}' created")
if not result.scalar_one_or_none():
admin = User(
username=settings.admin_username,
hashed_password=hash_password(settings.admin_password),
is_admin=True,
)
db.add(admin)
await db.commit()
print(f"Admin user '{settings.admin_username}' created")
except Exception as e:
msg = str(e).lower()
if "duplicate" in msg or "unique" in msg:
print("Admin user already exists - skipping seed")
else:
raise
@asynccontextmanager