diff --git a/app/__main__.py b/app/__main__.py index af746e4..c036a54 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -25,7 +25,32 @@ if int(os.getenv("SANIC_MAINTENANCE", '0')) == 1: while True: time.sleep(1) -from app.core.models import Memory + +def init_db_schema_sync() -> None: + """Initialise all SQLAlchemy models in the database before services start. + + This ensures that every table defined on AlchemyBase.metadata (including + newer ones like DHT and service_config) exists before any component + accesses the database. + """ + try: + from sqlalchemy import create_engine + from app.core.models import AlchemyBase # imports all models and populates metadata + + db_url = os.environ.get('DATABASE_URL') + if not db_url: + raise RuntimeError('DATABASE_URL is not set') + + # Normalise DSN to sync driver for schema creation + if '+asyncpg' in db_url: + db_url_sync = db_url.replace('+asyncpg', '+psycopg2') + else: + db_url_sync = db_url + + sync_engine = create_engine(db_url_sync, pool_pre_ping=True) + AlchemyBase.metadata.create_all(sync_engine) + except Exception as e: + make_log('Startup', f'DB sync init failed: {e}', level='error') async def queue_daemon(app): @@ -78,27 +103,15 @@ async def execute_queue(app): if __name__ == '__main__': + # Ensure DB schema is fully initialised for all models + init_db_schema_sync() + + from app.core.models import Memory main_memory = Memory() if startup_target == '__main__': # Defer heavy imports to avoid side effects in background services # Mark this process as the primary node for seeding/config init os.environ.setdefault('NODE_ROLE', 'primary') - # Create DB tables synchronously before importing HTTP app to satisfy _secrets - try: - from sqlalchemy import create_engine - from app.core.models import AlchemyBase # imports all models - db_url = os.environ.get('DATABASE_URL') - if not db_url: - raise RuntimeError('DATABASE_URL is not set') - # Normalize to sync driver - if '+asyncpg' in db_url: - db_url_sync = db_url.replace('+asyncpg', '+psycopg2') - else: - db_url_sync = db_url - sync_engine = create_engine(db_url_sync, pool_pre_ping=True) - AlchemyBase.metadata.create_all(sync_engine) - except Exception as e: - make_log('Startup', f'DB sync init failed: {e}', level='error') from app.api import app # Delay aiogram dispatcher creation until loop is running from app.core._config import SANIC_PORT, PROJECT_HOST, DATABASE_URL diff --git a/app/core/_secrets.py b/app/core/_secrets.py index be4ff79..282d715 100644 --- a/app/core/_secrets.py +++ b/app/core/_secrets.py @@ -32,22 +32,6 @@ def _init_seed_via_db() -> bytes: engine = create_engine(DATABASE_URL, pool_pre_ping=True) role = os.getenv("NODE_ROLE", "worker").lower() - # Best-effort: ensure service_config table exists before waiting on it. - # This complements the synchronous init in app.__main__ and protects - # against ordering issues where _secrets is imported before that init runs. - try: - from sqlalchemy.exc import SQLAlchemyError - - with engine.begin() as conn: - inspector = inspect(conn) - if not inspector.has_table('service_config'): - ServiceConfigValue.__table__.create(bind=conn, checkfirst=True) - except SQLAlchemyError as exc: - make_log("HotWallet", f"Failed to ensure service_config table: {exc}", level="error") - except Exception: - # Avoid failing hard here; the fallback waiter below may still succeed - pass - def db_ready(conn) -> bool: try: inspector = inspect(conn) diff --git a/app/core/network/dht/store.py b/app/core/network/dht/store.py index 9dc4991..bbca705 100644 --- a/app/core/network/dht/store.py +++ b/app/core/network/dht/store.py @@ -61,6 +61,7 @@ class DHTStore: try: from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker + from app.core._config import DATABASE_URL from app.core.models.dht import DHTRecordRow