Summary
The Helix Job Monitor (src/Microsoft.DotNet.Helix/JobMonitor) can never complete when a Helix job it is monitoring contains work items that are permanently stranded in the Waiting state (created but never dispatched to a machine). Because the monitor's job discovery is scoped by build + stage (not by pipeline attempt), retrying the monitor job re-discovers the same stranded work and re-drives the full timeout every time.
This is not a code bug — the monitor is faithfully implementing the spec — but the design decision makes it impossible for the monitor to ever reach a terminal (completed) state in this scenario. Filing so we can decide how the monitor should handle abandoned/never-scheduled work items.
Observed scenario
Build: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1513096 (dotnet/runtime PR #129690)
Job: Monitor Helix Jobs (record b0da1d21-03df-59a7-d49e-ac6d68c91909)
Every monitor attempt logs the same stable status and then times out at the 535‑minute (8h55m) budget:
ℹ️ Status: 80 processed / 80 completed / 3 running / 0 waiting jobs
6810 processed / 6810 completed / 83 running / 6 waiting work items
...
##[error]Helix Job Monitor timed out after 535 minute(s) (08:55:00). 4 Helix job(s) were unfinished or unprocessed:
The blocking Helix job is 62175fbc-d087-4e2b-b648-a2605eba4334 (windows-x64 Checked NativeAOT, queue windows.10.amd64.open.rt), created 2026-07-16T19:52 UTC. Live Helix state:
totalWorkItems: 9, passed: 5, inProgress: 4
- The 4 in-progress items are all
state: "Waiting", exitCode: -1, machineName: null — never scheduled, days after creation:
Loader
LoaderClassloaderGenerics
LoaderClassloaderRegressions
async
Stranded work-item API URIs:
https://helix.dot.net/api/2019-06-17/jobs/62175fbc-d087-4e2b-b648-a2605eba4334/workitems/Loader
https://helix.dot.net/api/2019-06-17/jobs/62175fbc-d087-4e2b-b648-a2605eba4334/workitems/LoaderClassloaderGenerics
https://helix.dot.net/api/2019-06-17/jobs/62175fbc-d087-4e2b-b648-a2605eba4334/workitems/LoaderClassloaderRegressions
https://helix.dot.net/api/2019-06-17/jobs/62175fbc-d087-4e2b-b648-a2605eba4334/workitems/async
Root cause
A Helix job is only counted complete when it is finished/failed, or (fallback) when every expected work item has a terminal exit code (JobMonitorRunner.AreAllWorkItemsTerminalAsync):
workItems.Count >= job.InitialWorkItemCount.Value
&& workItems.All(wi => wi.ExitCode.HasValue);
The stranded Waiting items never get an ExitCode, so the job never completes, and termination (all monitored AzDO jobs complete and all scoped Helix jobs complete — Design.md §5.4/§5.5) is never reached.
Discovery is scoped by build + stage, not by pipeline attempt (Design.md §2.1; HelixService.GetJobsForBuildAsync filters on the Helix source string plus the BuildId property, then System.StageName). An AzDO "retry" is a new attempt of the same BuildId, so each retry re-discovers the identical stranded job and re-runs the full 8h55m timeout. This cross-attempt scoping is intentional (crash/timeout resilience — re-attach to prior attempts' jobs to upload results and resubmit failures, Design.md §2.2/§2.6), so per-attempt scoping is not a free fix.
These items are also not eligible for the monitor's own resubmission path: retry only resubmits failed items of a completed latest incarnation; a still-Waiting incarnation is skipped (Design.md §2.3 rules 3 & 6).
Note: the same job reports InitialWorkItemCount=6 but totalWorkItems=9, so the initial-count heuristic is also stale here — though the All(ExitCode.HasValue) half is the actual blocker.
The gap
The monitor has no notion of a permanently stranded / abandoned work item. It cannot distinguish "legitimately still Waiting (queued, will run soon)" from "abandoned in the queue, will never run." As a result the only exit for this state is the global timeout, and every retry repeats it.
Possible directions (for discussion)
- Stranded-
Waiting detection / fail-fast (keeps build+stage scope, preserves resilience): treat a work item that has been Waiting with no machine assignment for longer than some threshold (absolute age, or age since monitor start) as abandoned. Surface it as a hard failure with actionable output instead of silently waiting out the full timeout, so retries don't each burn ~9 hours.
- Per-attempt scoping for discovery: only monitor jobs submitted by the current stage attempt. Simpler mental model, but trades away the §2.2/§2.6 crash-resilience / upload-recovery guarantees.
- Escalate to Helix service side: investigate why work items get stranded in
Waiting on windows.10.amd64.open.rt and whether Helix should itself terminate/abandon never-scheduled work items after some SLA.
cc @dotnet/dnceng
Summary
The Helix Job Monitor (
src/Microsoft.DotNet.Helix/JobMonitor) can never complete when a Helix job it is monitoring contains work items that are permanently stranded in theWaitingstate (created but never dispatched to a machine). Because the monitor's job discovery is scoped by build + stage (not by pipeline attempt), retrying the monitor job re-discovers the same stranded work and re-drives the full timeout every time.This is not a code bug — the monitor is faithfully implementing the spec — but the design decision makes it impossible for the monitor to ever reach a terminal (completed) state in this scenario. Filing so we can decide how the monitor should handle abandoned/never-scheduled work items.
Observed scenario
Build: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1513096 (dotnet/runtime PR #129690)
Job:
Monitor Helix Jobs(recordb0da1d21-03df-59a7-d49e-ac6d68c91909)Every monitor attempt logs the same stable status and then times out at the 535‑minute (8h55m) budget:
The blocking Helix job is
62175fbc-d087-4e2b-b648-a2605eba4334(windows-x64 Checked NativeAOT, queuewindows.10.amd64.open.rt), created 2026-07-16T19:52 UTC. Live Helix state:totalWorkItems: 9,passed: 5,inProgress: 4state: "Waiting",exitCode: -1,machineName: null— never scheduled, days after creation:LoaderLoaderClassloaderGenericsLoaderClassloaderRegressionsasyncStranded work-item API URIs:
Root cause
A Helix job is only counted complete when it is
finished/failed, or (fallback) when every expected work item has a terminal exit code (JobMonitorRunner.AreAllWorkItemsTerminalAsync):The stranded
Waitingitems never get anExitCode, so the job never completes, and termination (all monitored AzDO jobs complete and all scoped Helix jobs complete —Design.md§5.4/§5.5) is never reached.Discovery is scoped by build + stage, not by pipeline attempt (
Design.md§2.1;HelixService.GetJobsForBuildAsyncfilters on the Helixsourcestring plus theBuildIdproperty, thenSystem.StageName). An AzDO "retry" is a new attempt of the sameBuildId, so each retry re-discovers the identical stranded job and re-runs the full 8h55m timeout. This cross-attempt scoping is intentional (crash/timeout resilience — re-attach to prior attempts' jobs to upload results and resubmit failures,Design.md§2.2/§2.6), so per-attempt scoping is not a free fix.These items are also not eligible for the monitor's own resubmission path: retry only resubmits failed items of a completed latest incarnation; a still-
Waitingincarnation is skipped (Design.md§2.3 rules 3 & 6).Note: the same job reports
InitialWorkItemCount=6buttotalWorkItems=9, so the initial-count heuristic is also stale here — though theAll(ExitCode.HasValue)half is the actual blocker.The gap
The monitor has no notion of a permanently stranded / abandoned work item. It cannot distinguish "legitimately still
Waiting(queued, will run soon)" from "abandoned in the queue, will never run." As a result the only exit for this state is the global timeout, and every retry repeats it.Possible directions (for discussion)
Waitingdetection / fail-fast (keeps build+stage scope, preserves resilience): treat a work item that has beenWaitingwith no machine assignment for longer than some threshold (absolute age, or age since monitor start) as abandoned. Surface it as a hard failure with actionable output instead of silently waiting out the full timeout, so retries don't each burn ~9 hours.Waitingonwindows.10.amd64.open.rtand whether Helix should itself terminate/abandon never-scheduled work items after some SLA.cc @dotnet/dnceng