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:
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user