Skip to content

feat(stovepipe): add DLQ reconciliation for the process stage#347

Open
gsturges wants to merge 2 commits into
mainfrom
gsturges/stovepipe-process-dlq
Open

feat(stovepipe): add DLQ reconciliation for the process stage#347
gsturges wants to merge 2 commits into
mainfrom
gsturges/stovepipe-process-dlq

Conversation

@gsturges

Copy link
Copy Markdown
Contributor

Summary

A request whose process-stage message exhausts retries would previously sit in a non-terminal state forever — indistinguishable from "not yet validated" to callers gating deployments on greenness. Per the fail-closed policy in doc/rfc/stovepipe/workflow.md, such requests must be driven to a conservative not-green terminal state instead.

  • Entity: add RequestStateFailed terminal state and IsRequestStateTerminal helper.
  • New stovepipe/controller/dlq package: a consumer.Controller that drains the process_dlq topic and CAS-transitions the affected request to failed. If the request had already been admitted (processing), the queue's in_flight_count slot is released first, per the integrity rule in doc/rfc/stovepipe/steps/process.md.
  • Server wiring: second consumer using errs.AlwaysRetryableProcessor (a reconciliation failure retries in place — the DLQ subscription is a final destination with DLQ.Enabled = false, so nothing can cascade to a _dlq_dlq); process_dlq registered in the topic registry with raised retry attempts; DLQ consumer starts first and stops last.

Reconciliation is idempotent: already-terminal requests are skipped, and optimistic locking lets a late legitimate pipeline transition win. The design mirrors SubmitQueue's proven submitqueue/orchestrator/controller/dlq pattern, simplified for stovepipe's single payload shape.

Only process is a queue-consuming step today (ingest is synchronous RPC); future stages (build, buildsignal, record) can reuse the same pattern.

Test plan

  • New table-driven unit tests in stovepipe/controller/dlq/dlq_test.go: accepted→failed, processing→failed with slot release, already-terminal no-ops, not-found no-op, CAS version-mismatch paths, malformed/empty payloads
  • go build ./... and go test ./stovepipe/... ./service/stovepipe/... pass
  • make gazelle, make fmt, make lint-license clean

🤖 Generated with Claude Code

@gsturges gsturges marked this pull request as ready for review July 13, 2026 23:02
@gsturges gsturges requested review from a team, behinddwalls and sbalabanov as code owners July 13, 2026 23:02
Comment thread stovepipe/controller/dlq/request.go
Comment thread service/stovepipe/server/main.go Outdated
Comment thread stovepipe/controller/dlq/dlq.go Outdated
gsturges and others added 2 commits July 14, 2026 16:10
A request whose process-stage message exhausts retries would previously
sit in a non-terminal state forever, indistinguishable from "not yet
validated" to callers gating on greenness. Per the fail-closed policy in
doc/rfc/stovepipe/workflow.md, such requests must be driven to a
conservative not-green terminal state instead.

- Add RequestStateFailed terminal state and IsRequestStateTerminal helper
  to the entity package.
- Add stovepipe/controller/dlq: a consumer.Controller that drains the
  process_dlq topic and CAS-transitions the affected request to failed,
  releasing the queue's in_flight_count slot first when the request had
  already been admitted (processing), per the in_flight_count integrity
  rule in doc/rfc/stovepipe/steps/process.md.
- Wire a second consumer in the stovepipe server using
  errs.AlwaysRetryableProcessor, register the process_dlq topic with
  DLQ disabled (no cascade) and raised retry attempts, and start/stop
  the two consumers in the safe order (DLQ up first, drained last).

Reconciliation is idempotent: already-terminal requests are skipped, and
optimistic locking lets a late legitimate pipeline transition win.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback on the process DLQ PR: the DLQ subscription overrides
(DLQ.Enabled=false, MaxAttempts=1000) were duplicated across wiring files,
and two doc comments in the DLQ reconciler claimed behavior the code does
not have (release-first ordering prevents double release; decode errors
are non-retryable under AlwaysRetryableProcessor).

- platform/extension/messagequeue/subscription_config.go: add
  DLQSubscriptionConfig helper owning the DLQ-consumer subscription policy
- platform/extension/messagequeue/subscription_config_test.go: test the
  helper's contract
- service/stovepipe/server/main.go: use the helper
- service/submitqueue/orchestrator/server/main.go: use the helper
- stovepipe/controller/dlq/dlq.go: correct failRequest comment to state
  the real crash trade-off (double release vs leaked slot)
- stovepipe/controller/dlq/request.go: correct decode-error comment —
  retried deliberately to heal deployment skew, bounded by MaxAttempts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gsturges gsturges force-pushed the gsturges/stovepipe-process-dlq branch from 587410a to 78d3d91 Compare July 14, 2026 23:13
@gsturges

Copy link
Copy Markdown
Contributor Author

Rebased onto main. Besides the review feedback already discussed (the DLQSubscriptionConfig helper and the corrected failRequest / decode-error comments), the rebase required one substantive adaptation worth calling out:

The reconciler now forces recorded_not_green instead of introducing a separate failed state. While this PR was in review, main landed the terminal Request states (#362): recorded_green / recorded_not_green with an IsTerminal() method, and the RequestStateRecordedNotGreen doc comment (plus workflow.md's fail-closed section) already designates it as the conservative verdict the DLQ reconciler writes. Keeping this PR's RequestStateFailed would have meant two overlapping terminal-not-green states, so this branch now:

  • no longer touches stovepipe/entity/request.go (no RequestStateFailed, no IsRequestStateTerminal free function),
  • transitions reconciled requests to entity.RequestStateRecordedNotGreen and checks request.State.IsTerminal().

Reconciliation behavior is otherwise unchanged (idempotent terminal-skip, slot release before the terminal CAS, always-retryable pairing). newTopicRegistry keeps main's publish-only build topic alongside the new process_dlq subscription.

🤖 Generated with Claude Code

@gsturges gsturges enabled auto-merge July 15, 2026 18:04
func DLQSubscriptionConfig(subscriberName, consumerGroup string) SubscriptionConfig {
config := DefaultSubscriptionConfig(subscriberName, consumerGroup)
config.DLQ.Enabled = false
config.Retry.MaxAttempts = 1000

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we just add -1? thoughts? i don't recall if messaging queue support that symantix but i think it might

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.

3 participants