fix: republish delete events for resources stuck deleting (HYPERFLEET-1497) - #576
Conversation
…-1497) An upstream race in the OCM SDK agent store (sdk-go issue 232) can erase the DeletionTimestamp from the maestro-agent's in-memory store when a delete event lands while a status patch is in flight. The agent then recreates the deleted ManifestWork on the spoke and never reports Deleted=true, so the server's hard-delete never fires and cluster deletion hangs until an agent reconnect happens to trigger a spec resync (observed 16+ hours, ARO-28958). Before 3717fc6 (AROSLSRE-1547) every retried delete request enqueued a fresh delete event, and each re-published delete_request re-stamped the DeletionTimestamp in the agent store, accidentally healing the race within one retry cycle. That change made retries silent no-ops, which removed the healing along with the event storm. Restore the healing deliberately, with a hard cap: when MarkAsDeleting is called for a resource already soft-deleted, re-enqueue one delete event at most once per delete-event-republish-interval (default 60s, 0 disables) per resource. The check-then-create runs under the existing per-resource advisory lock, so the cap holds across replicas, and the throttle guarantees retries can never accumulate events without bound. The stale-delete detector now only retires delete events that are themselves older than the threshold, so a fresh healing event for a long soft-deleted resource gets a full threshold of delivery attempts instead of being retired within one detector tick. This is a bounded mitigation while the upstream fix (sdk-go PR 233) is in review; it also heals any future agent-side loss of deletion state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USPzrSgidrmV9YrxRRf5tQ
WalkthroughThe change adds a configurable delete-event republish interval. Resource deletion retries now republish stale delete events, and reconciliation preserves fresh events. DAO, service, unit, and integration tests cover the behavior. ChangesDelete event republish flow
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant ResourceService
participant EventService
participant EventDao
ResourceService->>EventService: FindLatestDeleteEvent(sourceID)
EventService->>EventDao: FindLatestDeleteEvent(sourceID)
EventDao-->>EventService: Latest delete event or none
EventService-->>ResourceService: Lookup result
ResourceService->>EventService: Create delete event when interval elapsed
🚥 Pre-merge checks | ✅ 9 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (9 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
test/integration/resource_test.go (1)
680-684: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAssert that the fresh event remains unreconciled.
The final count does not identify which event remains. A regression that reconciles the fresh healing event and keeps the old event can still produce a count of one.
Load both delete events after reconciliation. Assert that the old event has
ReconciledDateset and the fresh event hasReconciledDate == nil.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/integration/resource_test.go` around lines 680 - 684, Update the reconciliation assertions after ReconcileStaleDeleteEvents in the integration test to load both delete events and verify their individual states: the 2-hour-old event must have ReconciledDate set, while the fresh healing event must retain a nil ReconciledDate. Keep the existing count and pending-delete assertions as appropriate.pkg/config/event_server_test.go (1)
28-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest flag values that change behavior.
The cases only validate constructor defaults. They never set
delete-event-republish-interval. A broken flag binding or an incorrect zero-disable value can pass.Add cases for a positive override and
0, then assertDeleteEventRepublishInterval.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/config/event_server_test.go` around lines 28 - 65, Extend the EventServerConfig constructor tests to cover the delete-event-republish-interval input: add one case with a positive override and another with 0, and assert the corresponding DeleteEventRepublishInterval values in each expected EventServerConfig. Keep the existing default cases unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@pkg/config/event_server_test.go`:
- Around line 28-65: Extend the EventServerConfig constructor tests to cover the
delete-event-republish-interval input: add one case with a positive override and
another with 0, and assert the corresponding DeleteEventRepublishInterval values
in each expected EventServerConfig. Keep the existing default cases unchanged.
In `@test/integration/resource_test.go`:
- Around line 680-684: Update the reconciliation assertions after
ReconcileStaleDeleteEvents in the integration test to load both delete events
and verify their individual states: the 2-hour-old event must have
ReconciledDate set, while the fresh healing event must retain a nil
ReconciledDate. Keep the existing count and pending-delete assertions as
appropriate.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: openshift-online/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c4cfc8f8-e79d-4568-8fb9-b4533fc3182c
📒 Files selected for processing (10)
cmd/maestro/environments/service_types.gopkg/config/event_server.gopkg/config/event_server_test.gopkg/controllers/undelivered_detector_test.gopkg/dao/event.gopkg/dao/mocks/event.gopkg/services/event.gopkg/services/resource.gopkg/services/resource_test.gotest/integration/resource_test.go
What
Restores healing for resources stuck soft-deleted, with a hard cap. When
MarkAsDeletingis called for a resource whose deletion is already in flight, we now re-enqueue the delete event, throttled to at most one per--delete-event-republish-interval(default 60s, 0 disables) per resource. The check-then-create runs under the existing per-resource advisory lock, so the cap holds across replicas.The stale-delete detector is adjusted to match: it now only retires delete events that are themselves older than the threshold, so a fresh healing event for a long soft-deleted resource gets a full threshold of delivery attempts instead of being retired within one detector tick.
Why
An upstream race in the OCM SDK agent store (sdk-go#232) can erase the DeletionTimestamp from the maestro-agent's in-memory store when a delete event lands while a status patch is in flight. The agent then recreates the deleted ManifestWork on the spoke and never reports
Deleted=true, so the server's hard-delete never fires and cluster deletion hangs until an agent reconnect happens to trigger a spec resync. We observed 16+ hours across four e2e CI runs (ARO-28958, HYPERFLEET-1497).Before #562 (AROSLSRE-1547) every retried delete request enqueued a fresh delete event, and each re-published
delete_requestre-stamped the DeletionTimestamp in the agent store, accidentally healing the race within one retry cycle. #562 made retries silent no-ops for a good reason (the event storm was starving the spec-event worker), but that also removed the healing. This change brings the healing back deliberately: the throttle guarantees retries can never accumulate events without bound, so the AROSLSRE-1547 fix is preserved. Worst case accumulation per stuck resource is threshold/interval unreconciled rows (60 at defaults) while its agent is disconnected.This is a bounded mitigation while the upstream fix (sdk-go#233) is in review, and it also heals any future agent-side loss of deletion state. One caveat to be aware of: healing is driven by the client retrying deletes, which CS destructors do. Once the agent confirms the delete the resource row is hard-deleted and republishing stops naturally.
Testing
TestMarkAsDeletingRepublishesStaleDeleteEvent,TestMarkAsDeletingRepublishDisabled, and the existingTestMarkAsDeletingIsIdempotent(unchanged assertions, now also validates the throttle window). The mock event DAO now stampsCreatedAton create, mirroring gorm's autoCreateTime.TestMarkAsDeletingRepublishThrottle(throttle + republish after backdating the event row) andTestReconcileStaleDeleteEventsSparesFreshHealingEvents(detector retires a 2h-old event but spares the fresh healing event on a 2h soft-deleted resource). ExistingTestReconcileStaleDeleteEventspasses unchanged.Related: HYPERFLEET-1497, ARO-28958, upstream root cause sdk-go#232 / fix sdk-go#233, same symptoms in the wild ocm#1404.
Note for reviewers: this touches the same
MarkAsDeletingearly-returns as the pending HYPERFLEET-1496 logging PR (#575), whichever lands second is a trivial rebase.🤖 Generated with Claude Code
https://claude.ai/code/session_01USPzrSgidrmV9YrxRRf5tQ
Summary by CodeRabbit