Skip to content

feat: implement messaging, routing, and retry/cancel mechanisms (topics 7-9) - #19

Merged
gciavarrini merged 37 commits into
dcm-project:mainfrom
gabriel-farache:feat/topic9-retry-cancel-mechanisms
Aug 19, 2026
Merged

feat: implement messaging, routing, and retry/cancel mechanisms (topics 7-9)#19
gciavarrini merged 37 commits into
dcm-project:mainfrom
gabriel-farache:feat/topic9-retry-cancel-mechanisms

Conversation

@gabriel-farache

@gabriel-farache gabriel-farache commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR consolidates topics 7, 8, and 9. #15 (topic 7) and #18 (topic 8) were closed in favor of merging that work directly through this PR.

Topic 7 — Messaging integration

  • CloudEvent builder (NewCloudEvent, FormatSource) for spec-compliant CloudEvents
  • NATS JetStream messaging client: connection lifecycle, topic subscription/dispatch, response CE publishing, cancel deny-list management
  • Topic name validation and main/retry/cancel topic derivation (DeriveTopicNames)

Topic 8 — Resource operation routing

  • Route CloudEvent create/delete/cancel requests to the correct service provider by service type, with health-aware dispatch, retry backoff + jitter, and deny-list cancel semantics
  • Bug fixes and security hardening (ack-on-error, poison-pill ack-drop, TOCTOU race via atomic AddIfAbsent, bounded deny-list, payload validation, sanitized CE error details)

Topic 9 — Retry processor and cancel mechanisms

  • Retry processor: re-forwards pending requests from the retry topic once an SP recovers, with per-message handler deadline and stable Idempotency-Key forwarding
  • Health CE publisher: publishes dcm.agent.health.service-type-degraded / -unavailable CloudEvents on SP state transitions
  • SP forwarder extracted into a dedicated component; router internals refactored (DenyListResourceSet, RouterDeps, retry-consumer lifecycle wiring)
  • Transient in-flight lock (KeyLock) serializing concurrent forward attempts per resource_id (main-topic vs. retry-topic races)

@gabriel-farache

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Retry fetch errors masked ✓ Resolved 🐞 Bug ☼ Reliability ⭐ New
Description
retry.Processor.fetchAllFromConsumer breaks on any Consumer.Fetch error but always returns
(collected, nil), so non-timeout JetStream failures are silently treated as success. Callers (e.g.,
ProcessOnTransition) therefore proceed with partial/empty results without error propagation.
Code

internal/routing/retry/processor.go[R621-624]

+		batch, fetchErr := cons.Fetch(fetchBatchSize, jetstream.FetchMaxWait(fetchMaxWait))
+		if fetchErr != nil {
+			break
+		}
Evidence
ProcessOnTransition relies on fetchAllFromConsumer returning an error to abort/log;
fetchAllFromConsumer currently breaks on fetchErr but returns nil error unconditionally, masking
those failures.

internal/routing/retry/processor.go[131-152]
internal/routing/retry/processor.go[598-635]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`fetchAllFromConsumer` treats *all* `cons.Fetch` errors the same (break) and then returns `nil` error, masking real failures (auth/connection/consumer deleted/etc.).

## Issue Context
`ProcessOnTransition` only logs/returns errors when `fetchAllFromConsumer` returns a non-nil error; with the current implementation, many failures won’t be reported.

## Fix Focus Areas
- internal/routing/retry/processor.go[147-156]
- internal/routing/retry/processor.go[598-635]

## Proposed fix
- Distinguish expected “no more messages / timeout” from genuine errors:
 - If the error is the expected timeout (or context cancellation), stop fetching and return `collected, nil`.
 - Otherwise return `collected, fetchErr` (or wrap it) so callers can log and retry.
- Optionally log the fetch error (at least at debug/warn) when returning it to improve observability.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Cancel lacks handler deadline ✓ Resolved 🐞 Bug ☼ Reliability ⭐ New
Description
handleCancelMessage calls the cancel handler with context.Background(), so cancel processing has no
timeout/cancellation. Because Router.HandleCancel may synchronously purge the retry topic using the
provided ctx, a hung JetStream/store operation can stall the cancel consumer (and startup
cancel-drain) indefinitely.
Code

internal/messaging/handlers.go[R34-37]

+	c.logMessageReceived(msg, resourceID, ceID, ceType)
+
+	if err := c.cancelHandler(context.Background(), msg.Data()); err != nil {
+		if nakErr := msg.NakWithDelay(c.nakDelay()); nakErr != nil {
Evidence
The cancel handler is invoked with context.Background(), and Router cancel handling uses the passed
context to drive synchronous retry-topic purge work; without a deadline, cancel processing can hang
the consumer loop.

internal/messaging/handlers.go[15-43]
internal/routing/router.go[373-420]
internal/routing/router.go[430-437]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Cancel messages are processed with `context.Background()` in `handleCancelMessage`, so there is no per-message deadline/cancellation. Downstream cancel handling performs synchronous work (including retry-topic purge) and can block forever.

## Issue Context
- `Router.HandleCancel(ctx, ...)` calls `purgeFromRetryTopic(ctx, ...)`, which calls `FetchRetryMessages(ctx)`.
- If the cancel handler hangs, `drainCancelTopic` (which calls `handleCancelMessage` inline) can also hang, delaying/locking startup.

## Fix Focus Areas
- internal/messaging/handlers.go[15-49]
- internal/routing/router.go[373-455]

## Proposed fix
- In `handleCancelMessage`, create a per-message context with timeout (similar to `handleMainMessage`).
 - Prefer a timeout that is guaranteed `< CancelAckWait` to avoid redelivery during cancel handling (either a new `CancelHandlerTimeout` config, or `min(HandlerTimeout, CancelAckWait - safetyMargin)`).
- Pass that context into `c.cancelHandler(ctx, msg.Data())`.
- Optionally add a config invariant check analogous to `HandlerTimeout < AckWait`, but for cancel timeout vs `CancelAckWait`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Retry forwards can hang ✓ Resolved 🐞 Bug ☼ Reliability
Description
Retry processing on health transitions/restarts forwards to SPs without enforcing
ProcessorConfig.HandlerTimeout, so a hung SP call can block retry processing indefinitely when
invoked with the long-lived top-level context.
Code

internal/routing/retry/processor.go[R613-616]

+	err := routing.ForwardToSP(ctx, p.deps.Forwarder, sp, routing.ForwardParams{
+		ResourceID: res.resourceID, ServiceType: res.serviceType,
+		Spec: res.spec, EventID: res.eventID, IsCreate: res.ceType != cloudevent.TypeRequestDelete,
+	})
Evidence
The health-transition path uses the top-level context from main, and the retry processor forwarding
helper uses that context directly without applying HandlerTimeout, unlike the (unused here)
forwardAndAck path which does wrap with HandlerTimeout.

cmd/environment-agent/main.go[190-197]
internal/routing/retry/processor.go[148-193]
internal/routing/retry/processor.go[371-386]
internal/routing/retry/processor.go[589-616]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ProcessorConfig.HandlerTimeout` is not applied to forwarding during transition/restart retry processing (`forwardRequest`), so retry processing can hang indefinitely if an external SP call hangs (common when HTTP client has no timeout).

### Issue Context
- `main.go` wires `retryProcessor.RunTransition(ctx, ...)` using the top-level `run` context (cancellation-only, no deadline).
- `ProcessOnTransition` → `processTransitionItems` → `forwardRequest` forwards using the provided `ctx` directly.
- While `forwardAndAck` wraps ctx with `HandlerTimeout`, it’s only used by `Processor.Start` (not used in main wiring).

### Fix Focus Areas
- cmd/environment-agent/main.go[190-197]
- internal/routing/retry/processor.go[148-193]
- internal/routing/retry/processor.go[371-386]
- internal/routing/retry/processor.go[589-626]

### Suggested direction
- In `forwardRequest`, create a per-message context:
 - If `p.deps.Config.HandlerTimeout > 0`, wrap with `context.WithTimeout(ctx, ...)` (or compute min(existing_deadline, handler_timeout)).
 - Ensure `defer cancel()`.
- Use that per-message context for `routing.ForwardToSP(...)` so hung SP calls are canceled reliably during transition/restart processing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (2)
4. Retry JS captured nil ✓ Resolved 🐞 Bug ≡ Correctness
Description
retry.ProcessorDeps stores a one-time snapshot of msgClient.JS(); because messaging.Client.Start is
explicitly non-blocking, this snapshot can be nil during startup and the retry processor will
permanently no-op all retry fetch/purge logic.
Code

cmd/environment-agent/main.go[R144-147]

+		Forwarder:           forwarder,
+		Publisher:           msgClient,
+		JS:                  msgClient.JS(),
+		DenyList:            denyList,
Evidence
main wires retry.NewProcessor with a one-time msgClient.JS() value; messaging initializes c.js
later during setup, and the retry processor explicitly treats deps.JS == nil as a no-op when
fetching from consumers.

cmd/environment-agent/main.go[108-159]
internal/messaging/client.go[81-129]
internal/messaging/client.go[132-148]
internal/messaging/client.go[299-307]
internal/routing/retry/processor.go[33-42]
internal/routing/retry/processor.go[659-662]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`retry.NewProcessor` is constructed with `JS: msgClient.JS()` once at startup. If NATS/JetStream isn’t ready yet (Start is non-blocking), `msgClient.JS()` returns nil and the retry processor will keep `deps.JS == nil` forever, disabling retry-topic draining and cancel purging.

### Issue Context
- `messaging.Client.Start` returns immediately and initializes JetStream later during connection setup.
- `retry.Processor.fetchAllFromConsumer` returns `(nil, nil)` when `deps.JS` is nil, silently disabling processing.

### Fix Focus Areas
- cmd/environment-agent/main.go[108-159]
- internal/messaging/client.go[81-129]
- internal/messaging/client.go[299-307]
- internal/routing/retry/processor.go[33-42]
- internal/routing/retry/processor.go[659-662]

### Suggested direction
- Change `ProcessorDeps.JS` to a provider (e.g., `func() jetstream.JetStream` or an interface with `JS() jetstream.JetStream`) and resolve JetStream at call time inside `fetchAllFromConsumer`.
- Alternatively, delay creating/binding the processor (and `router.SetRetryConsumer`) until messaging setup has initialized JetStream, or add a `processor.SetJS(...)` that is invoked after successful messaging setup.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. SetupOnce blocks recovery ✓ Resolved 🐞 Bug ☼ Reliability
Description
messaging.Client.doSetup uses sync.Once around setupStreamsAndConsume; if setupStreamsAndConsume
exits early due to a transient JetStream/stream/consumer error, setup will never be retried on later
reconnects and consumers may never start.
Code

internal/messaging/client.go[R127-129]

+	c.setupOnce.Do(func() {
+		c.setupStreamsAndConsume(ctx, conn)
+	})
Evidence
doSetup permanently gates setup behind setupOnce.Do, while setupStreamsAndConsume returns on
errors without any mechanism to reset/retry setup, so subsequent connect callbacks cannot recover.

internal/messaging/client.go[93-106]
internal/messaging/client.go[123-154]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`sync.Once` is used to guard a fallible initialization path (`setupStreamsAndConsume`). If any step fails and returns, `setupOnce` still becomes “done”, so later successful connects can’t retry setup and the client may remain permanently uninitialized.

### Issue Context
- `setupStreamsAndConsume` logs and returns on multiple errors.
- `doSetup` is invoked from the NATS `ConnectHandler`, but `sync.Once` prevents subsequent calls.

### Fix Focus Areas
- internal/messaging/client.go[93-106]
- internal/messaging/client.go[123-154]

### Suggested direction
- Replace `setupOnce` with a stateful retry mechanism:
 - e.g., a `setupMu` + `setupDone bool` set only after full successful setup.
 - On failure, keep `setupDone=false` so the next connect can retry.
- Consider adding backoff/logging for repeated setup failures to improve operability.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Cancel panic terminates message ✓ Resolved 🐞 Bug ☼ Reliability ⭐ New
Description
On panic in cancel processing, handleCancelMessage calls msg.Term(), permanently discarding the
cancel message instead of retrying. This extends Term beyond the stated “parse failure” case and can
drop valid cancels when a transient bug/panic occurs.
Code

internal/messaging/handlers.go[R20-23]

+	defer func() {
+		if r := recover(); r != nil {
+			termErr := msg.Term()
+			attrs := []any{
Evidence
Cancel panic recovery explicitly calls Term(), while the messaging client’s consumer configuration
comment states cancels should only be Term’d on parse failure.

internal/messaging/handlers.go[15-32]
internal/messaging/client.go[415-417]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`handleCancelMessage` recovers panics by calling `msg.Term()`, which permanently removes the cancel message. This contradicts the nearby documented intent that cancel messages are only Term’d on parse failure.

## Issue Context
Cancel consumer is configured without `MaxDeliver`, so Term is the primary way cancels get dropped.

## Fix Focus Areas
- internal/messaging/handlers.go[15-32]
- internal/messaging/client.go[415-417]

## Proposed fix
- Change the cancel panic recovery behavior to mirror the main handler behavior:
 - Prefer `msg.NakWithDelay(c.nakDelay())` (or at least `msg.Nak()`) on panic, so a transient panic doesn’t silently drop the cancel.
- If dropping on panic is intentional, update the comment in `initConsumers` to reflect that cancels may be Term’d on panic as well, and consider publishing a terminal error CloudEvent for observability.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Start blocks during setup ✓ Resolved 🐞 Bug ☼ Reliability ⭐ New
Description
messaging.Client.Start is documented as non-blocking but calls doSetup synchronously when the
initial connection succeeds. doSetup can block up to requestStreamRetryTimeout waiting for the
request stream, so Start may block agent startup for a long time.
Code

internal/messaging/client.go[R201-204]

+	if conn.IsConnected() {
+		c.connected.Store(true)
+		c.doSetup(setupCtx, conn)
+	}
Evidence
Start explicitly documents non-blocking behavior, but invokes doSetup inline on an already-connected
connection. doSetup can block while creating request consumers, which retries until
requestStreamRetryTimeout expires.

internal/messaging/client.go[166-215]
internal/messaging/client.go[441-458]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`messaging.Client.Start` claims to be non-blocking, but it calls `c.doSetup(...)` inline when `conn.IsConnected()`. `doSetup`/consumer creation can block for up to `requestStreamRetryTimeout`, so `Start` can block despite the documented contract.

## Issue Context
This is especially likely when NATS is reachable but the control-plane request stream (`RequestStreamName`) is not created yet.

## Fix Focus Areas
- internal/messaging/client.go[166-215]

## Proposed fix
- In `Start`, change the `if conn.IsConnected()` path to `go c.doSetup(setupCtx, conn)` (matching the ConnectHandler/ReconnectHandler behavior).
- Consider whether `Start(ctx)` should use the passed `ctx` (instead of `context.Background()`) for setup cancellation, or ensure `Stop()` always cancels/halts setup promptly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Cancel purge resets retries ✓ Resolved 🐞 Bug ☼ Reliability
Description
Both routing.Router.purgeFromRetryTopic and retry.Processor handle retry-topic messages by
republishing the payload back onto <topic>.retry and then ACKing the original, effectively
rewriting the retry backlog. This creates new JetStream messages with fresh delivery history, making
cancel operations O(backlog) and undermining NumDelivered/MaxDeliver-based poison termination so
persistently failing messages can evade cutoff and retry indefinitely.
Code

internal/routing/router.go[R385-388]

+		if err := rc.RepublishToRetry(ctx, m.Data); err != nil {
+			return fmt.Errorf("failed to republish non-matching message: %w", err)
+		}
+		if err := m.AckFunc(); err != nil {
Evidence
The cited cancellation path republished every non-matching retry message and then ACKed it,
replacing messages one-for-one with newly published instances and thereby resetting JetStream
delivery metadata while doing work proportional to the entire fetched retry backlog. Separately, the
retry processor similarly republishes messages to the retry subject and then ACKs the originals
(including in route/transition failure paths), and poison handling uses
msg.Metadata().NumDelivered to decide when to emit a MaxDeliver terminal error and Term() the
message; because republishing creates a new message instance with NumDelivered restarted, the
MaxDeliver cutoff no longer reflects true attempt count and can be bypassed.

internal/routing/router.go[370-391]
internal/routing/types.go[37-49]
internal/routing/retry/processor.go[353-372]
internal/routing/retry/processor.go[644-671]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The retry pipeline currently “moves” retry messages by republishing them back onto `<topic>.retry` and then ACKing the original message (both in `Router.purgeFromRetryTopic` during cancel and within `retry.Processor` during retry/error transitions). This rewrites the retry backlog, resets JetStream delivery metadata (`msg.Metadata().NumDelivered`) that MaxDeliver poison handling relies on, makes cancel operations scale with the entire backlog, and can allow persistently failing messages to evade MaxDeliver termination.

## Issue Context
- In the router cancellation flow, non-matching (non-cancelled) retry messages are republished and then ACKed, effectively rewriting all unrelated messages just to cancel a subset.
- In the retry processor, multiple paths republish to the retry subject and then ACK the original, creating a new message instance with a fresh delivery history.
- Poison termination uses `msg.Metadata().NumDelivered` in `terminateIfMaxDeliver` to decide when to publish a MaxDeliver terminal error and `Term()` the message; republishing resets this accounting.
- The current `RetryTopicConsumer` abstraction appears to expose only `AckFunc` plus `RepublishToRetry`, which pushes callers toward republish+ack instead of preserving the original message.

## Fix Focus Areas
- internal/routing/router.go[370-391]
- internal/routing/types.go[37-49]
- internal/routing/retry/processor.go[644-672]
- internal/routing/retry/processor.go[353-374]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
9. Delete URL not escaped ✓ Resolved 🐞 Bug ≡ Correctness
Description
routing.Forwarder.deleteExternal concatenates endpoint + "/" + req.ResourceID; since resourceId is
only checked for non-empty, reserved URL characters in the ID can change the request path/query and
delete the wrong target or fail.
Code

internal/routing/forwarder.go[R103-105]

+func (f *Forwarder) deleteExternal(ctx context.Context, endpoint string, req DeleteResourceRequest) error {
+	url := endpoint + "/" + req.ResourceID
+	httpReq, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
Evidence
Router only enforces resourceId is non-empty, and forwarder’s DELETE path uses raw string
concatenation, so special characters can alter the resulting request URL.

internal/routing/router.go[121-136]
internal/routing/forwarder.go[103-111]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
DELETE forwarding builds the URL via string concatenation with an unescaped `resourceId`. If `resourceId` contains `/`, `?`, or `#`, the final HTTP request path can be malformed or refer to a different path than intended.

### Issue Context
- Router accepts `resourceId` from inbound CloudEvent data and only validates it is non-empty.
- Forwarder uses the raw value when constructing the DELETE URL.

### Fix Focus Areas
- internal/routing/forwarder.go[103-111]
- internal/routing/router.go[121-136]

### Suggested direction
- Use structured URL construction:
 - `url.JoinPath(endpoint, resourceID)` (Go will escape path segments), or
 - `u, _ := url.Parse(endpoint); u.Path = path.Join(u.Path, url.PathEscape(resourceID));`
- Optionally validate `resourceId` format if the system expects a constrained character set.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 045aeca

Results up to commit a7086d9 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Retry forwards can hang ✓ Resolved 🐞 Bug ☼ Reliability
Description
Retry processing on health transitions/restarts forwards to SPs without enforcing
ProcessorConfig.HandlerTimeout, so a hung SP call can block retry processing indefinitely when
invoked with the long-lived top-level context.
Code

internal/routing/retry/processor.go[R613-616]

+	err := routing.ForwardToSP(ctx, p.deps.Forwarder, sp, routing.ForwardParams{
+		ResourceID: res.resourceID, ServiceType: res.serviceType,
+		Spec: res.spec, EventID: res.eventID, IsCreate: res.ceType != cloudevent.TypeRequestDelete,
+	})
Evidence
The health-transition path uses the top-level context from main, and the retry processor forwarding
helper uses that context directly without applying HandlerTimeout, unlike the (unused here)
forwardAndAck path which does wrap with HandlerTimeout.

cmd/environment-agent/main.go[190-197]
internal/routing/retry/processor.go[148-193]
internal/routing/retry/processor.go[371-386]
internal/routing/retry/processor.go[589-616]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ProcessorConfig.HandlerTimeout` is not applied to forwarding during transition/restart retry processing (`forwardRequest`), so retry processing can hang indefinitely if an external SP call hangs (common when HTTP client has no timeout).

### Issue Context
- `main.go` wires `retryProcessor.RunTransition(ctx, ...)` using the top-level `run` context (cancellation-only, no deadline).
- `ProcessOnTransition` → `processTransitionItems` → `forwardRequest` forwards using the provided `ctx` directly.
- While `forwardAndAck` wraps ctx with `HandlerTimeout`, it’s only used by `Processor.Start` (not used in main wiring).

### Fix Focus Areas
- cmd/environment-agent/main.go[190-197]
- internal/routing/retry/processor.go[148-193]
- internal/routing/retry/processor.go[371-386]
- internal/routing/retry/processor.go[589-626]

### Suggested direction
- In `forwardRequest`, create a per-message context:
 - If `p.deps.Config.HandlerTimeout > 0`, wrap with `context.WithTimeout(ctx, ...)` (or compute min(existing_deadline, handler_timeout)).
 - Ensure `defer cancel()`.
- Use that per-message context for `routing.ForwardToSP(...)` so hung SP calls are canceled reliably during transition/restart processing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Retry JS captured nil ✓ Resolved 🐞 Bug ≡ Correctness
Description
retry.ProcessorDeps stores a one-time snapshot of msgClient.JS(); because messaging.Client.Start is
explicitly non-blocking, this snapshot can be nil during startup and the retry processor will
permanently no-op all retry fetch/purge logic.
Code

cmd/environment-agent/main.go[R144-147]

+		Forwarder:           forwarder,
+		Publisher:           msgClient,
+		JS:                  msgClient.JS(),
+		DenyList:            denyList,
Evidence
main wires retry.NewProcessor with a one-time msgClient.JS() value; messaging initializes c.js
later during setup, and the retry processor explicitly treats deps.JS == nil as a no-op when
fetching from consumers.

cmd/environment-agent/main.go[108-159]
internal/messaging/client.go[81-129]
internal/messaging/client.go[132-148]
internal/messaging/client.go[299-307]
internal/routing/retry/processor.go[33-42]
internal/routing/retry/processor.go[659-662]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`retry.NewProcessor` is constructed with `JS: msgClient.JS()` once at startup. If NATS/JetStream isn’t ready yet (Start is non-blocking), `msgClient.JS()` returns nil and the retry processor will keep `deps.JS == nil` forever, disabling retry-topic draining and cancel purging.

### Issue Context
- `messaging.Client.Start` returns immediately and initializes JetStream later during connection setup.
- `retry.Processor.fetchAllFromConsumer` returns `(nil, nil)` when `deps.JS` is nil, silently disabling processing.

### Fix Focus Areas
- cmd/environment-agent/main.go[108-159]
- internal/messaging/client.go[81-129]
- internal/messaging/client.go[299-307]
- internal/routing/retry/processor.go[33-42]
- internal/routing/retry/processor.go[659-662]

### Suggested direction
- Change `ProcessorDeps.JS` to a provider (e.g., `func() jetstream.JetStream` or an interface with `JS() jetstream.JetStream`) and resolve JetStream at call time inside `fetchAllFromConsumer`.
- Alternatively, delay creating/binding the processor (and `router.SetRetryConsumer`) until messaging setup has initialized JetStream, or add a `processor.SetJS(...)` that is invoked after successful messaging setup.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. SetupOnce blocks recovery ✓ Resolved 🐞 Bug ☼ Reliability
Description
messaging.Client.doSetup uses sync.Once around setupStreamsAndConsume; if setupStreamsAndConsume
exits early due to a transient JetStream/stream/consumer error, setup will never be retried on later
reconnects and consumers may never start.
Code

internal/messaging/client.go[R127-129]

+	c.setupOnce.Do(func() {
+		c.setupStreamsAndConsume(ctx, conn)
+	})
Evidence
doSetup permanently gates setup behind setupOnce.Do, while setupStreamsAndConsume returns on
errors without any mechanism to reset/retry setup, so subsequent connect callbacks cannot recover.

internal/messaging/client.go[93-106]
internal/messaging/client.go[123-154]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`sync.Once` is used to guard a fallible initialization path (`setupStreamsAndConsume`). If any step fails and returns, `setupOnce` still becomes “done”, so later successful connects can’t retry setup and the client may remain permanently uninitialized.

### Issue Context
- `setupStreamsAndConsume` logs and returns on multiple errors.
- `doSetup` is invoked from the NATS `ConnectHandler`, but `sync.Once` prevents subsequent calls.

### Fix Focus Areas
- internal/messaging/client.go[93-106]
- internal/messaging/client.go[123-154]

### Suggested direction
- Replace `setupOnce` with a stateful retry mechanism:
 - e.g., a `setupMu` + `setupDone bool` set only after full successful setup.
 - On failure, keep `setupDone=false` so the next connect can retry.
- Consider adding backoff/logging for repeated setup failures to improve operability.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
4. Delete URL not escaped ✓ Resolved 🐞 Bug ≡ Correctness
Description
routing.Forwarder.deleteExternal concatenates endpoint + "/" + req.ResourceID; since resourceId is
only checked for non-empty, reserved URL characters in the ID can change the request path/query and
delete the wrong target or fail.
Code

internal/routing/forwarder.go[R103-105]

+func (f *Forwarder) deleteExternal(ctx context.Context, endpoint string, req DeleteResourceRequest) error {
+	url := endpoint + "/" + req.ResourceID
+	httpReq, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
Evidence
Router only enforces resourceId is non-empty, and forwarder’s DELETE path uses raw string
concatenation, so special characters can alter the resulting request URL.

internal/routing/router.go[121-136]
internal/routing/forwarder.go[103-111]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
DELETE forwarding builds the URL via string concatenation with an unescaped `resourceId`. If `resourceId` contains `/`, `?`, or `#`, the final HTTP request path can be malformed or refer to a different path than intended.

### Issue Context
- Router accepts `resourceId` from inbound CloudEvent data and only validates it is non-empty.
- Forwarder uses the raw value when constructing the DELETE URL.

### Fix Focus Areas
- internal/routing/forwarder.go[103-111]
- internal/routing/router.go[121-136]

### Suggested direction
- Use structured URL construction:
 - `url.JoinPath(endpoint, resourceID)` (Go will escape path segments), or
 - `u, _ := url.Parse(endpoint); u.Path = path.Join(u.Path, url.PathEscape(resourceID));`
- Optionally validate `resourceId` format if the system expects a constrained character set.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 12f4e92 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Cancel purge resets retries ✓ Resolved 🐞 Bug ☼ Reliability
Description
Both routing.Router.purgeFromRetryTopic and retry.Processor handle retry-topic messages by
republishing the payload back onto <topic>.retry and then ACKing the original, effectively
rewriting the retry backlog. This creates new JetStream messages with fresh delivery history, making
cancel operations O(backlog) and undermining NumDelivered/MaxDeliver-based poison termination so
persistently failing messages can evade cutoff and retry indefinitely.
Code

internal/routing/router.go[R385-388]

+		if err := rc.RepublishToRetry(ctx, m.Data); err != nil {
+			return fmt.Errorf("failed to republish non-matching message: %w", err)
+		}
+		if err := m.AckFunc(); err != nil {
Evidence
The cited cancellation path republished every non-matching retry message and then ACKed it,
replacing messages one-for-one with newly published instances and thereby resetting JetStream
delivery metadata while doing work proportional to the entire fetched retry backlog. Separately, the
retry processor similarly republishes messages to the retry subject and then ACKs the originals
(including in route/transition failure paths), and poison handling uses
msg.Metadata().NumDelivered to decide when to emit a MaxDeliver terminal error and Term() the
message; because republishing creates a new message instance with NumDelivered restarted, the
MaxDeliver cutoff no longer reflects true attempt count and can be bypassed.

internal/routing/router.go[370-391]
internal/routing/types.go[37-49]
internal/routing/retry/processor.go[353-372]
internal/routing/retry/processor.go[644-671]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The retry pipeline currently “moves” retry messages by republishing them back onto `<topic>.retry` and then ACKing the original message (both in `Router.purgeFromRetryTopic` during cancel and within `retry.Processor` during retry/error transitions). This rewrites the retry backlog, resets JetStream delivery metadata (`msg.Metadata().NumDelivered`) that MaxDeliver poison handling relies on, makes cancel operations scale with the entire backlog, and can allow persistently failing messages to evade MaxDeliver termination.

## Issue Context
- In the router cancellation flow, non-matching (non-cancelled) retry messages are republished and then ACKed, effectively rewriting all unrelated messages just to cancel a subset.
- In the retry processor, multiple paths republish to the retry subject and then ACK the original, creating a new message instance with a fresh delivery history.
- Poison termination uses `msg.Metadata().NumDelivered` in `terminateIfMaxDeliver` to decide when to publish a MaxDeliver terminal error and `Term()` the message; republishing resets this accounting.
- The current `RetryTopicConsumer` abstraction appears to expose only `AckFunc` plus `RepublishToRetry`, which pushes callers toward republish+ack instead of preserving the original message.

## Fix Focus Areas
- internal/routing/router.go[370-391]
- internal/routing/types.go[37-49]
- internal/routing/retry/processor.go[644-672]
- internal/routing/retry/processor.go[353-374]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread cmd/environment-agent/main.go
Comment thread internal/routing/retry/processor.go Outdated
Comment thread internal/messaging/client.go Outdated
Comment thread internal/routing/forwarder.go Outdated
@gabriel-farache
gabriel-farache force-pushed the feat/topic9-retry-cancel-mechanisms branch from a7086d9 to 12f4e92 Compare August 7, 2026 09:04
@gabriel-farache

Copy link
Copy Markdown
Contributor Author

/agentic_review

Comment thread internal/routing/router.go Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 12f4e92

@gabriel-farache
gabriel-farache force-pushed the feat/topic9-retry-cancel-mechanisms branch 2 times, most recently from 5eef49c to 014640f Compare August 11, 2026 07:39
gabriel-farache and others added 3 commits August 11, 2026 09:48
Add UT-MSG-037–041 (whitelist regex edge cases) to unit test plan and
IT-MSG-071–073 (nested payload, delete response, publish error nak) to
integration test plan. Update traceability matrices accordingly.

Assisted by: Claude Code - claude-4.6-opus

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Add CloudEvent builder (internal/cloudevent) with FormatSource/FormatType
helpers and NewResponseCE constructor. Implement NATS JetStream messaging
client (internal/messaging) with topic subscription, handler dispatch,
response publishing, and cancel-request deny-list management. Wire
messaging setup into main via setupMessaging helper.

Includes whitelist-based topic validation, typed CE payload extraction,
compile-time interface assertions, and separated handler file. Config
extended with NATS and messaging fields. Full unit and integration test
coverage with test suites.

Assisted by: Claude Code - claude-4.6-opus

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…peration routing

Update architecture decision DD-190 with deny-list semantics and add
ASM-RTE-010 documenting the resource operation routing contract and
CloudEvent publishing interface.

Assisted by: Claude Code - opus-4.6

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@gabriel-farache
gabriel-farache force-pushed the feat/topic9-retry-cancel-mechanisms branch from dae70a3 to 02a177a Compare August 11, 2026 07:48
gabriel-farache and others added 6 commits August 11, 2026 10:06
Add resource operation router with deny-list filtering, backoff-aware
routing decisions, and CloudEvent publishing. Includes supporting
refactors to messaging handlers, config, provider store, and cloudevent
types, plus full unit and integration test coverage.

Assisted by: Claude Code - opus-4.6

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Ignore locally downloaded dev tools (.bin/, e.g. the staticcheck binary
pulled during lint/review work), and replace the by-name exclusions for
.ai/reviews, .ai/plans, and .ai/checkpoints with a blanket ignore of
.ai/ that only re-includes the tracked specs, decisions, and test docs
directories.

Assisted by: Claude Code - sonnet-5
Co-authored-by: Cursor <cursoragent@cursor.com>

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Signed-off-by: gabriel-farache <gfarache@redhat.com>
…ening-pass work

Add acceptance criteria, decision-log entries, and unit/integration test
coverage for the retry-topic processor, cancel mechanisms, messaging
reliability, provider registry, and configuration hardening work in this
branch: restart-drain sequencing and NATS reconnect backoff, retry-topic
MaxDeliver handling, registry slot-ownership checks, FileStore fsync
durability, registrar panic recovery, embedded SP transition ordering,
file-based config loading, input normalization, and RFC 7807
error-response wiring.

Sync the spec and decision log with the CP/agent wire-format alignment:
snake_case CE and REST payload fields, dcm.agent. subject prefixing
(including the reserved-prefix rejection rule), control-plane ownership
of dcm-agent-requests/dcm-agent-responses, Nats-Msg-Id dedup on
response/health CEs, and /api/v1alpha1 endpoints. Also corrects stale
REQ-MSG-051/AC-MSG-012/DD-230 wording and §9 requirement-ID counts, and
documents heartbeat timestamp monotonicity (REQ-DCM-150, AC-DCM-095).

Add DD-250/260/270 documenting three audit findings intentionally out of
scope for this hardening pass: embedded SP operation handlers were never
built, DCM resource-capacity reporting has no data source yet, and
Kubernetes pod conditions are unimplemented beyond a dead config flag.

Assisted by: Claude Code - sonnet-5
Co-authored-by: Cursor <cursoragent@cursor.com>

Signed-off-by: gabriel-farache <gfarache@redhat.com>
…ng and harden reliability

Implement the retry-topic processor with deadline enforcement, max-delivery
limits, idempotency tracking, and cancel-via-new-CE support; extract
SPForwarder, refactor the router into smaller components, add a health CE
publisher, and introduce shared test helpers.

Harden the new retry/cancel path: distinguish transient store I/O errors
from "provider not found" so callers retry instead of permanently
rejecting requests, add panic recovery with stack traces to both message
handlers, gate deny-list checks on isCreate so deletes bypass it, fix
O(N^2) batch heartbeating, use a store-error-specific retry delay, and
validate CE types consistently with the router.

Align CE wire format and stream ownership with the control plane: convert
CE data and DCM REST payloads to snake_case, prefix CP-facing subjects
with dcm.agent., stop creating streams for CP-owned subjects (consume via
durable consumers with bounded-then-background retry instead), publish
via PublishWithMsgID for JetStream dedup, and move registration/heartbeat
to /api/v1alpha1.

Fix IT-HTTP-090 and IT-RCM-080 test flakes, and rewire the health
integration suite through the production handler chain
(handler.New -> NewStrictHandlerWithOptions -> apiserver.New) instead of a
hand-rolled stub.

Close a further set of reliability/correctness gaps found during a
hardening pass: sequence restart-drain before live consumption, add NATS
reconnect backoff with jitter, drain in-flight handlers before shutdown;
enforce registry slot-ownership and fsync FileStore writes; recover the
DCM registrar goroutine from panics; apply MaxDeliver termination to the
retry topic; fix embedded SP health-transition wiring order; wire RFC
7807-compliant decode error responses; add minimal file-based config
loading.

Assisted by: Claude Code - sonnet-5
Co-authored-by: Cursor <cursoragent@cursor.com>

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Add REQ/AC/IT entries and structured logging so a resource's full
lifecycle (create, cancel, queue, retry, delete) and the agent's own
lifecycle (DCM registration, health checks, SP registration/embedded
startup) are auditable and traceable end to end. All correlation
fields use one canonical snake_case name each (resource_id, ce_id,
ce_type, service_type, provider_id) instead of mixed camelCase
variants.

- messaging: log message receipt, ack/nak/term resolution, and
  consumer start/stop, captured before the panic-recovery defer so
  panics remain traceable to the message that caused them.
- routing/retry: log SP dispatch outcomes, CE publish results, retry
  attempts, deny-list drops, and drain-on-restart summaries via a
  SafeErrorAttrs helper that never leaks a wrapped SP response body.
  Fixes a MaxDeliver-defeating bug where cancel-purge acked and
  republished every non-matching retry message as a fresh copy,
  resetting JetStream's delivery count for other in-flight retries on
  every single cancel; those messages are now Nak'd in place instead.
- health/provider: log health-check results/transitions and SP
  registration/rejection/restore, previously silent on the success
  path. Fixes a staleness bug where changing only a provider's service
  type (not its endpoint) never re-synced the health monitor's cached
  service_type, and a shutdown-latency bug where periodic health
  checks weren't bound to the monitor's own stop context, so Stop()
  could block up to checkTimeout on an in-flight check.
- dcm: log registrar start (fresh vs. post-panic restart, so operators
  can tell them apart), heartbeat success, and re-registration.

Also fix .gitignore: the .ai/ blanket exclusion silently shadowed the
!.ai/specs, !.ai/decisions, !.ai/test-plans negations (a parent
directory exclusion can't be re-included path-by-path in git), and the
negations referenced stale directory names that didn't match the
actual specs/ and test-plans/ directories.

Reviewed across two rounds of adversarial review by independent
subagents (Claude Opus, Grok, Gemini, Codex), each topic covered by at
least two different models per round, with no reviewer repeating a
role/scope across rounds.

Assisted by: Claude Code - opus-4.6

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@gciavarrini gciavarrini left a comment

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.

Title/summary are Topic 9 (retry/cancel, AC-RCM-070 through AC-RCM-130), but later commits also add CP wire format and stream ownership, logging REQs/ACs (240 through 270), hardening DDs (250/260/270 and related), health IT rewiring, and .cursor/rules. Worth updating the PR body to list what is in scope now, or splitting / marking the non topic 9 work. “Section 4.9 all satisfied” also looks stale after logging/hardening and DD-250.

Comment thread internal/routing/router.go
Comment thread internal/messaging/client.go Outdated
Comment thread .ai/specs/environment-agent.spec.md Outdated
Comment thread .ai/specs/environment-agent.spec.md Outdated
Comment thread .ai/specs/environment-agent.spec.md Outdated
Comment thread .ai/test-plans/2026-07-17-16-28-integration-tests.md Outdated
Comment thread internal/routing/retry/processor.go
Comment thread internal/routing/router.go
Comment thread internal/routing/types.go Outdated
Comment thread internal/routing/retry/processor.go
gabriel-farache and others added 3 commits August 11, 2026 15:10
Cancel-purge Naks every non-matching retry-topic message in place on
every cancel, incrementing JetStream's delivery count for messages
unrelated to the cancelled resource. With a MaxDeliver ceiling in
place, a burst of cancels for other resources could push an
otherwise-healthy retry message toward premature MAX_DELIVERY_EXCEEDED
termination — a false positive driven by unrelated cancel traffic, not
actual SP failures.

The retry-subject consumer now has no MaxDeliver limit, mirroring the
existing cancel-consumer exemption; REQ-RCM-150 scopes MaxDeliver to
the main-subject consumer only, and retry.Processor's mirrored
terminalOnMaxDeliver guard is removed along with it. Accepted
trade-off (documented in DD-410): retry-topic residency is now bounded
only by SP health-state transitions, and REQ-HMN-090 means a reachable
SP that persistently reports itself unhealthy never reaches
Unavailable, so its queued resources can sit in the retry topic
indefinitely with no error CloudEvent ever published. No time-based
Unhealthy-to-Unavailable escalation was added to close that gap.

Assisted by: Claude Code - claude-4.6-opus

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Apply all 14 findings (F1-F14) from the PR dcm-project#19 review audit:

- Fix ConsumerLag() to count NumPending + NumAckPending (was
  undercounting delivered-but-unacked messages) and fetch live
  consumer info instead of stale cached info
- Add a transient in-flight lock (REQ-RTE-200/210) to block
  concurrent double-dispatch for the same resourceId, without
  breaking delete-after-create or blocking later redeliveries
- Consolidate verbose Nak-in-place/publish-and-ack comments
- Sync spec/decisions/test-plan docs with the Nak-in-place wording,
  the embedded-SP idempotency deferral (DD-250), and the AC-RCM
  logging renumbering that closes the 140-230 gap

A follow-up round of independent multi-model reviews (2 models per
topic) then caught and fixed:

- The in-flight lock used an LRU-evicting set, so eviction under
  high resourceId cardinality could silently release an active lock
  and re-enable the exact double-dispatch it exists to prevent.
  Switched to a non-evicting set — it's naturally bounded by
  concurrent-forward count, not resource cardinality, so unbounded
  is safe
- Stale AC-RCM-250 references left in three test files after the
  logging AC renumbering (now AC-RCM-150)
- Spec §6's consolidated config table was missing the two Topic 9
  config rows already defined in §4.9
- AC-RTE-025/026 wording gaps: an untestable "Then" clause, a missing
  "log the anomaly" assertion, and an error-code literal not used by
  any sibling AC

Assisted by: Claude Code - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Section 4.5's config table still described auto/true/false runtime
behavior for AGENT_POD_CONDITIONS_ENABLED, contradicting DD-270 (the
value is parsed and validated but never read past config loading —
pod-condition updates are unimplemented in v1alpha1).

Assisted by: Claude Code - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread internal/messaging/client.go
Comment thread internal/routing/router.go
Comment thread internal/routing/retry/processor.go
Comment thread internal/routing/resolve.go
Comment thread internal/routing/router.go
gabriel-farache and others added 5 commits August 12, 2026 15:32
…ames

ValidateTopicName allows dots in the base name (they are valid NATS
subject tokens, per REQ-MSG-010/UT-MSG-034), but the same base is also
used to derive JetStream stream and durable-consumer names
(MainConsumer, CancelConsumer, RetryStream, RetryConsumer), which
nats.go rejects outright if they contain a dot. A dotted
AGENT_NAME/AGENT_TOPIC_NAME previously passed startup validation and
then hung forever in createRequestConsumer's retry loop, never
becoming functional.

Add ValidateJetStreamSafeName, wired into setupMessaging alongside
ValidateTopicName, so this now fails startup immediately with a clear
error instead of an indefinite silent retry loop. Documented as
REQ-MSG-011/AC-MSG-011. Also backfills two previously-untested
ValidateTopicName boundary cases ("." and "..") found while auditing
this area.

Found during multi-agent review of PR15 findings closure.

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Multiple independent reviewers flagged that IT-DCM-106 didn't actually
catch the bug it claims to guard: its gap>0/gap<=MaxBackoff assertions
also pass under the pre-fix immediate-retry bug, since real wall-clock
time always elapses even for a "0 backoff" retry. Verified this
directly by running the old test against the pre-fix commit (c3f5b8d)
in a scratch worktree: 1052 registrations/sec under the bug, vs an
expected ~5/sec under real full-jitter backoff. Rewrote it to assert
attempt count over a fixed window instead of a single inter-attempt
gap, which does discriminate the two behaviors with a wide margin.

Also:
- Add IT-DCM-107: a literal Retry-After:"0" (ParseRetryAfter returns
  (0, true), not (0, false)) exercises computeBackoff's own
  RetryAfter>0 guard specifically, previously untested end to end.
- Add UT-DCM-039: ParseRetryAfter at the exact d>0 boundary (HTTP-date
  equal to now), not just clearly-past dates.
- Add UT-DCM-016/017: CalculateBackoff with non-positive initial, and
  with a near-int64-max cap requiring many doublings — UT-DCM-015's
  huge attempt count doesn't exercise the overflow guard at a
  meaningful boundary when max is small, since d exceeds max within a
  handful of doublings regardless of attempt's magnitude.

Found during multi-agent review of PR15 findings closure.

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Round 2 review found UT-DCM-017 used max=MaxInt64/2, where the next
doubling after the guard fires (2^62) still fits in int64 — so an
implementation with the d > max/2 guard deleted entirely returns the
identical value, meaning the test didn't actually prove overflow
safety despite testing at "near-int64-max scale". Verified by running
both variants directly, and by mutation-testing (temporarily deleting
the guard in backoff.go): with max=MaxInt64/2 the test still passed;
with max=MaxInt64 (2^62's doubling wraps to a large negative value
without the guard) it correctly fails.

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ValidateJetStreamSafeName (added for the dot-rejection fix) only
checked character content, not length. A base name up to 255 chars
passes ValidateTopicName's own limit, but the derived JetStream
stream/consumer names append a suffix on top of it (the longest is
CancelConsumer's "-cancel-consumer", 16 chars) — pushing the derived
name past NATS server's 255-char JSMaxNameLen for base names above
239 chars. This reproduces the exact same indefinite setup-retry hang
as the dot bug, just via length instead of character set.

Extend ValidateJetStreamSafeName to also reject base names that would
overflow once the longest suffix is appended, and wire the same check
into the ClientConfig godoc that callers are expected to follow.
Update REQ-MSG-011/AC-MSG-011 and DD-220 to document the length
constraint alongside the existing dot constraint, and add unit/
integration test coverage at the 239/240-char boundary.

Found during round 2 multi-agent review of the round1-fix dot-name
change.

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Full jitter over [0, 200ms] draws a mean wait of 100ms, giving ~10
attempts/sec, not the ~5/sec the comment claimed. Doesn't affect test
correctness (10 is still far below the 40 threshold), but the wrong
number could mislead a future maintainer reasoning about the
threshold from the comment alone.

Found independently by two round 2 reviewers.

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
gabriel-farache and others added 4 commits August 12, 2026 16:48
…40 test

resolveEmbeddedIdentity's 4 tests were mis-cited as UT-SPR-100-103,
colliding with tests rightfully owned by AC-SPR-015 and AC-SPR-111.
Renumber them to UT-SPR-104-107 (free IDs) and correctly assign
UT-SPR-102/103 to the removeStaleEmbedded tests that actually claim
them in the test-plan docs.

REQ-RCM-240 (MUST: log every CloudEvent publish attempt, success or
failure) had no test actually exercising the failure path, and the
success path was only touched incidentally by an unrelated test.
Add IT-RTE-140 covering both outcomes via Router.publishCE.

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
A repo-wide traceability audit found 18 tests that are documented and
functionally correct, but predate the Ginkgo ID-citation convention
and never got the ID embedded in the code (client_backoff_test.go,
client_setup_test.go, client_drain_test.go, client_onsetupready_test.go,
concurrency_race_test.go, health_monitor_integration_test.go,
handlers_test.go). No test logic changed; only citations were added.

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Repo-wide audit cross-referencing every UT-*/IT-* ID in code against
the test-plan docs. Fixes and backfills found beyond the two prior
commits:

- AC-DCM-095 was defined twice in the spec for two unrelated behaviors
  ("Heartbeat timestamps strictly increase" and "Heartbeat failure
  resilience"). Renamed the former to AC-DCM-091 (the latter is the one
  already cross-referenced by IT-DCM-150 in the docs).
- Extended AC-DCM-055 to require the post-panic restart's "DCM
  registrar starting" log to carry restart_attempt, distinguishing it
  from the initial startup log — implemented and tested (IT-DCM-195),
  but not previously specified.
- Added REQ/AC-SPR-221, -222, -241 for embedded/external SP
  registration-and-removal logging edge cases (stale-save-failure WARN,
  removal success/failure, update success/failure) — all implemented
  and tested (IT-SPR-194-198), but had no governing requirement.
- Backfilled 14 tests that exist in code and pass, but had zero
  test-plan entry: IT-DCM-035/135/195, IT-MSG-105/107, IT-SPR-085,
  IT-SPR-194-199, UT-HMN-080, UT-MSG-025.

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The spec's "Requirement ID Index" (Section 9) totals were stale after
this session's REQ-SPR-052/221/222/241 and earlier REQ-MSG-011
additions: SPR was 31 (actual 35), MSG was 27 (actual 28), Total was
208 (actual 213). Recomputed and verified all 13 prefixes against the
actual REQ- IDs in the spec.

Separately, cross-checking every ### IT-* test-plan entry against the
integration-tests.md traceability matrix (by AC, not just by ID
existence — a different axis than the earlier code-citation audit)
found 15 fully-documented tests with zero row in the matrix: IT-DCM-015,
IT-DCM-190, IT-HMN-190/191, IT-MSG-095/170/171/172, IT-SPR-145/146/147/
191/192/193, IT-XC-LOG-030. Added them to their already-stated
"Validates AC" rows. IT-MSG-140 is intentionally left out — its own
entry already documents that it validates no AC.

Verified every AC-* heading in the spec has at least one test-plan
mention, and every ### test-plan header appears in its own matrix
(modulo IT-MSG-140's documented exception).

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@gabriel-farache
gabriel-farache marked this pull request as ready for review August 12, 2026 14:58
@gabriel-farache
gabriel-farache requested a review from a team as a code owner August 12, 2026 14:58
Comment thread internal/messaging/client.go
Comment thread internal/messaging/handlers.go
Comment thread internal/messaging/handlers.go
Comment thread internal/routing/retry/processor.go
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8ee6fd0

@gabriel-farache gabriel-farache changed the title feat(routing): implement retry processor and cancel mechanisms (topic 9) feat: implement messaging, routing, and retry/cancel mechanisms (topics 7-9) Aug 12, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

feat(routing): retry processor, cancel purge, SP forwarder, health CE publisher (Topic 9)

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add retry-topic processor that re-forwards queued requests on SP recovery with per-message
 deadlines.
• Add cancel handling that purges matching retry-topic messages and blocks late creates via
 deny-list.
• Extract SP forwarding into a dedicated forwarder supporting stable Idempotency-Key propagation.
• Publish health degraded/unavailable CloudEvents on provider health transitions; wire lifecycle in
 main.
Diagram

graph TD
  CP["Control Plane"] -->|"CE requests"| Router["Router"] -->|"healthy SP"| Forwarder["SP Forwarder"] -->|"HTTP/in-proc"| SP["Service Provider"]
  Router -->|"queue to retry"| RetryStream[("Retry Stream")]
  HealthMonitor(["Health Monitor"]) -->|"on transition"| RetryProcessor["Retry Processor"] -->|"drain & forward"| RetryStream
  RetryProcessor -->|"forward"| Forwarder
  HealthMonitor -->|"on transition"| CEPublisher(["Health CE Publisher"]) -->|"health CEs"| CP
  Router -. "share KeyLock/ResourceSet" .- RetryProcessor
  subgraph Legend
    direction LR
    _svc(["Service/Monitor"]) ~~~ _db[("Stream") ] ~~~ _mod["Module"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Distributed coordination (NATS KV / external lock) for in-flight guard
  • ➕ Would prevent double-dispatch across multiple agent replicas
  • ➕ Makes retry/main coordination robust under horizontal scaling
  • ➖ Adds operational complexity and latency to every dispatch attempt
  • ➖ Unnecessary if the agent remains single-instance per environment
2. Ack+republish instead of Nak-in-place for retry-topic messages
  • ➕ Simplifies message movement model (always republish to reschedule)
  • ➖ Resets delivery count and complicates any delivery-based observability/limits
  • ➖ Adds extra publishes and increases stream churn compared to NakWithDelay

Recommendation: Given a single-instance agent, the PR’s in-process KeyLock/ResourceSet sharing between Router and Retry Processor is the right complexity/performance trade-off and directly targets main-vs-retry races. If/when the agent is scaled horizontally, revisit this design and move coordination to a distributed primitive.

Files changed (93) +15169 / -563

Enhancement (23) +3213 / -141
main.goWire messaging + routing + retry processor + health CE publisher +140/-24

Wire messaging + routing + retry processor + health CE publisher

• Builds and wires the messaging client, Router, Forwarder, Retry Processor, and Health CE publisher; adds startup ordering to run restart draining before live consumption.

cmd/environment-agent/main.go

builder.goAdd CloudEvent builder helpers +33/-0

Add CloudEvent builder helpers

• Adds helper constructors/utilities for producing consistent CloudEvents used by router/retry/health publishing.

internal/cloudevent/builder.go

publish.goAdd CloudEvent publish helper +33/-0

Add CloudEvent publish helper

• Centralizes CloudEvent publishing behavior and error propagation to callers.

internal/cloudevent/publish.go

types.goAdd CloudEvent type/subject constants +29/-0

Add CloudEvent type/subject constants

• Defines shared CE type and subject constants used across routing/messaging/health.

internal/cloudevent/types.go

client.goAdjust DCM client for messaging integration points +18/-14

Adjust DCM client for messaging integration points

• Updates DCM client wiring to work with the new messaging/lag-provider integration.

internal/dcm/client.go

registrar.goWire registrar to real consumer lag provider +74/-18

Wire registrar to real consumer lag provider

• Uses the messaging client as ConsumerLagProvider and updates registrar behavior accordingly.

internal/dcm/registrar.go

cepublisher.goPublish degraded/unavailable health CloudEvents on transitions +88/-0

Publish degraded/unavailable health CloudEvents on transitions

• Implements CEPublisher that resolves provider metadata and publishes health CloudEvents for Unhealthy/Unavailable transitions.

internal/health/cepublisher.go

monitor.goTrack serviceType and improve transition logging +42/-18

Track serviceType and improve transition logging

• Extends Monitor.RegisterProvider to include serviceType and adds structured logging around registration, checks, and transitions.

internal/health/monitor/monitor.go

panic.goAdd panic-to-error helper +0/-16

Add panic-to-error helper

• Introduces a helper for representing panic conditions in HTTP error handling.

internal/httperror/panic.go

problem.goAdd RFC7807-style problem helper +0/-22

Add RFC7807-style problem helper

• Introduces a helper type for structured problem responses.

internal/httperror/problem.go

client.goImplement JetStream messaging client (topics, consumers, publish) +637/-0

Implement JetStream messaging client (topics, consumers, publish)

• Adds a NATS/JetStream client with durable consumer setup, publish helpers (including Msg-Id), reconnect backoff, and controlled shutdown draining.

internal/messaging/client.go

handlers.goAdd main/cancel handler dispatch with deadlines and MaxDeliver guard +213/-0

Add main/cancel handler dispatch with deadlines and MaxDeliver guard

• Implements message dispatch with per-message handler context timeout, panic recovery, MaxDeliver terminal handling (main consumer), and consistent ack/nak behavior.

internal/messaging/handlers.go

topics.goDerive/validate main/cancel/retry subjects and names +146/-0

Derive/validate main/cancel/retry subjects and names

• Adds TopicNames derivation plus validation for NATS subjects and JetStream-safe stream/consumer names.

internal/messaging/topics.go

health_state.goHealth state tracking adjustments +3/-0

Health state tracking adjustments

• Small updates to provider health state representations used by routing/health subsystems.

internal/provider/health_state.go

provider.goProvider registry updates supporting routing resolution +8/-1

Provider registry updates supporting routing resolution

• Adjusts provider registry behavior used by routing resolution and retry processing.

internal/provider/provider.go

service.goPass serviceType into health monitor and adjust registration ordering +53/-21

Pass serviceType into health monitor and adjust registration ordering

• Updates provider service registration/monitor wiring to include serviceType and match new startup ordering constraints.

internal/provider/service/service.go

file.goEnhance file-backed provider store APIs and logging +56/-7

Enhance file-backed provider store APIs and logging

• Adjusts the file store constructor/signature and adds behavior needed by health CE publishing and routing lookups.

internal/provider/store/file.go

forwarder.goIntroduce SP forwarder with Idempotency-Key support +201/-0

Introduce SP forwarder with Idempotency-Key support

• Implements a forwarder abstraction handling external HTTP and embedded SPs, forwarding CE id as Idempotency-Key and logging safely.

internal/routing/forwarder.go

keylock.goAdd transient per-resource in-flight lock +47/-0

Add transient per-resource in-flight lock

• Introduces KeyLock to block concurrent forward attempts for the same resource ID without long-lived blocking.

internal/routing/keylock.go

resourceset.goIntroduce bounded LRU ResourceSet (deny-list/ledger) +110/-0

Introduce bounded LRU ResourceSet (deny-list/ledger)

• Adds a concurrent, bounded, LRU-evicting set used for deny-list and claimed-resources tracking.

internal/routing/resourceset.go

processor.goImplement retry processor (transition + restart flows) +651/-0

Implement retry processor (transition + restart flows)

• Adds Processor that fetches retry messages on provider transitions/restarts, dedups create/delete pairs, forwards with handler deadlines, and acks/naks appropriately.

internal/routing/retry/processor.go

router.goImplement Router with queueing, cancel purge, and retry/backoff forwarding +455/-0

Implement Router with queueing, cancel purge, and retry/backoff forwarding

• Introduces Router with decomposed request parsing, provider resolution, unhealthy queueing to retry topic, cancel deny-list + retry purge, and forward retry/backoff with in-flight locking.

internal/routing/router.go

types.goAdd routing interfaces and CE payload types +176/-0

Add routing interfaces and CE payload types

• Defines SPForwarder/Publisher/RetryTopicConsumer interfaces plus shared payload and error helpers used across router/retry/health.

internal/routing/types.go

Bug fix (1) +16 / -8
backoff.goMake backoff calculation overflow-safe +16/-8

Make backoff calculation overflow-safe

• Replaces float-based exponential backoff with integer doubling capped before overflow.

internal/backoff/backoff.go

Refactor (6) +83 / -7
server.goAdjust server implementation for new subsystem wiring +5/-2

Adjust server implementation for new subsystem wiring

• Small updates to align server behavior with newly introduced components/config.

internal/apiserver/server.go

backoff.goAlign DCM backoff usage with shared backoff changes +4/-1

Align DCM backoff usage with shared backoff changes

• Minor updates related to shared backoff behavior/usage.

internal/dcm/backoff.go

handler.goUpdate handler layer for new routing/messaging flow +11/-4

Update handler layer for new routing/messaging flow

• Small handler adjustments to match new routing/messaging data flow and error handling expectations.

internal/handler/handler.go

checker.goMinor checker-related update +2/-0

Minor checker-related update

• Small change to health checker wiring/definitions to support monitor changes.

internal/health/monitor/checker.go

statemachine.goState machine support update +13/-0

State machine support update

• Small additions supporting the updated monitor behavior and observability.

internal/health/monitor/statemachine.go

resolve.goExtract provider resolution helper +48/-0

Extract provider resolution helper

• Adds ResolveProvider helper used by Router and Retry Processor to determine provider and current health status.

internal/routing/resolve.go

Documentation (10) +2827 / -217
environment-agent.decisions.mdUpdate architecture decisions for routing/retry/cancel semantics +607/-9

Update architecture decisions for routing/retry/cancel semantics

• Documents new and amended design decisions related to retry processing, cancel/deny-list behavior, and concurrency controls.

.ai/decisions/environment-agent.decisions.md

environment-agent.spec.mdUpdate spec for Topic 9 routing/retry/health requirements +705/-143

Update spec for Topic 9 routing/retry/health requirements

• Extends specification content to cover retry processor behavior, deadlines/idempotency expectations, and health-event semantics (with deferred items noted).

.ai/specs/environment-agent.spec.md

2026-07-17-16-28-integration-tests.mdAdd integration test plan cases for messaging/routing/retry +903/-28

Add integration test plan cases for messaging/routing/retry

• Adds new integration-test plan entries and traceability updates for topic 7–9 behaviors.

.ai/test-plans/2026-07-17-16-28-integration-tests.md

2026-07-17-16-28-unit-tests.mdAdd unit test plan cases for routing/messaging primitives +429/-37

Add unit test plan cases for routing/messaging primitives

• Adds new unit-test plan entries and traceability updates for primitives and edge cases (topic validation, locks, etc.).

.ai/test-plans/2026-07-17-16-28-unit-tests.md

api.mdcAdd repository API development rules +25/-0

Add repository API development rules

• Adds Cursor rules to standardize API-related work in this repository.

.cursor/rules/api.mdc

commits.mdcAdd commit message rules +35/-0

Add commit message rules

• Adds Cursor rules for commit hygiene and formatting.

.cursor/rules/commits.mdc

go-patterns.mdcAdd Go implementation guidelines +49/-0

Add Go implementation guidelines

• Adds Cursor rules describing preferred Go patterns used in this codebase.

.cursor/rules/go-patterns.mdc

project.mdcAdd project-wide Cursor rules +33/-0

Add project-wide Cursor rules

• Adds Cursor project rules to align generated and authored changes with repo conventions.

.cursor/rules/project.mdc

testing.mdcAdd testing guidelines +39/-0

Add testing guidelines

• Adds Cursor rules for unit/integration testing conventions.

.cursor/rules/testing.mdc

doc.goAdd retry package documentation +2/-0

Add retry package documentation

• Adds package docs for retry processor subsystem.

internal/routing/retry/doc.go

Other (53) +9030 / -190
.gitignoreUpdate ignored artifacts +11/-3

Update ignored artifacts

• Adds/adjusts ignored files for local tooling and build/test artifacts.

.gitignore

MakefileMinor build/test target tweaks +2/-2

Minor build/test target tweaks

• Small Makefile adjustments to support updated build/test workflows.

Makefile

main_test.goUpdate main package tests for new wiring +87/-2

Update main package tests for new wiring

• Adjusts tests to reflect new startup behavior, dependencies, and invariants.

cmd/environment-agent/main_test.go

main_wiring_test.goAdd wiring test coverage for startup composition +120/-0

Add wiring test coverage for startup composition

• Adds targeted tests ensuring the runtime wiring between messaging/routing/retry/health is correct.

cmd/environment-agent/main_wiring_test.go

go.modAdd NATS + CloudEvents dependencies +17/-0

Add NATS + CloudEvents dependencies

• Introduces JetStream/NATS client/server libraries and CloudEvents SDK required for messaging and CE publishing.

go.mod

go.sumAdd checksums for new dependencies +41/-0

Add checksums for new dependencies

• Updates module checksums for newly introduced dependencies.

go.sum

server_integration_test.goUpdate apiserver integration tests +23/-5

Update apiserver integration tests

• Updates integration tests to match server wiring/behavior changes.

internal/apiserver/server_integration_test.go

backoff_test.goAdd backoff edge-case test coverage +23/-0

Add backoff edge-case test coverage

• Adds tests covering large attempts and boundary conditions for backoff calculation.

internal/backoff/backoff_test.go

builder_test.goTest CloudEvent builder helpers +47/-0

Test CloudEvent builder helpers

• Adds unit tests for CloudEvent construction helpers.

internal/cloudevent/builder_test.go

suite_test.goAdd cloudevent test suite bootstrap +13/-0

Add cloudevent test suite bootstrap

• Adds suite wiring for cloudevent package tests.

internal/cloudevent/suite_test.go

config.goAdd routing/messaging config + optional config-file merge +135/-7

Add routing/messaging config + optional config-file merge

• Introduces RoutingConfig, expands MessagingConfig, supports AGENT_CONFIG_FILE merge, and adds cross-field invariant validation for handler timeout vs ack wait.

internal/config/config.go

config_test.goTest config defaults, validation, and config-file loading +258/-19

Test config defaults, validation, and config-file loading

• Adds tests for new config fields, validation ranges, and file-vs-env precedence behavior.

internal/config/config_test.go

backoff_unit_test.goAdd DCM backoff unit tests +17/-0

Add DCM backoff unit tests

• Adds test coverage for DCM backoff behavior.

internal/dcm/backoff_unit_test.go

registrar_integration_test.goUpdate registrar integration tests +321/-27

Update registrar integration tests

• Adjusts integration tests for registrar changes and messaging-backed lag reporting.

internal/dcm/registrar_integration_test.go

cepublisher_test.goTest health CE publisher +237/-0

Test health CE publisher

• Adds unit tests covering type selection, store lookup failures, and publish error handling.

internal/health/cepublisher_test.go

health_integration_test.goUpdate health integration tests for CE publishing +33/-69

Update health integration tests for CE publishing

• Updates integration tests to account for health CE publication on transitions.

internal/health/health_integration_test.go

monitor_test.goUpdate monitor tests +234/-9

Update monitor tests

• Adds/updates tests for serviceType-aware registration and transition behavior.

internal/health/monitor/monitor_test.go

problem_unit_test.goUnit tests for problem helper +0/-28

Unit tests for problem helper

• Adds tests for the new problem response helper behavior.

internal/httperror/problem_unit_test.go

client_backoff_test.goTest messaging reconnect backoff behavior +78/-0

Test messaging reconnect backoff behavior

• Adds unit tests validating reconnect delay/backoff calculations.

internal/messaging/client_backoff_test.go

client_drain_test.goTest messaging Stop() drain behavior +65/-0

Test messaging Stop() drain behavior

• Adds tests ensuring in-flight handlers are drained before shutdown completes.

internal/messaging/client_drain_test.go

client_integration_test.goEnd-to-end tests for messaging client against NATS +1326/-0

End-to-end tests for messaging client against NATS

• Adds integration tests covering consumer setup, publish/consume flows, and shutdown behavior.

internal/messaging/client_integration_test.go

client_msgid_wire_test.goTest Msg-Id header wiring for dedup +104/-0

Test Msg-Id header wiring for dedup

• Verifies PublishWithMsgID sets JetStream deduplication headers correctly.

internal/messaging/client_msgid_wire_test.go

client_onsetupready_test.goTest OnSetupReady ordering semantics +190/-0

Test OnSetupReady ordering semantics

• Verifies deferred consumption starts only after setup callbacks run (used for restart draining).

internal/messaging/client_onsetupready_test.go

client_setup_test.goTest consumer/stream setup logic +108/-0

Test consumer/stream setup logic

• Covers durable consumer creation and startup-race tolerance for CP-owned stream existence.

internal/messaging/client_setup_test.go

handlers_test.goTest messaging handler ack/nak/panic behavior +769/-0

Test messaging handler ack/nak/panic behavior

• Adds extensive tests for handler execution, deadline handling, MaxDeliver, and error paths.

internal/messaging/handlers_test.go

router_integration_test.goIntegration tests for messaging ↔ router wiring +449/-0

Integration tests for messaging ↔ router wiring

• Validates that consumed messages invoke router handlers and produce expected outcomes.

internal/messaging/router_integration_test.go

suite_test.goAdd messaging test suite bootstrap +67/-0

Add messaging test suite bootstrap

• Adds suite wiring for messaging package tests.

internal/messaging/suite_test.go

topics_test.goTest topic derivation and validation +96/-0

Test topic derivation and validation

• Adds unit tests for topic naming rules and edge cases.

internal/messaging/topics_test.go

health_monitor_integration_test.goUpdate provider health monitor integration test coverage +3/-12

Update provider health monitor integration test coverage

• Updates integration tests to align with serviceType-aware health monitor registration.

internal/provider/health_monitor_integration_test.go

health_state_test.goTest provider health state changes +10/-0

Test provider health state changes

• Adds unit tests for provider health state behavior.

internal/provider/health_state_test.go

provider_integration_test.goUpdate provider integration tests +208/-3

Update provider integration tests

• Expands/updates integration tests for provider changes relied on by routing/retry.

internal/provider/provider_integration_test.go

provider_unit_test.goAdd/update provider unit tests +29/-0

Add/update provider unit tests

• Adds unit-level coverage for provider registry/service behaviors.

internal/provider/provider_unit_test.go

service_test.goUpdate provider service tests +557/-2

Update provider service tests

• Updates tests to reflect service registration and health monitor wiring changes.

internal/provider/service/service_test.go

file_durability_test.goAdd file store durability tests +91/-0

Add file store durability tests

• Adds tests validating persistence/durability expectations for the file-backed store.

internal/provider/store/file_durability_test.go

file_test.goUpdate file store unit tests +77/-0

Update file store unit tests

• Adds/updates unit tests for updated file store behavior.

internal/provider/store/file_test.go

suite_test.goAdd provider store test suite bootstrap +13/-0

Add provider store test suite bootstrap

• Adds suite wiring for provider store package tests.

internal/provider/store/suite_test.go

backoff_routing_test.goAdd routing retry/backoff tests +27/-0

Add routing retry/backoff tests

• Adds targeted tests for routing retry/backoff behavior on transient SP errors.

internal/routing/backoff_routing_test.go

concurrency_race_test.goAdd routing concurrency race tests +90/-0

Add routing concurrency race tests

• Adds tests covering main-topic vs retry-topic races and in-flight dispatch prevention.

internal/routing/concurrency_race_test.go

forwarder_test.goTest SP forwarder behavior +439/-0

Test SP forwarder behavior

• Adds tests for HTTP request construction, embedded routing, and error handling.

internal/routing/forwarder_test.go

keylock_test.goTest KeyLock primitive +37/-0

Test KeyLock primitive

• Adds unit tests for KeyLock AddIfAbsent/Remove/Len behavior.

internal/routing/keylock_test.go

resourceset_test.goTest ResourceSet behavior +71/-0

Test ResourceSet behavior

• Adds tests for LRU behavior, consume semantics, and set operations.

internal/routing/resourceset_test.go

deadline_integration_test.goRetry deadline integration tests +122/-0

Retry deadline integration tests

• Adds integration coverage ensuring handler timeout cancels hung forwarding during retry processing.

internal/routing/retry/deadline_integration_test.go

idempotency_integration_test.goRetry idempotency integration tests +115/-0

Retry idempotency integration tests

• Adds integration coverage validating stable Idempotency-Key behavior across retry forwards.

internal/routing/retry/idempotency_integration_test.go

inflight_integration_test.goRetry/main in-flight coordination integration tests +119/-0

Retry/main in-flight coordination integration tests

• Adds integration tests validating shared KeyLock prevents double-dispatch between router and retry processor.

internal/routing/retry/inflight_integration_test.go

nak_semantics_integration_test.goRetry Nak-in-place integration tests +125/-0

Retry Nak-in-place integration tests

• Adds integration tests asserting retry-topic messages are Nak’d in place rather than republished.

internal/routing/retry/nak_semantics_integration_test.go

processor_integration_test.goRetry processor integration tests +371/-0

Retry processor integration tests

• Adds end-to-end coverage for retry draining and transition-triggered processing.

internal/routing/retry/processor_integration_test.go

startup_integration_test.goRetry processor startup drain tests +191/-0

Retry processor startup drain tests

• Adds tests covering restart drain ordering and behavior before live consumption begins.

internal/routing/retry/startup_integration_test.go

suite_test.goAdd retry package test suite bootstrap +62/-0

Add retry package test suite bootstrap

• Adds suite wiring for retry processor tests.

internal/routing/retry/suite_test.go

router_publish_logging_test.goTest router publish logging/outcomes +91/-0

Test router publish logging/outcomes

• Adds tests ensuring CE publish results are logged consistently and safely.

internal/routing/router_publish_logging_test.go

routing_integration_test.goRouting integration test suite +926/-0

Routing integration test suite

• Adds broad integration coverage for create/delete/cancel flows including unhealthy queueing and retry paths.

internal/routing/routing_integration_test.go

helpers.goAdd routing test helpers and fakes +336/-0

Add routing test helpers and fakes

• Adds shared fakes/helpers used by routing and retry integration tests.

internal/routing/routingtest/helpers.go

suite_test.goAdd routing package test suite bootstrap +41/-0

Add routing package test suite bootstrap

• Adds suite wiring for routing tests.

internal/routing/suite_test.go

shutdown_test.goAdjust e2e shutdown test for new startup/shutdown sequence +8/-2

Adjust e2e shutdown test for new startup/shutdown sequence

• Updates e2e shutdown behavior expectations to match messaging drain and component lifecycle ordering.

test/e2e/httpserver/shutdown_test.go

Fixes 4 issues flagged in qodo's review of PR dcm-project#19:
- Start's synchronous-connect path ran doSetup inline, so Start could
  block up to 30s waiting on the control-plane's request stream despite
  being documented as non-blocking (REQ-MSG-110). Now runs in a
  goroutine; added WaitUntilReady so main.go can bound its wait for
  JetStream readiness before RegisterEmbedded without reintroducing a
  long startup block.
- handleCancelMessage's panic recovery called Term() instead of Nak(),
  permanently dropping a cancel message on a transient handler bug even
  though the cancel consumer intentionally has no MaxDeliver limit.
- fetchAllFromConsumer masked genuine Fetch/MessageBatch errors as the
  expected FetchMaxWait timeout, hiding real outages as "done fetching".
- handleCancelMessage had no deadline on cancelHandler, so a hung
  retry-topic purge or CE publish could block the cancel consumer
  indefinitely. Added a separately configurable CancelHandlerTimeout
  (bounded below CancelAckWait, mirroring the main-path invariant).

Several integration tests asserted durable consumers/streams exist
synchronously right after Start() returns, which the Start fix above
breaks; updated them to poll (Eventually) instead, per AC-RCM-047's
pre-existing "must not depend on synchronous setup" guarantee. No
assertion was weakened, only the timing assumption was corrected.

Assisted by: Cursor - Sonnet 5

Signed-off-by: gabriel-farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@jordigilh jordigilh left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the full history across topics 7-9: all 4 Qodo bot findings, gciavarrini's spec-consistency pass, and jordigilh's test-coverage/design threads were addressed with concrete fixes and follow-up tests. Confirmed the key correctness fixes landed as described (lazy JetStream resolution via JSProvider, in-place Nak preserving NumDelivered on cancel purge, KeyLock/in-flight guard, Idempotency-Key forwarding). Left a note on the outstanding testing.mdc rewrite ask to track it as pending follow-up work, not a blocker. LGTM.

@gciavarrini

Copy link
Copy Markdown
Contributor

Merging this PR.

@jordigilh i don't see the not on testing.mdc can you please double check? so we can track the work :)

@gciavarrini
gciavarrini merged commit 46e0e11 into dcm-project:main Aug 19, 2026
6 checks passed
@gabriel-farache
gabriel-farache deleted the feat/topic9-retry-cancel-mechanisms branch September 3, 2026 09:49
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.

4 participants