Skip to content

Commit 3e85103

Browse files
qiujian16claude
andcommitted
Use local resource version management in agent client
This commit introduces local resource version management to prevent race conditions in the CloudEvents agent client, particularly in the manifestwork Patch operations. Key changes: - Add local versioner in AgentInformerWatcherStore to track resource versions independently from source versions - Clear resource version when publishing status updates to source, allowing source to manage its own version tracking - Refactor Patch method to handle status updates and deletions consistently, extracting version comparison logic into dedicated function - Support empty resource version strings throughout the codebase (codec, agent client, resync operations) to enable separate local and remote versioning - Add SetResourceVersion to ResourceObject interface for version management - Add comprehensive tests for new functionality including race condition tests The local versioner increments on each Add/Update operation and is cleared on Delete, providing monotonic version tracking for conflict detection without depending on source resource versions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Jian Qiu <jqiu@redhat.com>
1 parent b492d8b commit 3e85103

9 files changed

Lines changed: 1663 additions & 129 deletions

File tree

pkg/cloudevents/clients/work/agent/client/manifestwork.go

Lines changed: 90 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package client
33
import (
44
"context"
55
"fmt"
6+
"k8s.io/apimachinery/pkg/api/meta"
67
"net/http"
78
"strconv"
89
"sync"
910

1011
"k8s.io/apimachinery/pkg/api/errors"
11-
"k8s.io/apimachinery/pkg/api/meta"
1212
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1313
kubetypes "k8s.io/apimachinery/pkg/types"
1414
"k8s.io/apimachinery/pkg/watch"
@@ -41,7 +41,7 @@ type ManifestWorkAgentClient struct {
4141
var _ workv1client.ManifestWorkInterface = &ManifestWorkAgentClient{}
4242

4343
func NewManifestWorkAgentClient(
44-
clusterName string,
44+
_ string,
4545
watcherStore store.ClientWatcherStore[*workv1.ManifestWork],
4646
cloudEventsClient generic.CloudEventsClient[*workv1.ManifestWork],
4747
) *ManifestWorkAgentClient {
@@ -126,22 +126,39 @@ func (c *ManifestWorkAgentClient) Watch(ctx context.Context, opts metav1.ListOpt
126126

127127
func (c *ManifestWorkAgentClient) Patch(ctx context.Context, name string, pt kubetypes.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *workv1.ManifestWork, err error) {
128128
klog.V(4).Infof("patching manifestwork %s/%s", c.namespace, name)
129+
130+
// avoid race conditions among the agent's go routines
131+
c.Lock()
132+
defer c.Unlock()
133+
134+
var returnErr *errors.StatusError
135+
defer func() {
136+
if returnErr != nil {
137+
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
138+
} else {
139+
metrics.IncreaseWorkProcessedCounter("patch", metav1.StatusSuccess)
140+
}
141+
}()
142+
143+
if len(subresources) != 0 && !utils.IsStatusPatch(subresources) {
144+
msg := fmt.Sprintf("unsupported subresources %v", subresources)
145+
returnErr = errors.NewGenericServerResponse(http.StatusMethodNotAllowed, "patch", common.ManifestWorkGR, name, msg, 0, false)
146+
return nil, returnErr
147+
}
148+
129149
lastWork, exists, err := c.watcherStore.Get(c.namespace, name)
130150
if err != nil {
131-
returnErr := errors.NewInternalError(err)
132-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
151+
returnErr = errors.NewInternalError(err)
133152
return nil, returnErr
134153
}
135154
if !exists {
136-
returnErr := errors.NewNotFound(common.ManifestWorkGR, name)
137-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
155+
returnErr = errors.NewNotFound(common.ManifestWorkGR, name)
138156
return nil, returnErr
139157
}
140158

141159
patchedWork, err := utils.Patch(pt, lastWork, data)
142160
if err != nil {
143-
returnErr := errors.NewInternalError(err)
144-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
161+
returnErr = errors.NewInternalError(err)
145162
return nil, returnErr
146163
}
147164

@@ -155,93 +172,43 @@ func (c *ManifestWorkAgentClient) Patch(ctx context.Context, name string, pt kub
155172
eventType := types.CloudEventsType{
156173
CloudEventsDataType: *eventDataType,
157174
SubResource: types.SubResourceStatus,
175+
Action: types.UpdateRequestAction,
176+
}
177+
178+
// we first compare local resource version
179+
if returnErr = versoinCompare(patchedWork, lastWork); returnErr != nil {
180+
return nil, returnErr
158181
}
159182

160183
newWork := patchedWork.DeepCopy()
161184

162-
if utils.IsStatusPatch(subresources) {
163-
// avoid race conditions among the agent's go routines
164-
c.Lock()
165-
defer c.Unlock()
185+
isDeleted := !newWork.DeletionTimestamp.IsZero() && len(newWork.Finalizers) == 0
166186

167-
eventType.Action = types.UpdateRequestAction
168-
// publish the status update event to source, source will check the resource version
169-
// and reject the update if it's status update is outdated.
170-
if err := c.cloudEventsClient.Publish(ctx, eventType, newWork); err != nil {
171-
returnErr := cloudeventserrors.ToStatusError(common.ManifestWorkGR, name, err)
172-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
173-
return nil, returnErr
187+
if utils.IsStatusPatch(subresources) || isDeleted {
188+
if isDeleted {
189+
meta.SetStatusCondition(&newWork.Status.Conditions, metav1.Condition{
190+
Type: common.ResourceDeleted,
191+
Status: metav1.ConditionTrue,
192+
Reason: "ManifestsDeleted",
193+
Message: fmt.Sprintf("The manifests are deleted from the cluster %s", newWork.Namespace),
194+
})
174195
}
175196

176-
// Fetch the latest work from the store and verify the resource version to avoid updating the store
177-
// with outdated work. Return a conflict error if the resource version is outdated.
178-
// Due to the lack of read-modify-write guarantees in the store, race conditions may occur between
179-
// this update operation and one from the agent informer after receiving the event from the source.
180-
latestWork, exists, err := c.watcherStore.Get(c.namespace, name)
181-
if err != nil {
182-
returnErr := errors.NewInternalError(err)
183-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
184-
return nil, returnErr
185-
}
186-
if !exists {
187-
returnErr := errors.NewNotFound(common.ManifestWorkGR, name)
188-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
189-
return nil, returnErr
190-
}
191-
lastResourceVersion, err := strconv.ParseInt(latestWork.GetResourceVersion(), 10, 64)
192-
if err != nil {
193-
returnErr := errors.NewInternalError(err)
194-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
195-
return nil, returnErr
196-
}
197-
newResourceVersion, err := strconv.ParseInt(newWork.GetResourceVersion(), 10, 64)
198-
if err != nil {
199-
returnErr := errors.NewInternalError(err)
200-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
201-
return nil, returnErr
202-
}
203-
// ensure the resource version of the work is not outdated
204-
if newResourceVersion < lastResourceVersion {
205-
// It's safe to return a conflict error here, even if the status update event
206-
// has already been sent. The source may reject the update due to an outdated resource version.
207-
returnErr := errors.NewConflict(common.ManifestWorkGR, name, fmt.Errorf("the resource version of the work is outdated"))
208-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
209-
return nil, returnErr
210-
}
211-
if err := c.watcherStore.Update(newWork); err != nil {
212-
returnErr := errors.NewInternalError(err)
213-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
197+
// Set work's resource version to remote resource version for publishing
198+
workToPublish := newWork.DeepCopy()
199+
workToPublish.ResourceVersion = ""
200+
201+
// publish the status update event to source, source will check the resource version
202+
// and reject the update if it's status update is outdated.
203+
if err := c.cloudEventsClient.Publish(ctx, eventType, workToPublish); err != nil {
204+
returnErr = cloudeventserrors.ToStatusError(common.ManifestWorkGR, name, err)
214205
return nil, returnErr
215206
}
216-
217-
metrics.IncreaseWorkProcessedCounter("patch", metav1.StatusSuccess)
218-
return newWork, nil
219-
}
220-
221-
if len(subresources) != 0 {
222-
msg := fmt.Sprintf("unsupported subresources %v", subresources)
223-
returnErr := errors.NewGenericServerResponse(http.StatusMethodNotAllowed, "patch", common.ManifestWorkGR, name, msg, 0, false)
224-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
225-
return nil, returnErr
226207
}
227208

228209
// the finalizers of a deleting manifestwork are removed, marking the manifestwork status to deleted and sending
229210
// it back to source
230-
if !newWork.DeletionTimestamp.IsZero() && len(newWork.Finalizers) == 0 {
231-
meta.SetStatusCondition(&newWork.Status.Conditions, metav1.Condition{
232-
Type: common.ResourceDeleted,
233-
Status: metav1.ConditionTrue,
234-
Reason: "ManifestsDeleted",
235-
Message: fmt.Sprintf("The manifests are deleted from the cluster %s", newWork.Namespace),
236-
})
237-
238-
eventType.Action = types.UpdateRequestAction
239-
if err := c.cloudEventsClient.Publish(ctx, eventType, newWork); err != nil {
240-
returnErr := cloudeventserrors.ToStatusError(common.ManifestWorkGR, name, err)
241-
metrics.IncreaseWorkProcessedCounter("delete", string(returnErr.ErrStatus.Reason))
242-
return nil, returnErr
243-
}
244-
211+
if isDeleted {
245212
if err := c.watcherStore.Delete(newWork); err != nil {
246213
returnErr := errors.NewInternalError(err)
247214
metrics.IncreaseWorkProcessedCounter("delete", string(returnErr.ErrStatus.Reason))
@@ -252,12 +219,49 @@ func (c *ManifestWorkAgentClient) Patch(ctx context.Context, name string, pt kub
252219
return newWork, nil
253220
}
254221

222+
// Fetch the latest work from the store and verify the resource version to avoid updating the store
223+
// with outdated work. Return a conflict error if the resource version is outdated.
224+
// Due to the lack of read-modify-write guarantees in the store, race conditions may occur between
225+
// this update operation and one from the agent informer after receiving the event from the source.
226+
latestWork, exists, err := c.watcherStore.Get(c.namespace, name)
227+
if err != nil {
228+
returnErr = errors.NewInternalError(err)
229+
return nil, returnErr
230+
}
231+
if !exists {
232+
returnErr = errors.NewNotFound(common.ManifestWorkGR, name)
233+
return nil, returnErr
234+
}
235+
if returnErr = versoinCompare(patchedWork, latestWork); returnErr != nil {
236+
return nil, returnErr
237+
}
255238
if err := c.watcherStore.Update(newWork); err != nil {
256-
returnErr := errors.NewInternalError(err)
257-
metrics.IncreaseWorkProcessedCounter("patch", string(returnErr.ErrStatus.Reason))
239+
returnErr = errors.NewInternalError(err)
258240
return nil, returnErr
259241
}
260-
261-
metrics.IncreaseWorkProcessedCounter("patch", metav1.StatusSuccess)
262242
return newWork, nil
263243
}
244+
245+
func versoinCompare(new, old *workv1.ManifestWork) *errors.StatusError {
246+
// If resource version is empty or 0, skip the comparison (e.g., when publishing status updates)
247+
if new.GetResourceVersion() == "" || new.GetResourceVersion() == "0" {
248+
return nil
249+
}
250+
251+
lastResourceVersion, err := strconv.ParseInt(old.GetResourceVersion(), 10, 64)
252+
if err != nil {
253+
return errors.NewInternalError(err)
254+
}
255+
newResourceVersion, err := strconv.ParseInt(new.GetResourceVersion(), 10, 64)
256+
if err != nil {
257+
return errors.NewInternalError(err)
258+
}
259+
// ensure the resource version of the work is not outdated
260+
if newResourceVersion < lastResourceVersion {
261+
// It's safe to return a conflict error here, even if the status update event
262+
// has already been sent. The source may reject the update due to an outdated resource version.
263+
return errors.NewConflict(common.ManifestWorkGR, new.Name, fmt.Errorf(
264+
"the resource version of the work is outdated, new %d, old %d", newResourceVersion, lastResourceVersion))
265+
}
266+
return nil
267+
}

0 commit comments

Comments
 (0)