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
// DLQ.TopicSuffix) when the process controller exhausts retries; DLQ.Enabled is false on its own
// subscription so a reconciliation failure retries in place rather than cascading to a further DLQ.
func newTopicRegistry(q extqueue.Queue, subscriberName string) (consumer.TopicRegistry, error) {
dlqSub := extqueue.DefaultSubscriptionConfig(subscriberName, "stovepipe-process-dlq")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add an extqueue.DLQSubscriptionConfig helper that sets these values then you can just call it in the Subscription key similar to the DefaultSubscriptionConfig

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated

Comment thread stovepipe/controller/dlq/dlq.go Outdated
// terminal state. If the request had reached RequestStateProcessing — meaning process's
// admit step already CAS-incremented the queue's in_flight_count for it — the queue's
// slot is released first, so a crash between the two writes leaves the count still
// bound to a non-terminal request rather than double-released.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this true? Current ordering is: 1) release slot 2) potential crash 3) request state never gets updated out of processing, and could cause this whole logic to run again. In that cause it could get double released.

Either way I think it's ok for now, I don't foresee this being a typical issue and better to leave the queue unblocked than potentially deadlock it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are correct, I updated comment

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

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