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:
@@ -360,89 +360,6 @@ async def test_sync_environment_deletes_stale_mappings(db_session):
|
||||
# #endregion test_sync_environment_deletes_stale_mappings
|
||||
|
||||
|
||||
# #region test_start_scheduler_basic [C:2] [TYPE Function]
|
||||
# @RELATION BINDS_TO ->[TestMappingService]
|
||||
@patch("src.core.mapping_service.seed_trace_id")
|
||||
@patch("src.core.mapping_service.BackgroundScheduler")
|
||||
def test_start_scheduler_basic(mock_scheduler_cls, mock_seed, db_session):
|
||||
"""start_scheduler adds job and starts scheduler."""
|
||||
mock_scheduler = MagicMock()
|
||||
mock_scheduler_cls.return_value = mock_scheduler
|
||||
mock_scheduler.running = False
|
||||
|
||||
service = IdMappingService(db_session)
|
||||
mock_factory = MagicMock(return_value=MagicMock())
|
||||
service.start_scheduler("0 */5 * * *", ["env1", "env2"], mock_factory)
|
||||
|
||||
mock_scheduler.add_job.assert_called_once()
|
||||
mock_scheduler.start.assert_called_once()
|
||||
|
||||
|
||||
# #endregion test_start_scheduler_basic
|
||||
|
||||
|
||||
# #region test_start_scheduler_sync_all_called [C:2] [TYPE Function]
|
||||
# @RELATION BINDS_TO ->[TestMappingService]
|
||||
def test_start_scheduler_sync_all_called(db_session):
|
||||
"""sync_all inner function executed via scheduler.add_job (lines 75-80)."""
|
||||
mock_scheduler = MagicMock()
|
||||
mock_scheduler.running = False
|
||||
|
||||
captured_fn = [None]
|
||||
|
||||
def _capture_job(fn, *args, **kwargs):
|
||||
captured_fn[0] = fn
|
||||
return MagicMock(id="sync-job")
|
||||
|
||||
mock_scheduler.add_job.side_effect = _capture_job
|
||||
|
||||
mock_factory = MagicMock(return_value=MagicMock())
|
||||
|
||||
with patch("src.core.mapping_service.seed_trace_id") as mock_seed, \
|
||||
patch("src.core.mapping_service.BackgroundScheduler", return_value=mock_scheduler):
|
||||
|
||||
service = IdMappingService(db_session)
|
||||
with patch.object(service, 'sync_environment', return_value=None) as mock_sync:
|
||||
service.start_scheduler("0 */5 * * *", ["env1", "env2"], mock_factory)
|
||||
# Call the captured sync_all
|
||||
captured_fn[0]()
|
||||
|
||||
mock_seed.assert_called_once()
|
||||
assert mock_factory.call_count == 2
|
||||
mock_factory.assert_any_call("env1")
|
||||
mock_factory.assert_any_call("env2")
|
||||
assert mock_sync.call_count == 2
|
||||
# #endregion test_start_scheduler_sync_all_called
|
||||
|
||||
|
||||
# #region test_start_scheduler_update_existing [C:2] [TYPE Function]
|
||||
# @RELATION BINDS_TO ->[TestMappingService]
|
||||
@patch("src.core.mapping_service.seed_trace_id")
|
||||
@patch("src.core.mapping_service.BackgroundScheduler")
|
||||
def test_start_scheduler_update_existing(mock_scheduler_cls, mock_seed, db_session):
|
||||
"""start_scheduler with existing job → updates, doesn't re-start scheduler."""
|
||||
mock_scheduler = MagicMock()
|
||||
mock_scheduler_cls.return_value = mock_scheduler
|
||||
mock_scheduler.running = False # Not running initially
|
||||
|
||||
service = IdMappingService(db_session)
|
||||
mock_factory = MagicMock(return_value=MagicMock())
|
||||
service.start_scheduler("0 */5 * * *", ["env1"], mock_factory)
|
||||
|
||||
# First call should start the scheduler (was not running)
|
||||
mock_scheduler.start.assert_called_once()
|
||||
|
||||
# Second call with existing job — scheduler already running
|
||||
mock_scheduler.running = True
|
||||
service.start_scheduler("0 */10 * * *", ["env1"], mock_factory)
|
||||
|
||||
mock_scheduler.remove_job.assert_called_once() # called because _sync_job exists
|
||||
assert mock_scheduler.add_job.call_count == 2
|
||||
|
||||
|
||||
# #endregion test_start_scheduler_update_existing
|
||||
|
||||
|
||||
# #region test_get_remote_id_value_error [C:2] [TYPE Function]
|
||||
# @RELATION BINDS_TO ->[TestMappingService]
|
||||
def test_get_remote_id_value_error(db_session):
|
||||
|
||||
Reference in New Issue
Block a user