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 f32a9e9648
commit 4726fd110d
17 changed files with 969 additions and 374 deletions

View File

@@ -32,7 +32,7 @@ from ...core.logger import belief_scope, logger
from ...core.mapping_service import IdMappingService
from ...core.migration.dry_run_orchestrator import MigrationDryRunService
from ...core.async_superset_client import AsyncSupersetClient
from ...dependencies import get_config_manager, get_task_manager, has_permission
from ...dependencies import get_async_job_runner, get_config_manager, get_task_manager, has_permission
from ...models.dashboard import DashboardMetadata, DashboardSelection
from ...models.mapping import ResourceMapping
@@ -377,12 +377,13 @@ async def trigger_sync_now(
db.commit()
service = IdMappingService(db)
runner = get_async_job_runner()
results = {"synced": [], "failed": []}
for env in environments:
try:
client = AsyncSupersetClient(env)
service.sync_environment(env.id, client)
runner.run(service.sync_environment(env.id, client))
results["synced"].append(env.id)
logger.reason(f"Synced environment {env.id}", extra={"src": "trigger_sync_now"})
except Exception as e:

View File

@@ -172,10 +172,10 @@ async def disable_schedule(
logger.reason(f"disable_schedule — Job: {job_id}, User: {current_user.username}", extra={"src": "translate_routes"})
try:
scheduler = TranslationScheduler(db, config_manager, current_user.username)
scheduler.set_schedule_active(job_id, is_active=False)
schedule = scheduler.set_schedule_active(job_id, is_active=False)
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.remove_translation_job(schedule_id=job_id)
sched_svc.remove_translation_job(schedule_id=schedule.id)
return {"status": "disabled", "job_id": job_id}
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
@@ -199,10 +199,12 @@ async def delete_schedule(
logger.reason(f"delete_schedule — Job: {job_id}, User: {current_user.username}", extra={"src": "translate_routes"})
try:
scheduler = TranslationScheduler(db, config_manager, current_user.username)
schedule = scheduler.get_schedule(job_id)
schedule_id = schedule.id
scheduler.delete_schedule(job_id)
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.remove_translation_job(schedule_id=job_id)
sched_svc.remove_translation_job(schedule_id=schedule_id)
return None
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))