fix(core): centralize async/sync bridge for APScheduler scheduled jobs

Create AsyncJobRunner — centralized bridge between APScheduler
(BackgroundScheduler, sync thread pool) and async coroutines.

Fixes:
- P0: execute_run() called without await from APScheduler thread,
  causing coroutine to be silently discarded (root cause: no
  translation history)
- P0: get_async_job_runner() deadlock when called from APScheduler
  thread pool without running event loop
- P1: ID mismatch in disable_schedule/delete_schedule routes
  (job_id passed instead of schedule_id)
- P1: asyncio.run() in APScheduler callbacks incompatible with
  running event loop
- Delete unused llm_analysis/scheduler.py (not used in production)

Changes:
  core/async_job_runner.py          — new: AsyncJobRunner class
  core/scheduler.py                 — use runner.run()/run_later()
  translate/scheduler.py            — use runner.run() for execute_run
  mapping_service.py                — remove unused BackgroundScheduler
  dependencies.py                   — add get_async_job_runner() DI
  app.py                            — init runner in lifespan
  api/routes/migration.py           — use runner.run()
  _schedule_routes.py               — fix ID mismatch
  plugins/migration.py              — use runner.run()
  llm_analysis/scheduler.py         — delete (unused)
  tests: 151 new/updated tests, all passing
This commit is contained in:
2026-06-17 16:22:30 +03:00
parent 85ef486d23
commit 880bdcf9c8
17 changed files with 969 additions and 374 deletions

View File

@@ -61,7 +61,7 @@ from .core.database import AuthSessionLocal, init_db
from .core.encryption_key import ensure_encryption_key
from .core.logger import belief_scope, logger
from .core.utils.network import NetworkError
from .dependencies import get_scheduler_service, get_task_manager
from .dependencies import get_async_job_runner, get_scheduler_service, get_task_manager
from .models.auth import Role, User
@@ -108,6 +108,8 @@ async def lifespan(app: FastAPI):
_s.close()
except Exception as _e:
logger.warning(f"Failed to clean up stuck validation runs: {_e}")
logger.info("⏰ Initializing AsyncJobRunner...")
get_async_job_runner() # Initialize singleton with running event loop BEFORE scheduler starts
logger.info("⏰ Starting scheduler...")
scheduler = get_scheduler_service()
scheduler.start()