feat: implement messaging, routing, and retry/cancel mechanisms (topics 7-9) - #19
Conversation
|
/agentic_review |
Code Review by Qodo
1.
|
a7086d9 to
12f4e92
Compare
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 12f4e92 |
5eef49c to
014640f
Compare
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>
dae70a3 to
02a177a
Compare
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>
02a177a to
91aa518
Compare
gciavarrini
left a comment
There was a problem hiding this comment.
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.
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>
…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>
…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>
|
Code review by qodo was updated up to the latest commit 8ee6fd0 |
PR Summary by Qodofeat(routing): retry processor, cancel purge, SP forwarder, health CE publisher (Topic 9)
AI Description
Diagram
High-Level Assessment
Files changed (93)
|
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
left a comment
There was a problem hiding this comment.
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.
|
Merging this PR. @jordigilh i don't see the not on |
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
NewCloudEvent,FormatSource) for spec-compliant CloudEventsDeriveTopicNames)Topic 8 — Resource operation routing
AddIfAbsent, bounded deny-list, payload validation, sanitized CE error details)Topic 9 — Retry processor and cancel mechanisms
Idempotency-Keyforwardingdcm.agent.health.service-type-degraded/-unavailableCloudEvents on SP state transitionsDenyList→ResourceSet,RouterDeps, retry-consumer lifecycle wiring)KeyLock) serializing concurrent forward attempts perresource_id(main-topic vs. retry-topic races)