Skip to content

CxODEV-2161: keep the engine polling after Conductor errors (4.4.2) - #233

Draft
milansvitlica-codaxy wants to merge 4 commits into
masterfrom
CxODEV-2161-engine-keeps-polling
Draft

milansvitlica-codaxy wants to merge 4 commits into
masterfrom
CxODEV-2161-engine-keeps-polling

Conversation

@milansvitlica-codaxy

@milansvitlica-codaxy milansvitlica-codaxy commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes CxODEV-2161 and CxODEV-2000 (YouTrack). A ConductorSharp engine no longer stops polling for good after a Conductor error.

Root cause: WorkflowEngineBackgroundService.RunAsync ran deploy + execution manager once. Any exception ended polling while the host stayed up. An HTTP timeout (TaskCanceledException) was also taken for a shutdown: the engine logged INF Stopping ConductorSharp background service and stayed Healthy.

Fix: RunAsync is a supervisor loop. It deploys and runs the execution manager. On any error that is not our own shutdown, it marks the engine not running, logs an Error, backs off (1 s doubling to 30 s, ≤ 500 ms jitter) and starts again. TaskQueuePollingService marks the engine running after a successful queue read, so /health is Healthy only while the engine actually reads its queues. Only a run that polled restarts the backoff. A shutdown cancels a hung deploy. TaskQueuePollingService's bounded retry (#215) stays as the inner, fast layer.

Failure Before After
Conductor hangs (HTTP timeout) stops after ~8.5 min, INF log, Healthy forever Unhealthy while backing off, resumes when Conductor answers
Refused / 5xx while running stops after ~16 s, FTL, Unhealthy forever Unhealthy while backing off, resumes
Deploy error at start (task-def registration race 500/409, Conductor not up) stops at once retried; the retry takes the update path
Non-transient error (401, 404, bad JSON) stops at once retried every ≤ 30 s, Error log, Unhealthy

Tests

  • Unit/WorkflowEngineBackgroundServiceTests (11): retry of deploy errors, restart after a connection error, timeout ≠ shutdown, health while backing off, capped backoff, backoff reset only after a successful poll, deploy alone does not mark the engine running, stop during backoff, stop while a deploy hangs, stop while running.
  • Unit/TaskQueuePollingServiceTests (+2): a successful read marks the engine running; failed reads do not.
  • Integration/EngineRecoveryTests (5): a real engine (DI, hosted service, beta execution manager, HTTP client) against a fake Conductor. Covers a 503 outage, a hang, the registration race, a task in flight during an outage, and task queues failing while metadata works (stays Unhealthy). All fail on the old loop.
  • Suite: 91/91 locally (net6.0 via DOTNET_ROLL_FORWARD=Major, LF checkout).

Reproduced before the fix (2026-10-19), on a local cxo-composer stack with SOE/ROE/locking on 3.10.0: after docker pause on Conductor, all three engines stopped 8 min 36 s later and stayed healthy. After docker stop + start, they logged FTL, turned 503 and never reconnected. Only a restart brought them back.

Behaviour change to note: the old FTL Workflow Engine Background Service encountered an (API) error(s) is gone. Each failed attempt now logs at Error: ConductorSharp engine stopped polling (attempt N); retrying in …. Alerts that matched the old FTL text need updating.

Version: 4.4.2 (all five packages). The v3 backport (3.10.1) follows in a separate PR; CxO engines pin 3.10.x.

🤖 Generated with Claude Code

milansvitlica-codaxy and others added 3 commits October 19, 2026 13:37
…t is not a shutdown (CxODEV-2000)

WorkflowEngineBackgroundService ran the deployment and the execution manager
once. Any error, or an HTTP timeout mistaken for a shutdown, ended polling for
good while the host stayed up (and, for a timeout, stayed Healthy).

RunAsync is now a supervisor loop: deploy, mark running, run the execution
manager; on any error that is not our own shutdown, mark not running, log an
error, back off (1 s doubling to 30 s, 500 ms jitter) and start again. The
backoff resets after a successful deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A real engine (DI wiring, hosted service, beta execution manager, HTTP
client) against a fake Conductor that answers 503, hangs (request timeout)
or loses the task-def registration race, then recovers. Each test checks
that the engine backs off, reports not running, and completes a task queued
after recovery; one also checks that a task in flight during the outage
still completes. All four fail on the old loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shutdown cancel a hung deploy

Review findings on the supervisor loop:

- Health and the backoff reset followed a successful deploy, not a successful
  poll. With Conductor's metadata up and its task queues down (e.g. the queue
  backend), the engine reported Healthy most of the time while polling nothing,
  and redeployed about once a second on a 4xx. TaskQueuePollingService now marks
  the engine running after a successful read; the background service no longer
  marks it after deploy and restarts the backoff only after a run that polled.
  Unhealthy is still set only by the background service.
- Deploy takes no token, so a hung deploy blocked StopAsync. It now runs under
  WaitAsync(stoppingToken).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant