Skip to content

backend-worker: treat codes.NotFound on job completion as terminal, not infinitely retried - #7893

Open
DeviousCardi wants to merge 3 commits into
grafana:mainfrom
DeviousCardi:fix/7879-backendworker-notfound-completion-retry
Open

DeviousCardi wants to merge 3 commits into
grafana:mainfrom
DeviousCardi:fix/7879-backendworker-notfound-completion-retry

Conversation

@DeviousCardi

Copy link
Copy Markdown

Summary

Fixes #7879.

When a backend-worker finished a job and called a completion RPC (UpdateJob, completeRedactionJob, failJob) and the backend-scheduler returned codes.NotFound (e.g. after a scheduler restart lost the in-flight assignment), the worker retried the same job forever inside callSchedulerWithBackoffBackoff.MaxRetries defaults to 0, which dskit treats as infinite retries. The worker never returned to Next(), permanently starving itself of new work.

Next()'s own NotFound handling turned out to be broken too: it checked for codes.NotFound in its callback, but the actual infinite-retry loop lived inside callSchedulerWithBackoff itself, so the check never had a chance to stop anything — every idle "no jobs queued" poll cycle was silently retrying via the same unbounded loop.

Changes

  • callSchedulerWithBackoff now short-circuits on codes.NotFound via a small isNotFound/wrapSchedulerErr helper pair (the latter avoids fmt.Errorf's %w wrapping, which would otherwise hide the status code from status.FromError's plain type assertion).
  • processJobs's Next() call site now treats a NotFound result as the expected "no jobs queued" case: it returns quietly and resumes polling, instead of the previous behavior of never terminating the retry loop or producing spurious level.Error logs.
  • All 4 completion call sites (processCompactionJob's and processRetentionJob's UpdateJob, completeRedactionJob, failJob) now treat NotFound as terminal: warn-log, drop the job, and resume polling instead of retrying forever. completeRedactionJob was also missing the failJob escalation that processCompactionJob/processRetentionJob already had for genuine (non-NotFound) errors — fixed to match.
  • Extracted the duplicated warn-and-drop/escalate logic into a handleCompletionErr helper shared by all three completion functions.
  • Added a .chloggen entry per this repo's changelog convention.

Test plan

  • go build ./modules/backendworker/...
  • go vet ./modules/backendworker/...
  • go test ./modules/backendworker/... — including new tests: TestCompletionNotFoundIsTerminal, TestNextNotFoundIsQuietIdlePolling (asserts idle polling produces no Error-level log and doesn't touch the retry-count metric), TestCompleteRedactionJobFailsOnGenuineError, TestIsNotFound.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Signed commits report

All 3 commits between main and fix/7879-backendworker-notfound-completion-retry have verified signatures. ✅

@cla-assistant

cla-assistant Bot commented Sep 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

DeviousCardi and others added 3 commits September 15, 2026 14:31
The Next() polling path already special-cased codes.NotFound from the
scheduler, but the four completion call sites (UpdateJob in
processCompactionJob and processRetentionJob, completeRedactionJob, and
failJob) did not. All four route through callSchedulerWithBackoff, whose
retry loop is effectively infinite because Backoff.MaxRetries defaults to
0. A NotFound response on completion (e.g. after a scheduler restart loses
the in-flight assignment) therefore retried forever and the worker never
called Next() again, starving itself of new work.

Factor the NotFound check into a small isNotFound helper, apply it inside
callSchedulerWithBackoff so a NotFound short-circuits the retry loop
instead of looping until MaxRetries, and have each completion call site
log a warning and drop the job (returning nil / the original failure
reason) instead of escalating to another doomed completion call.

Fixes grafana#7879

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dedupe NotFound wrapping

completeRedactionJob was the only one of the three job-completion functions
that did not call failJob on a genuine (non-NotFound) UpdateJob error. It
just returned the wrapped error, leaving the redaction job stuck as
in-progress/leased to this worker forever instead of being marked failed
and made available for retry/reassignment. Bring it in line with
processCompactionJob and processRetentionJob.

Also extract the repeated "if isNotFound(err) { return err }; return
fmt.Errorf(...)" branch (needed because gogo/status's FromError uses a
plain type assertion rather than errors.As, so %w-wrapping a NotFound
error would hide it from later detection) into a wrapSchedulerErr helper,
used at all 5 callSchedulerWithBackoff call sites instead of duplicating
it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The prior two commits made callSchedulerWithBackoff short-circuit on any
codes.NotFound scheduler error, intending to make job-completion NotFound
(UpdateJob/failJob/completeRedactionJob) terminal instead of retried
forever. But callSchedulerWithBackoff also wraps the Next() polling call in
processJobs, where NotFound just means "no jobs queued right now" after a
routine long-poll timeout -- not a failure. That made every idle poll cycle
propagate up through running() as a level.Error "error processing jobs" log
and skip metricWorkerCallRetries, misrepresenting normal operation as a
failure.

processJobs now recognizes isNotFound on the Next() call specifically and
returns quietly (nil) so running() resets its backoff and polls again,
leaving the terminal short-circuit behavior for the four completion call
sites untouched.

Also extract the near-identical NotFound-drop-and-log block duplicated
across processCompactionJob, processRetentionJob, and completeRedactionJob
into a shared handleCompletionErr helper, keeping each call site's log
message distinguishable by job kind.

Adds TestNextNotFoundIsQuietIdlePolling, which asserts an idle Next()
NotFound does not produce an Error-level log and does not touch
metricWorkerCallRetries, distinguishing it from TestCompletionNotFoundIsTerminal's
genuinely terminal completion case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@DeviousCardi
DeviousCardi force-pushed the fix/7879-backendworker-notfound-completion-retry branch from 2068aca to 1cf3a48 Compare September 15, 2026 09:01
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.

backend-worker retries UpdateJob forever on codes.NotFound (starves itself of new work)

1 participant