Skip to content

🐛 Fix agent watcher store write race that erases DeletionTimestamp - #233

Open
ciaranRoche wants to merge 1 commit into
open-cluster-management-io:mainfrom
ciaranRoche:agent-store-write-race
Open

🐛 Fix agent watcher store write race that erases DeletionTimestamp#233
ciaranRoche wants to merge 1 commit into
open-cluster-management-io:mainfrom
ciaranRoche:agent-store-write-race

Conversation

@ciaranRoche

@ciaranRoche ciaranRoche commented Aug 12, 2026

Copy link
Copy Markdown

Fixes #232

What this does

Closes the write race between ManifestWorkAgentClient.Patch and the agent watcher store's HandleReceivedResource that erases DeletionTimestamp from the agent's in-memory store and resurrects deleted ManifestWorks on the spoke (full analysis and log timeline in #232, in the wild report in open-cluster-management-io/ocm#1404).

Three changes:

  1. Store-level mutex. Both the generic AgentInformerWatcherStore[T] and the work agent store now serialize their writers, and HandleReceivedResource holds the lock for its whole read-modify-write sequence, so another writer can't be interleaved between reading the cached resource and writing it back.
  2. ConditionalUpdater capability. A new optional interface in clients/store with one method, UpdateWithVersion(ctx, resource, expectedResourceVersion). The work agent store implements it as a single critical section: get, compare the resource version for equality, bump the versioner, write. Equality (rather than the < check in versionCompare) also rejects stale writes after a delete/re-add cycle resets the per-name versioner, which was a second latent bug in the same code path.
  3. Patch uses the capability when available. The final store write in Patch type-asserts for ConditionalUpdater and uses it, keeping the existing Get/versionCompare/Update as the fallback for stores that don't implement it, so ClientWatcherStore itself is unchanged and other implementations keep working as before.

The CloudEvents publish stays outside the lock, so the lock is only ever held for an in-memory map operation, there's no contention on the network path. On conflict the caller retries and re-derives the patch from the fresh store state (which now includes the deletion timestamp), so nothing is lost.

Tests

  • TestManifestWorkAgentClient_ConcurrentStatusPatchAndDeleteEvent reproduces the production failure. On current main it fails within a few iterations:

    --- FAIL: TestManifestWorkAgentClient_ConcurrentStatusPatchAndDeleteEvent (0.14s)
        manifestwork_race_test.go:225: iteration 34: the deletion timestamp was lost from the store
    

    With this change it passes reliably under -race.

  • TestManifestWorkAgentClient_Patch_DeleteEventDuringStatusPublish deterministically interleaves a delete event while the status publish is in flight (via a publish hook) and asserts the stale patch gets a conflict and the deletion timestamp survives.

  • TestAgentInformerWatcherStore_UpdateWithVersion covers the CAS semantics: match, stale, force with "0", empty version, missing work, the versioner reset case, and delete-event-wins.

go test -race ./pkg/cloudevents/... is green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01USPzrSgidrmV9YrxRRf5tQ

Summary by CodeRabbit

  • Bug Fixes
    • Prevented concurrent resource updates from overwriting one another.
    • Added atomic version checks to detect stale updates and return conflicts.
    • Preserved deletion information when status updates race with resource deletion.
    • Improved handling of missing resources and forced updates.
  • Reliability
    • Strengthened event and cache consistency during simultaneous resource changes.
    • Added coverage for concurrent updates, deletion races, version conflicts, and resource recreation.

A delete event applied by HandleReceivedResource while a status patch is
in flight could be overwritten by the patch's final store write, because
the version check and the write in ManifestWorkAgentClient.Patch are not
atomic with respect to the store writers. The work then loses its
DeletionTimestamp in the agent's in-memory store and the work controller
recreates the deleted ManifestWork on the spoke on its next requeue.

Serialize the agent watcher store writers with a store-level mutex, make
HandleReceivedResource's read-modify-write atomic, and add an optional
ConditionalUpdater capability (UpdateWithVersion) that performs the
version check and the write in one critical section. The version check
requires equality, which also rejects stale writes after the per-name
versioner is reset by a delete/re-add cycle. Patch uses the capability
when the store provides it and falls back to the previous behaviour
otherwise, so the ClientWatcherStore interface is unchanged.

Fixes open-cluster-management-io#232

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USPzrSgidrmV9YrxRRf5tQ
Signed-off-by: Ciaran Roche <croche@redhat.com>
@openshift-ci
openshift-ci Bot requested review from deads2k and tesshuflower August 12, 2026 10:05
@openshift-ci

openshift-ci Bot commented Aug 12, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: ciaranRoche
Once this PR has been reviewed and has the lgtm label, please assign qiujian16 for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 63c5876c-58b6-4a4d-9d81-f2fff078e623

📥 Commits

Reviewing files that changed from the base of the PR and between c812bc0 and dabfbd4.

📒 Files selected for processing (6)
  • pkg/cloudevents/clients/store/informer.go
  • pkg/cloudevents/clients/store/interface.go
  • pkg/cloudevents/clients/work/agent/client/manifestwork.go
  • pkg/cloudevents/clients/work/agent/client/manifestwork_race_test.go
  • pkg/cloudevents/clients/work/store/informer.go
  • pkg/cloudevents/clients/work/store/informer_conditionalupdate_test.go

Walkthrough

The change serializes watcher-store mutations, adds atomic conditional updates, and updates ManifestWork patching to use resource-version checks. New tests cover stale updates, deletion races, version resets, forced updates, and concurrent status patches.

Changes

Atomic update flow

Layer / File(s) Summary
Serialize watcher-store mutations
pkg/cloudevents/clients/store/informer.go, pkg/cloudevents/clients/work/store/informer.go
Mutexes protect store writes and received-resource read-modify-write operations. Internal helpers prevent nested locking.
Add conditional store updates
pkg/cloudevents/clients/store/interface.go, pkg/cloudevents/clients/work/store/informer.go, pkg/cloudevents/clients/work/store/informer_conditionalupdate_test.go
The ConditionalUpdater interface and implementation validate resource versions and return conflict, not-found, and internal errors. Tests cover update, force, reset, and deletion cases.
Use atomic updates in ManifestWork patches
pkg/cloudevents/clients/work/agent/client/manifestwork.go, pkg/cloudevents/clients/work/agent/client/manifestwork_race_test.go
Patch uses conditional updates when supported. Race tests verify that deletion timestamps remain intact during concurrent status publication.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • Issue 232: The changes implement the proposed fix for stale patches overwriting deletion timestamps or recreated resources.

Possibly related PRs

Suggested labels: approved, lgtm

Suggested reviewers: deads2k

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the bug fix for the agent watcher store write race that erases DeletionTimestamp.
Description check ✅ Passed The description explains the race, implementation, compatibility behavior, tests, and related issue, but uses What this does instead of Summary.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mikeshng mikeshng changed the title Fix agent watcher store write race that erases DeletionTimestamp 🐛 Fix agent watcher store write race that erases DeletionTimestamp Aug 12, 2026

@mikeshng mikeshng left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The good thing about this PR is only impacting users that uses the cloudevent stuffs and within that only the ManifestWork cloudevent. So most likely the impact is only Maestro so regression possibility for OCM-io kube driver (which to my knowledge 99% of users are on) is literally none.

This PR is also functionally correct but I am worry about the performance impact of introducing the lock/sync. One of the main goals of Maestro to my knowledge is performance so this might impact it depending on usage.

I will leave it to the maintainers to make the call.

/assign @jnpacker @qiujian16 @tesshuflower

}

func (s *AgentInformerWatcherStore[T]) Add(resource runtime.Object) error {
s.mu.Lock()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I do not think work agent use this store. Do you need to update this generic store?

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.

Agent watcher store race: in-flight Patch can erase DeletionTimestamp and resurrect deleted ManifestWorks on the spoke

5 participants