Skip to content

fix(helpers): record a failed task's error before WaitAsync returns - #9334

Open
stigi wants to merge 1 commit into
apache:mainfrom
stigi:fix/worker-scheduler-lost-error
Open

stigi wants to merge 1 commit into
apache:mainfrom
stigi:fix/worker-scheduler-lost-error

Conversation

@stigi

@stigi stigi commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Summary

WorkerScheduler.WaitAsync can return nil although a task failed. The error is then reported by the next WaitAsync call on the same client, which is usually the next subtask.

A failed task reports its error with a panic, and the ants pool's panic handler records it. But SubmitBlocking calls waitGroup.Done() in a defer inside the task. Go runs that defer while the panic unwinds, before ants recovers and calls the handler. So WaitAsync can wake up, see no errors and return nil. The error is recorded a moment later.

When this happens to a collector, the subtask counts as successful and StatefulApiCollector saves its incremental state. The data that the failed requests should have collected is never collected again.

On our instance the CircleCI token lost access to the project, and every request got 404 Project not found for three weeks:

  • On most nights collectPipelines passed and moved its since marker forward. The 404 for /v2/project/.../pipeline was reported by collectWorkflows, which never calls that URL.
  • On other nights collectPipelines failed correctly.
  • After the token was fixed, the collector continued from the last marker, and two weeks of pipelines and deployments were missing.

This PR recovers the panic inside the task and records it before Done().

Sequence
  1. DoAsync exhausts its retries and returns an error. The SubmitBlocking task calls panic(err).
  2. The task's deferred s.waitGroup.Done() runs, and the counter reaches 0.
  3. WaitAsync wakes up, reads workerErrors (empty) and returns nil.
  4. The ants worker recovers and calls the panic handler, and checkError appends the error.

Does this close any open issues?

No issue filed.

Screenshots

Not applicable (helper-only change).

Tests

helpers/pluginhelper/api/worker_scheduler_test.go: TestWorkerSchedulerWaitAsyncReturnsTaskError submits a failing task 1000 times and checks that WaitAsync returns the error each time. Without the fix it fails within a few hundred runs. With the fix it passes, also at -count=10. go test ./helpers/pluginhelper/api/ ./plugins/circleci/tasks/ ./plugins/github/tasks/ passes.

🤖 Generated with Claude Code

SubmitBlocking marks a task done in a deferred waitGroup.Done(), and a
failed task reports its error by panicking into the ants pool's panic
handler. Go runs the task's own defers while the panic unwinds, so Done()
runs before ants recovers and calls the handler. WaitAsync can wake up,
find no errors yet and return nil; the error is recorded a moment later.

A collector then counts as successful, and StatefulApiCollector saves its
incremental state. The API client is shared by all subtasks of a task, so
the lost error is reported by the next subtask instead. On our instance a
CircleCI token lost access and every request got a 404: on most nights
collectPipelines passed and moved its `since` marker forward, the error
appeared in collectWorkflows, and two weeks of pipelines were never
collected again.

Recover the panic inside the task and record it before Done().

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.

2 participants