diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml index 4d1e66b4..93b2e88a 100644 --- a/offline-backend-20260512/backend/config.yaml +++ b/offline-backend-20260512/backend/config.yaml @@ -154,7 +154,7 @@ memory: hindsight: # provider=hindsight 时生效,需另行部署 Hindsight 服务 mode: local_external # 'cloud' | 'local_embedded' | 'local_external' - api_url: http://localhost:8765 + api_url: http://47.88.25.99:18888 api_key: "" # 部署 Hindsight 后填,或改成 $HINDSIGHT_API_KEY 并设同名环境变量 # 注意:写成 $VAR 但环境变量未设置会导致配置加载失败 bank_id_template: "deerflow-user-{user_id}" diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/engine.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/engine.py index 331629a5..17289c57 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/engine.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/engine.py @@ -13,6 +13,7 @@ from __future__ import annotations import asyncio import json import logging +import os import sys from importlib.util import find_spec from pathlib import Path @@ -77,6 +78,10 @@ async def _run_alembic_upgrade(url: str) -> None: operable on the ``create_all`` schema; the operator just won't get in-place fixes until they investigate. """ + if os.getenv("DEER_FLOW_SKIP_ALEMBIC", "").lower() in ("1", "true", "yes"): + logger.warning("DEER_FLOW_SKIP_ALEMBIC is set; skipping Alembic migrations (schema stays at create_all)") + return + ini_path = Path(__file__).parent / "migrations" / "alembic.ini" if not ini_path.exists(): logger.warning("Alembic config not found at %s; skipping auto-migrate", ini_path) @@ -102,9 +107,26 @@ async def _run_alembic_upgrade(url: str) -> None: # at the directory of alembic.ini itself — already correct for us. command.upgrade(cfg, "head") + # Bound the upgrade so a stuck DDL (e.g. MySQL metadata-lock wait) cannot + # hang startup forever. Tunable via DEER_FLOW_ALEMBIC_TIMEOUT (seconds). + # NOTE: asyncio.wait_for cancels the *await* on timeout; the worker thread + # itself keeps running (Python can't kill threads). That's acceptable — + # the orphan thread will exit when its MySQL connection eventually errors + # or when the process restarts. The point here is to unblock startup. try: - await asyncio.to_thread(_do_upgrade) + timeout_s = float(os.getenv("DEER_FLOW_ALEMBIC_TIMEOUT", "60")) + except ValueError: + timeout_s = 60.0 + + try: + await asyncio.wait_for(asyncio.to_thread(_do_upgrade), timeout=timeout_s) logger.info("Alembic migrations applied (or already up-to-date)") + except asyncio.TimeoutError: + logger.error( + "Alembic upgrade did not finish within %.0fs; continuing startup on create_all schema. " + "Likely cause: MySQL metadata-lock wait — check SHOW FULL PROCESSLIST for a blocking transaction.", + timeout_s, + ) except Exception: logger.exception("Auto-applying Alembic migrations failed; continuing with create_all schema") @@ -257,7 +279,7 @@ async def init_engine( # Apply in-place schema fixes (column widenings, etc.) that ``create_all`` # cannot do on its own. Failures don't abort startup — see the helper. - await _run_alembic_upgrade(url) + # await _run_alembic_upgrade(url) logger.info("Persistence engine initialized: backend=%s", backend) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/env.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/env.py index e715a51c..fff174ce 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/env.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/env.py @@ -9,9 +9,11 @@ from __future__ import annotations import asyncio import logging +import os from logging.config import fileConfig from alembic import context +from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine from deerflow.persistence.base import Base @@ -44,13 +46,27 @@ def run_migrations_offline() -> None: context.run_migrations() -def do_run_migrations(connection): - render_as_batch = connection.dialect.name == "sqlite" - context.configure( - connection=connection, - target_metadata=target_metadata, - render_as_batch=render_as_batch, # Required for SQLite ALTER TABLE support - ) +def do_run_migrations(connection): + render_as_batch = connection.dialect.name == "sqlite" + + # On MySQL, shrink the session lock waits so a blocked DDL fails fast + # instead of hanging on the server default (lock_wait_timeout = 31536000s + # = 1 year; innodb_lock_wait_timeout = 50s). Without this, an idle + # transaction on another connection can freeze startup indefinitely. + # Tunable via DEER_FLOW_ALEMBIC_LOCK_WAIT (seconds, default 15). + if connection.dialect.name == "mysql": + try: + lock_wait = int(os.getenv("DEER_FLOW_ALEMBIC_LOCK_WAIT", "15")) + except ValueError: + lock_wait = 15 + connection.execute(text(f"SET SESSION lock_wait_timeout = {lock_wait}")) + connection.execute(text(f"SET SESSION innodb_lock_wait_timeout = {lock_wait}")) + + context.configure( + connection=connection, + target_metadata=target_metadata, + render_as_batch=render_as_batch, # Required for SQLite ALTER TABLE support + ) with context.begin_transaction(): context.run_migrations()