Repository navigation
Conversation
ThreadJobExecutor never called telemetry.metrics.job_started()/job_ended(), so lk_agents_active_job_count had no writer when jobs ran under JobExecutorType.THREAD (the Windows default and the console/Jupyter path). The gauge's only writers were ProcJobExecutor.launch_job/_supervise_task, leaving the metric at 0 (and the labeled series absent entirely) for the whole lifetime of thread-executed jobs. Mirror the process executor's accounting: increment when a job is launched, roll back the increment if the StartJobRequest cannot be sent, and decrement once the runner thread exits while a job was assigned. Add a regression test that spies on job_started()/job_ended() and asserts the thread executor calls them around a job lifecycle.
The fake job's entrypoint could complete between launch_job() and the 'started' assertion, letting _main_task record 'ended' first. CI hit that window; keep the entrypoint parked on a threading.Event until the test has observed the running state, then release it and assert the final sequence.
The runner can exit while launch_job() is still sending the StartJobRequest: _main_task then observes the join and decrements the gauge, and the pending send fails afterwards, so the error path decremented the same job again and left the gauge negative. Track the outstanding increment on the executor and settle it through a single idempotent helper used by both the runner-exit path and the failed-dispatch path, so whichever sees it first owns the only decrement. Cancellation of launch_job() is handled explicitly: it can only land at the send's single suspension point (drain()), after the whole request has been written to the channel, so the dispatch is kept assigned and settled when the runner exits instead of being rolled back as unstarted. Add regression tests for the runner-exit-during-dispatch double decrement and for a cancelled dispatch.
|
Follow-up for the two Devin findings on Failed-dispatch double decrement — fixed. The runner can exit while Cancelled dispatch — rebutted, and made explicit. A cancelled Verification: |
fix(ipc): update the active-job gauge in the thread job executor
What breaks
lk_agents_active_job_countnever moves for the thread job executor: the gauge reports0while a job is running, and stays0after it ends.ThreadJobExecutornever callstelemetry.metrics.job_started()/job_ended(), unlikeProcJobExecutorwhich calls them inlaunch_joband its supervise task.The thread executor is the default on Windows (
worker.pyfalls back toJobExecutorType.THREAD) and is forced by the devmode CLI, so on those deployments the active-job metric is permanently zero — dashboards and autoscaling rules that read it see an idle worker while jobs are running.Root cause
launch_job()(livekit-agents/livekit/agents/ipc/job_thread_executor.py) starts the runner and sends theStartJobRequestwithout touching the metrics module, and_main_task()completes after_join_futwithout decrementing. The gauge is therefore only ever updated by the process executor path.Fix
Mirror
ProcJobExecutorexactly:launch_job(): callmetrics.job_started()once the initialized check passes; if sending theStartJobRequestfails, reset_running_job/_job_statusand callmetrics.job_ended()before re-raising (no leaked increment)._main_task(): afterawait self._join_fut, if a job was running, callmetrics.job_ended().Tests
New test
tests/test_ipc.py::test_thread_executor_updates_active_job_metricspies ontelemetry.metrics.job_started/job_ended, runs a realThreadJobExecutorthrough_create_thread_proc, and asserts the gauge is incremented while the entrypoint is live and decremented afteraclose().AssertionError: job_started must be called while a job is running(both metric functions never called; the job provably runs).tests/test_ipc.py+tests/test_ipc_preload.pygreen (31 passed); ruff clean.The test is marked
no_concurrent(it monkeypatches process-global metric functions) so parallel thread-executor tests cannot pollute the spy. The change does not overlap the in-flight #7899 hunks in the same file (initialize()/aclose()/_monitor_task()); both patches apply independently.