Skip to content

fix(stovepipe): persist the terminal outcome reason on Request - #965

Open
sbalabanov wants to merge 1 commit into
mainfrom
sergeyb/sp-oct-001-terminal-reason
Open

sbalabanov wants to merge 1 commit into
mainfrom
sergeyb/sp-oct-001-terminal-reason

Conversation

@sbalabanov

@sbalabanov sbalabanov commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Why?

Request persisted its terminal state but not the reason, so each writer of a state/<version> log reconstructed the reason itself: buildsignal mapped failed to build_failed, while the DLQ reconcilers used their own stage reason. The materializer correctly rejects the second, disagreeing writer, and DLQ consumers retry without an attempt cap; in the opposite order a late poll mislabels a forced infrastructure failure as build_failed. Repro:

  1. buildsignal marks request version 3 failed and retains state/3 with build_failed.
  2. Its record publish keeps failing, so the buildsignal message dead-letters.
  3. The buildsignal DLQ finds the request already failed and re-logs state/3 with build_polling_exhausted.
  4. PersistLog rejects the conflict, and the DLQ retries it forever.

What?

entity.Request gains OutcomeReason, stored in the MySQL request.outcome_reason column. Every writer sets it in the same compare-and-swap that writes the terminal state: buildsignal markOutcome (build-derived reason), the DLQ failRequest (its stage reason), and process supersedeRequest. requestlog.NewRequestStateLog now records the request's stored reason; the caller-supplied reason is only a fallback for nonterminal requests and for rows made terminal before this change. So buildsignal and the DLQ log the same reason for one state version and no longer conflict. The materializer's conflict check is unchanged. Existing deployments must add the column before deploying (see the storage README).

Test Plan

make fmt && make lint && make check-tidy && make check-gazelle && make check-mocks, ./tool/bazel test //stovepipe/... //service/stovepipe/... --test_output=errors, and ./tool/bazel test //test/integration/stovepipe/extension/storage/mysql:go_default_test (Docker).

Audit: SP-OCT-001 in doc/audit/stovepipe-fable-v2.md (Sol v2 #4, Opus v2 #2).

🤖 Generated with Claude Code

Request stored its terminal state but not the reason, so buildsignal
relabeled every failed request build_failed while the DLQ reconcilers
logged their own stage reason. Two writers of one state/<version> log
disagreed, the materializer rejected the second, and the DLQ retried
that rejection forever.

Store OutcomeReason on Request in the same compare-and-swap as the
terminal state (buildsignal, the DLQ reconcilers, process supersede),
and build every state log from the stored reason. Only rows made
terminal before this change fall back to the writing stage's reason.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sbalabanov
sbalabanov marked this pull request as ready for review October 27, 2026 23:41
@sbalabanov
sbalabanov requested review from a team and behinddwalls as code owners October 27, 2026 23:41

This branch has not been deployed

No deployments
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