Skip to content

🐛 fix grpc broker resync bugs - #198

Merged
openshift-merge-bot[bot] merged 1 commit into
open-cluster-management-io:mainfrom
skeeey:grpc-broker-resync
Feb 10, 2026
Merged

🐛 fix grpc broker resync bugs#198
openshift-merge-bot[bot] merged 1 commit into
open-cluster-management-io:mainfrom
skeeey:grpc-broker-resync

Conversation

@skeeey

@skeeey skeeey commented Feb 9, 2026

Copy link
Copy Markdown
Member

Summary

Related issue(s)

Fixes #

Summary by CodeRabbit

  • New Features

    • Events now use resource UID for resourceid, surface deletion-timestamp as an explicit extension, and return empty payloads for deletion events.
    • Added centralized deletion-aware decoding utility and consolidated resource-version helpers for consistent setting/parsing.
  • Bug Fixes / Behavior

    • Store/watch flow updated to resolve cached objects by UID and handle pre-deletion paths consistently.
  • Tests

    • Large new test suites for encode/decode (including deletion), resource-version utils, store behavior, and end-to-end broker resync scenarios.

@openshift-ci
openshift-ci Bot requested review from deads2k and qiujian16 February 9, 2026 01:50
@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Swaps codec resource identifiers to UIDs, centralizes resourceVersion handling, adds deletion-aware encode/decode utilities, updates store/informer flows to UID-based deletion handling, refactors gRPC broker resync to use event-derived resource versions, and adds extensive unit and integration tests across affected packages.

Changes

Cohort / File(s) Summary
Codecs (UID, resourceVersion, deletion handling)
pkg/cloudevents/clients/addon/v1alpha1/codec.go, pkg/cloudevents/clients/addon/v1beta1/codec.go, pkg/cloudevents/clients/cluster/codec.go, pkg/cloudevents/clients/csr/codec.go, pkg/cloudevents/clients/lease/codec.go, pkg/cloudevents/clients/event/codec.go
Use object UID for resourceid, delegate resourceVersion setting to genericutils.SetResourceVersion, add early-return deletion handling (set deletiontimestamp extension), and switch decoding to utils.DecodeWithDeletionHandling.
Codec tests
pkg/cloudevents/clients/.../*_codec_test.go
Add comprehensive encode/decode tests for addon, cluster, csr, lease verifying EventDataType, UID-based resourceid, clustername extension, deletion path (empty payload + deletiontimestamp extension), unsupported data-type errors, and round-trip integrity.
Decode util & tests
pkg/cloudevents/clients/utils/decode.go, pkg/cloudevents/clients/utils/decode_test.go
Add generic DecodeWithDeletionHandling[T metav1.Object] to construct objects for deletion events (UID + DeletionTimestamp) or unmarshal payload when present; include extensive tests for success and error scenarios.
ResourceVersion utils & tests
pkg/cloudevents/generic/utils/resourceversion.go, pkg/cloudevents/generic/utils/resourceversion_test.go
Introduce SetResourceVersion, GetResourceVersionFromObject, GetResourceVersionFromEvent with ManifestBundle special-case (use Generation), parsing/validation, and tests covering branches and error cases.
Store changes & tests
pkg/cloudevents/clients/store/base.go, pkg/cloudevents/clients/store/informer.go, pkg/cloudevents/clients/store/simplestore.go, pkg/cloudevents/clients/store/simplestore_test.go
Add findObjByUID; rename locals to newRuntimeObj/newMetaObj; implement UID-based cached lookups for deletion handling (resolve cached object by UID), adjust update/delete semantics, and update tests to include UIDs.
Agent client & interface
pkg/cloudevents/generic/clients/agentclient.go, pkg/cloudevents/generic/interface.go
Resync retrieves resourceVersion via GetResourceVersionFromObject(eventType, obj); doc comment for GetResourceVersion clarified.
gRPC broker resync & tests
pkg/cloudevents/server/grpc/broker.go, pkg/cloudevents/server/grpc/broker_resync_test.go
Resync refactor: extract resourceVersion from events via GetResourceVersionFromEvent, use event type per-entry, perform UID-based lookup for deletions, build delete events instead of mutating objects, and add extensive resync integration tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

approved, lgtm

Suggested reviewers

  • deads2k
  • qiujian16
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is empty, containing only the default template with unfilled 'Summary' and 'Related issue(s)' sections, failing to explain the code changes or rationale. Provide a detailed summary explaining which resync bugs were fixed, what changes address them, and reference any related issues or GitHub issue numbers.
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title ':bug: fix grpc broker resync bugs' clearly summarizes the main change—fixing gRPC broker resync bugs—and aligns with the extensive changes to resync logic across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@pkg/cloudevents/clients/addon/v1alpha1/codec.go`:
- Around line 40-50: The event codec currently sets the resource ID using
event.Name; change it to use the UID to match other codecs: when building the
CloudEvent in pkg/cloudevents/clients/event/codec.go (the code path that calls
types.NewEventBuilder(...).WithResourceID(...)), replace event.Name with
string(event.UID) so the ExtensionResourceID extension is UID-based and
consistent with addon/cluster/work/csr/lease/serviceaccount codecs.

In `@pkg/cloudevents/clients/store/informer.go`:
- Around line 70-87: The metadata mutations
(SetDeletionTimestamp/SetFinalizers/SetGeneration) are being applied to
cachedMetaObj (the original cached object) while cachedRuntimeObj (used in
s.Update) was created earlier via utils.ToRuntimeObject, so the deep copy passed
to s.Update doesn't include the changes; fix by applying the mutations to a deep
copy or by creating cachedRuntimeObj after performing the
SetDeletionTimestamp/SetFinalizers/SetGeneration changes on the meta accessor,
e.g. obtain meta.Accessor(cachedResource), perform the three Set* calls, then
call utils.ToRuntimeObject to produce cachedRuntimeObj and pass that into
s.Update so the updated metadata is persisted and emitted.

In `@pkg/cloudevents/clients/store/simplestore.go`:
- Around line 62-85: The branch that handles deletion-with-finalizers returns
early when newMetaObj has finalizers, which prevents the cached object's
DeletionTimestamp/Finalizers from being updated; instead, after finding
cachedResource via s.findObjByUID and obtaining cachedMetaObj (meta.Accessor)
and cachedRuntimeObj (utils.ToRuntimeObject), copy the deletion timestamp and
finalizers from newMetaObj into cachedMetaObj and persist the change with
s.Update(cachedRuntimeObj) (falling back to returning nil if cached resource
doesn't exist), mirroring the behavior used by the informer update logic rather
than silently discarding the update.

In `@pkg/cloudevents/clients/utils/decode.go`:
- Around line 47-50: The error return in Decode (when calling evt.DataAs(obj))
has a typo ("even" → "event") and creates a new shadowed zero value `var zero
T`; fix by replacing the error string with "failed to unmarshal event: %v" (or
similar) and remove the local `var zero T` so the function returns the
already-declared zero value `zero` from the top of the function; update the
fmt.Errorf call in the block containing `evt.DataAs(obj)` to use the corrected
message and the existing `zero` identifier.

In `@pkg/cloudevents/generic/utils/resourceversion.go`:
- Line 9: The import alias in resourceversion.go is misspelled as workpaylaod;
change the import alias to workpayload and update all usages in this file that
reference workpaylaod. Locate the import line that declares workpaylaod
"open-cluster-management.io/sdk-go/pkg/cloudevents/clients/work/payload" and
rename it to workpayload, then replace every occurrence of workpaylaod.<symbol>
with workpayload.<symbol> (e.g., any functions, types, or constants referenced
via the alias) to keep identifiers consistent.
- Around line 48-52: GetResourceVersionFromEvent currently uses
cloudeventstypes.ToInteger (int32) to read the extension
types.ExtensionResourceVersion which causes truncation for generations >
MaxInt32; update GetResourceVersionFromEvent to read the extension value as a
full int64 (or parse it as int64) instead of using ToInteger — e.g., use a
ToInt64/ToInteger64 helper if available, or fetch
evt.Extensions()[types.ExtensionResourceVersion] as an interface and
convert/parse it to int64 safely, then return that int64 and any parsing error;
ensure the change aligns with how SetResourceVersion wrote obj.GetGeneration()
(int64).

In `@pkg/cloudevents/server/grpc/broker.go`:
- Around line 376-381: Replace the hardcoded "source" literal passed to
types.NewEventBuilder with the actual source from the incoming event by calling
evt.Source(); specifically update the call to types.NewEventBuilder(...) in the
broker.go block that builds the deletion event (the NewEventBuilder call that
currently uses "source" and respEventType) to use evt.Source() so the
constructed event (evt :=
types.NewEventBuilder(...).WithResourceID(...).WithClusterName(...).WithDeletionTimestamp(...).NewEvent())
carries the correct source metadata.
- Around line 331-333: The loop mutates events in-place via
evt.SetType(respEventType.String()), which alters the original pointers returned
by Service.List() and can persist into service caches; fix by cloning each event
before any mutation (e.g., create respEvt := evt.Clone() or equivalent) and then
call respEvt.SetType(...) and use respEvt (or &respEvt as needed) and evtLogger
derived from the cloned instance throughout the loop body so the original evt
objects remain unchanged.
🧹 Nitpick comments (8)
pkg/cloudevents/clients/store/simplestore.go (1)

75-83: Roundabout conversion: ToRuntimeObject(cachedMetaObj) on a metav1.Object.

cachedResource is already of type T (which satisfies runtime.Object). You could call utils.ToRuntimeObject(cachedResource) directly instead of going through meta.Accessor and then back to ToRuntimeObject(cachedMetaObj). The meta.Accessor call is unnecessary on this code path since neither DeletionTimestamp, Finalizers, nor Generation are being set on the cached object here (unlike informer.go).

♻️ Proposed simplification
-		cachedMetaObj, err := meta.Accessor(cachedResource)
-		if err != nil {
-			return err
-		}
-
-		cachedRuntimeObj, err := utils.ToRuntimeObject(cachedMetaObj)
-		if err != nil {
-			return err
-		}
-		return s.Delete(cachedRuntimeObj)
+		cachedRuntimeObj, err := utils.ToRuntimeObject(cachedResource)
+		if err != nil {
+			return err
+		}
+		return s.Delete(cachedRuntimeObj)
pkg/cloudevents/clients/utils/decode.go (1)

39-44: Deletion-with-data path does not explicitly set DeletionTimestamp from the extension.

When a deletion event carries data (line 40-44), the code relies entirely on DataAs to populate the DeletionTimestamp. If the serialized payload omits it (e.g., the source only signals deletion via the CloudEvent extension), the returned object won't reflect the deletion state. Consider explicitly setting the deletion timestamp from the extension after unmarshalling, as done in the no-data branch (lines 30-36), for defensive consistency.

🛡️ Proposed defensive fix
 		if err := evt.DataAs(obj); err != nil {
 			return zero, fmt.Errorf("failed to unmarshal event, %v", err)
 		}

+		if obj.GetDeletionTimestamp() == nil {
+			deletionTimestamp, err := cloudeventstypes.ToTime(evt.Extensions()[types.ExtensionDeletionTimestamp])
+			if err != nil {
+				return zero, err
+			}
+			obj.SetDeletionTimestamp(&metav1.Time{Time: deletionTimestamp})
+		}
+
 		return obj, nil
pkg/cloudevents/clients/utils/decode_test.go (2)

30-36: expectedErr field is declared but unused in the test runner.

The expectedErr field is defined in the test struct but the test loop (lines 230-236) never checks it — all error assertions are handled inside the validate callback. Consider removing the field to avoid confusion.


76-93: "Deletion event with data" test does not assert DeletionTimestamp.

This test validates Name, Namespace, UID, and Data, but doesn't check that DeletionTimestamp is set on the decoded object. Given that the extension signals deletion, verifying this field would strengthen coverage and would also surface the gap noted in decode.go where the deletion-with-data path doesn't explicitly set DeletionTimestamp from the extension.

pkg/cloudevents/generic/utils/resourceversion.go (1)

24-30: Redundant call to obj.GetResourceVersion() on line 29.

rv already holds the value from line 24.

♻️ Proposed fix
 	rv := obj.GetResourceVersion()
 	if rv == "" {
 		return
 	}

-	evt.SetExtension(types.ExtensionResourceVersion, obj.GetResourceVersion())
+	evt.SetExtension(types.ExtensionResourceVersion, rv)
pkg/cloudevents/clients/lease/codec_test.go (1)

197-199: Nil-pointer dereference risk on HolderIdentity.

If Decode ever fails to restore the HolderIdentity pointer, dereferencing decoded.Spec.HolderIdentity (line 197) and *decoded.Spec.HolderIdentity (line 274) will panic instead of producing a clear test failure. A nil guard would make failures easier to diagnose.

Proposed guard
+	if decoded.Spec.HolderIdentity == nil {
+		t.Fatal("expected non-nil HolderIdentity")
+	}
 	if *decoded.Spec.HolderIdentity != "holder-1" {
 		t.Errorf("expected HolderIdentity holder-1, got %s", *decoded.Spec.HolderIdentity)
 	}
pkg/cloudevents/server/grpc/broker_resync_test.go (2)

76-81: Unchecked type assertion on ExtensionResourceID will produce a confusing panic if the extension is missing.

Line 79 does evt.Extensions()[cetypes.ExtensionResourceID].(string) without a comma-ok check. The same pattern appears in the receive handler at lines 283 and 464. If an event lacks this extension (e.g., due to an encoding bug), the test will panic with an unhelpful stack trace rather than a clear failure message.

Proposed guard for setResource
 func (s *testResyncService) setResource(evt *cloudevents.Event) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
-	resourceID := evt.Extensions()[cetypes.ExtensionResourceID].(string)
+	resourceID, ok := evt.Extensions()[cetypes.ExtensionResourceID].(string)
+	if !ok {
+		panic(fmt.Sprintf("event missing resourceid extension: %v", evt.Extensions()))
+	}
 	s.resources[resourceID] = evt
 }

297-302: Silently discarding errors from event creation functions.

Throughout the test (e.g., work1, _ := createWorkEvent(...) at line 297, res1, _ := createEventFn(...) at line 485), errors from event creation are ignored. If encoding fails, nil events will be passed to svc.setResource, causing a panic at the type assertion. Check the error to get clear test failures.

Example fix
-		res1, _ := createEventFn(clusterName, "res-1", 100, nil)
+		res1, err := createEventFn(clusterName, "res-1", 100, nil)
+		if err != nil {
+			t.Fatalf("failed to create event: %v", err)
+		}

Also applies to: 485-490

Comment thread pkg/cloudevents/clients/addon/v1alpha1/codec.go
Comment thread pkg/cloudevents/clients/store/informer.go
Comment on lines +62 to +85
if !newMetaObj.GetDeletionTimestamp().IsZero() {
cachedResource, exists, err := s.findObjByUID(ctx, newMetaObj.GetUID())
if err != nil {
return err
}
if !exists {
return nil
}

if len(newMetaObj.GetFinalizers()) != 0 {
return nil
}

cachedMetaObj, err := meta.Accessor(cachedResource)
if err != nil {
return err
}

cachedRuntimeObj, err := utils.ToRuntimeObject(cachedMetaObj)
if err != nil {
return err
}
return s.Delete(cachedRuntimeObj)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Deletion-with-finalizers silently discards state updates, unlike informer.go.

When newMetaObj has finalizers (line 71-73), this store returns nil without updating the cached object's DeletionTimestamp/Finalizers. In contrast, informer.go (lines 82-87) updates the cached object with these fields and persists via s.Update(cachedRuntimeObj).

This means a subsequent Get from this store won't reflect that the resource is being deleted (has a DeletionTimestamp set), which could lead to stale reads. If SimpleStore consumers never inspect deletion metadata, this is fine — but the inconsistency is worth confirming as intentional.

🤖 Prompt for AI Agents
In `@pkg/cloudevents/clients/store/simplestore.go` around lines 62 - 85, The
branch that handles deletion-with-finalizers returns early when newMetaObj has
finalizers, which prevents the cached object's DeletionTimestamp/Finalizers from
being updated; instead, after finding cachedResource via s.findObjByUID and
obtaining cachedMetaObj (meta.Accessor) and cachedRuntimeObj
(utils.ToRuntimeObject), copy the deletion timestamp and finalizers from
newMetaObj into cachedMetaObj and persist the change with
s.Update(cachedRuntimeObj) (falling back to returning nil if cached resource
doesn't exist), mirroring the behavior used by the informer update logic rather
than silently discarding the update.

Comment thread pkg/cloudevents/clients/utils/decode.go
Comment thread pkg/cloudevents/generic/utils/resourceversion.go Outdated
Comment on lines +48 to +52
func GetResourceVersionFromEvent(eventType types.CloudEventsType, evt cloudevents.Event) (int64, error) {
if eventType.CloudEventsDataType == workpaylaod.ManifestBundleEventDataType {
gen, err := cloudeventstypes.ToInteger(evt.Extensions()[types.ExtensionResourceVersion])
return int64(gen), err
}

@coderabbitai coderabbitai Bot Feb 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

cloudevents sdk-go ToInteger return type

💡 Result:

In cloudevents/sdk-go (v2), types.ToInteger returns:

func ToInteger(v interface{}) (int32, error)

So the value type is int32 (plus an error). [1]

Sources: [1]


🏁 Script executed:

cd /
find . -path ./tmp -prune -o -name "resourceversion.go" -type f -print 2>/dev/null | head -20

Repository: open-cluster-management-io/sdk-go

Length of output: 145


🏁 Script executed:

git ls-files | grep -E "resourceversion\.go|types\.go" | head -20

Repository: open-cluster-management-io/sdk-go

Length of output: 1078


🏁 Script executed:

cat -n pkg/cloudevents/generic/utils/resourceversion.go

Repository: open-cluster-management-io/sdk-go

Length of output: 3115


🏁 Script executed:

wc -l pkg/cloudevents/generic/utils/resourceversion.go

Repository: open-cluster-management-io/sdk-go

Length of output: 128


🏁 Script executed:

grep -r "GetGeneration" pkg/cloudevents/generic/types.go

Repository: open-cluster-management-io/sdk-go

Length of output: 142


🏁 Script executed:

grep -r "type ResourceObject" pkg/cloudevents/generic/ --include="*.go"

Repository: open-cluster-management-io/sdk-go

Length of output: 145


🏁 Script executed:

cat -n pkg/cloudevents/generic/interface.go

Repository: open-cluster-management-io/sdk-go

Length of output: 4185


Address int32 truncation for generation values in ManifestBundleEvent.

SetResourceVersion stores obj.GetGeneration() (int64) as the extension value, but GetResourceVersionFromEvent reads it back with cloudeventstypes.ToInteger() which returns int32. If generation exceeds math.MaxInt32, the value will be silently truncated when read, despite the subsequent cast to int64 at line 51.

🤖 Prompt for AI Agents
In `@pkg/cloudevents/generic/utils/resourceversion.go` around lines 48 - 52,
GetResourceVersionFromEvent currently uses cloudeventstypes.ToInteger (int32) to
read the extension types.ExtensionResourceVersion which causes truncation for
generations > MaxInt32; update GetResourceVersionFromEvent to read the extension
value as a full int64 (or parse it as int64) instead of using ToInteger — e.g.,
use a ToInt64/ToInteger64 helper if available, or fetch
evt.Extensions()[types.ExtensionResourceVersion] as an interface and
convert/parse it to int64 safely, then return that int64 and any parsing error;
ensure the change aligns with how SetResourceVersion wrote obj.GetGeneration()
(int64).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this convert is fine to us for now, I added a todo for this, we need to use the string as the resourcevesion extension in cloudevent to avoid type conversion

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if a TODO was added to the resourceversion.go file
rg -n "TODO|FIXME" pkg/cloudevents/generic/utils/resourceversion.go

Length of output: 61


@skeeey, sounds good! I can see the TODO comment you added at line 50. Using a string for the resource version extension will indeed avoid the type conversion concerns altogether. Thanks for documenting this for future work! 👍

Comment thread pkg/cloudevents/server/grpc/broker.go
Comment on lines +376 to 381
// for deletion, we don't care about the resourceVersion.
evt := types.NewEventBuilder("source", respEventType).
WithResourceID(rv.ResourceID).
WithResourceVersion(rv.ResourceVersion).
WithClusterName(clusterName).
WithDeletionTimestamp(time.Now()).
NewEvent()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how NewEventBuilder is called elsewhere to see what source values are typically used
rg -n 'NewEventBuilder\(' --type go -C 2

Repository: open-cluster-management-io/sdk-go

Length of output: 12836


🏁 Script executed:

cat -n pkg/cloudevents/server/grpc/broker.go | sed -n '360,390p'

Repository: open-cluster-management-io/sdk-go

Length of output: 1321


🏁 Script executed:

cat -n pkg/cloudevents/server/grpc/broker.go | sed -n '1,100p' | head -80

Repository: open-cluster-management-io/sdk-go

Length of output: 3448


🏁 Script executed:

cat -n pkg/cloudevents/server/grpc/broker.go | sed -n '300,377p'

Repository: open-cluster-management-io/sdk-go

Length of output: 3339


🏁 Script executed:

cat -n pkg/cloudevents/server/grpc/broker.go | sed -n '300,330p'

Repository: open-cluster-management-io/sdk-go

Length of output: 1296


🏁 Script executed:

cat -n pkg/cloudevents/server/grpc/broker.go | grep -A 5 -B 5 "evt.Source\|evt.Context\|Source()"

Repository: open-cluster-management-io/sdk-go

Length of output: 741


🏁 Script executed:

rg -n "evt\.Source\(\)" --type go pkg/cloudevents/server/grpc/

Repository: open-cluster-management-io/sdk-go

Length of output: 59


🏁 Script executed:

# Check if there's a source extraction pattern elsewhere
rg -n "\.Source\(\)" --type go -C 2 | head -40

Repository: open-cluster-management-io/sdk-go

Length of output: 2973


🏁 Script executed:

cat -n pkg/cloudevents/generic/clients/sourceclient.go | sed -n '280,300p'

Repository: open-cluster-management-io/sdk-go

Length of output: 969


🏁 Script executed:

cat -n pkg/cloudevents/server/grpc/broker.go | sed -n '300,315p'

Repository: open-cluster-management-io/sdk-go

Length of output: 771


Replace hardcoded "source" literal with evt.Source().

types.NewEventBuilder("source", respEventType) at line 377 uses the string literal "source" as a placeholder. The incoming event parameter has the actual source available via evt.Source(), which should be used instead to ensure proper event metadata for downstream consumers.

🤖 Prompt for AI Agents
In `@pkg/cloudevents/server/grpc/broker.go` around lines 376 - 381, Replace the
hardcoded "source" literal passed to types.NewEventBuilder with the actual
source from the incoming event by calling evt.Source(); specifically update the
call to types.NewEventBuilder(...) in the broker.go block that builds the
deletion event (the NewEventBuilder call that currently uses "source" and
respEventType) to use evt.Source() so the constructed event (evt :=
types.NewEventBuilder(...).WithResourceID(...).WithClusterName(...).WithDeletionTimestamp(...).NewEvent())
carries the correct source metadata.

@skeeey
skeeey force-pushed the grpc-broker-resync branch from 6abd1b9 to 04df29b Compare February 9, 2026 01:58

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/cloudevents/server/grpc/broker_resync_test.go`:
- Around line 280-293: The goroutine that calls
agentOptions.CloudEventsTransport.Receive currently calls t.Errorf directly
which can race if the test ends; modify the goroutine to capture any error and
send it on a buffered error channel (e.g., errCh := make(chan error, 1)) instead
of calling t.Errorf inside the goroutine, then ensure the main test waits for
the goroutine to finish (e.g., via canceling the context with
t.Cleanup(ctxCancel) or a sync.WaitGroup) and after the goroutine returns read
from errCh and call t.Errorf/fatal in the main test goroutine; apply the same
pattern to the other occurrence in testBrokerResync so all Receive errors are
reported from the main test goroutine.
🧹 Nitpick comments (5)
pkg/cloudevents/clients/store/simplestore.go (1)

75-83: Eliminate unnecessary round-trip through meta.Accessor before ToRuntimeObject.

cachedResource from findObjByUID is already of type T (a generic.ResourceObject), which can be passed directly to utils.ToRuntimeObject. The detour through meta.Accessor (returning metav1.Object) is redundant since ToRuntimeObject is defined to accept T generic.ResourceObject directly.

♻️ Suggested simplification
-		cachedMetaObj, err := meta.Accessor(cachedResource)
-		if err != nil {
-			return err
-		}
-
-		cachedRuntimeObj, err := utils.ToRuntimeObject(cachedMetaObj)
+		cachedRuntimeObj, err := utils.ToRuntimeObject(cachedResource)
 		if err != nil {
 			return err
 		}
 		return s.Delete(cachedRuntimeObj)
pkg/cloudevents/generic/utils/resourceversion.go (1)

24-29: Reuse the already-fetched rv variable instead of calling GetResourceVersion() twice.

Line 29 calls obj.GetResourceVersion() again when the value is already stored in rv from line 24.

♻️ Proposed fix
-	evt.SetExtension(types.ExtensionResourceVersion, obj.GetResourceVersion())
+	evt.SetExtension(types.ExtensionResourceVersion, rv)
pkg/cloudevents/server/grpc/broker.go (1)

309-313: Consider using cloudeventstypes.ToString for cluster name extraction consistency.

Line 313 uses fmt.Sprintf("%s", clusterNameValue) to convert the extension to a string, while line 345 uses cloudeventstypes.ToString(...) with proper error handling for resourceID. Using ToString here too would provide consistent error handling if the extension is unexpectedly missing or of the wrong type.

Proposed fix
-	clusterNameValue, err := evt.Context.GetExtension(types.ExtensionClusterName)
-	if err != nil {
+	clusterNameValue, err := evt.Context.GetExtension(types.ExtensionClusterName)
+	if err != nil {
 		return err
 	}
-	clusterName := fmt.Sprintf("%s", clusterNameValue)
+	clusterName, err := cloudeventstypes.ToString(clusterNameValue)
+	if err != nil {
+		return fmt.Errorf("failed to get clustername extension: %v", err)
+	}
pkg/cloudevents/server/grpc/broker_resync_test.go (2)

295-336: TestBrokerResync_Work subtests clean up at the end instead of the beginning.

Lines 331–335 clean up receivedEvents and svc at the end of each subtest. If a subtest fails via t.Fatalf (e.g., line 320), cleanup is skipped and the next subtest inherits stale state. The generic testBrokerResync function correctly cleans up at the start of each subtest (e.g., lines 477–482). Align this test with the same pattern.

Proposed fix (example for the first subtest)
 	t.Run("work: empty resync gets all resources", func(t *testing.T) {
+		// Clean state first
+		svc.clear()
+		drainChannel(receivedCh)
+		mu.Lock()
+		receivedEvents = make(map[string]*cloudevents.Event)
+		mu.Unlock()
+
 		// Setup: Create 3 resources on source
 		work1, _ := createWorkEvent(clusterName, "work-1", 100, nil)
 		...
 
 		if count != 3 {
 			t.Errorf("expected 3 resources, got %d", count)
 		}
-
-		// Cleanup
-		mu.Lock()
-		receivedEvents = make(map[string]*cloudevents.Event)
-		mu.Unlock()
-		svc.clear()
 	})

Apply the same pattern to the other Work subtests.


297-302: Errors from createWorkEvent are silently discarded.

If createWorkEvent (or any createEventFn) returns an error, the event will be nil, and svc.setResource(work1) will panic on the unguarded type assertion at line 79 (evt.Extensions()[cetypes.ExtensionResourceID].(string)). Use t.Fatal on error for clearer test failures. The same pattern applies to the generic testBrokerResync (e.g., lines 485–490).

Proposed fix
-		work1, _ := createWorkEvent(clusterName, "work-1", 100, nil)
-		work2, _ := createWorkEvent(clusterName, "work-2", 200, nil)
-		work3, _ := createWorkEvent(clusterName, "work-3", 300, nil)
+		work1, err := createWorkEvent(clusterName, "work-1", 100, nil)
+		if err != nil {
+			t.Fatalf("failed to create work event: %v", err)
+		}
+		work2, err := createWorkEvent(clusterName, "work-2", 200, nil)
+		if err != nil {
+			t.Fatalf("failed to create work event: %v", err)
+		}
+		work3, err := createWorkEvent(clusterName, "work-3", 300, nil)
+		if err != nil {
+			t.Fatalf("failed to create work event: %v", err)
+		}

Comment on lines +280 to +293
go func() {
if err := agentOptions.CloudEventsTransport.Receive(ctx, func(ctx context.Context, event cloudevents.Event) {
mu.Lock()
resourceID := event.Extensions()[cetypes.ExtensionResourceID].(string)
receivedEvents[resourceID] = &event
mu.Unlock()
select {
case receivedCh <- struct{}{}:
default:
}
}); err != nil && err != context.Canceled {
t.Errorf("receive error: %v", err)
}
}()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

t.Errorf called from a goroutine may race with test completion.

If this goroutine outlives the test (e.g., the Receive callback fires after the test returns), calling t.Errorf on a finished test panics. The same issue exists in the generic testBrokerResync at line 472. Consider capturing errors into a channel and asserting after the goroutine is joined, or using t.Cleanup to ensure the context is cancelled before the test returns.

🤖 Prompt for AI Agents
In `@pkg/cloudevents/server/grpc/broker_resync_test.go` around lines 280 - 293,
The goroutine that calls agentOptions.CloudEventsTransport.Receive currently
calls t.Errorf directly which can race if the test ends; modify the goroutine to
capture any error and send it on a buffered error channel (e.g., errCh :=
make(chan error, 1)) instead of calling t.Errorf inside the goroutine, then
ensure the main test waits for the goroutine to finish (e.g., via canceling the
context with t.Cleanup(ctxCancel) or a sync.WaitGroup) and after the goroutine
returns read from errCh and call t.Errorf/fatal in the main test goroutine;
apply the same pattern to the other occurrence in testBrokerResync so all
Receive errors are reported from the main test goroutine.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/cloudevents/clients/utils/decode_test.go`:
- Around line 219-235: The test "regular event without data" calls
DecodeWithDeletionHandling which currently may invoke evt.DataAs(obj) on an
event with empty payload; change the test to avoid undefined DataAs behavior by
either (A) setting explicit non-empty valid data on the event before calling
DecodeWithDeletionHandling, or (B) update the code path under test
(DecodeWithDeletionHandling) to check if len(evt.Data()) == 0 before calling
evt.DataAs and treat empty-data events as empty-success (mirroring deletion
handling). Refer to DecodeWithDeletionHandling, evt.Data(), and evt.DataAs when
applying the fix so the test does not rely on decoder-specific behavior.
🧹 Nitpick comments (8)
pkg/cloudevents/clients/addon/v1alpha1/codec_test.go (2)

204-249: "addon with labels and annotations" test case never asserts labels or annotations survive the round-trip.

The test sets Labels and Annotations on the input but only checks UID, Name, and Namespace after decoding. Either add assertions for the metadata maps or rename the case to avoid implying coverage that isn't there.

Proposed additional assertions
 			if decoded.Namespace != tt.addon.Namespace {
 				t.Errorf("Namespace mismatch: expected %s, got %s", tt.addon.Namespace, decoded.Namespace)
 			}
+			if tt.addon.Labels != nil {
+				for k, v := range tt.addon.Labels {
+					if decoded.Labels[k] != v {
+						t.Errorf("Label %s mismatch: expected %s, got %s", k, v, decoded.Labels[k])
+					}
+				}
+			}
+			if tt.addon.Annotations != nil {
+				for k, v := range tt.addon.Annotations {
+					if decoded.Annotations[k] != v {
+						t.Errorf("Annotation %s mismatch: expected %s, got %s", k, v, decoded.Annotations[k])
+					}
+				}
+			}

143-183: Consider adding a decode test for a deletion event.

The encode path for deletion is tested (TestManagedClusterAddOnCodec_Encode_WithDeletionTimestamp), but there is no corresponding decode test that verifies what Decode returns for an event carrying a deletiontimestamp extension and empty payload. Given the PR introduces DecodeWithDeletionHandling, a round-trip through a deletion event would strengthen confidence in that path.

pkg/cloudevents/clients/utils/decode_test.go (2)

30-36: Unused expectedErr field in test table.

The expectedErr field is defined in the test case struct but never read in the test runner loop (lines 238–244). The validate callback handles all assertions, making expectedErr dead code. Either remove the field or use it in the runner to guard the validate call.

♻️ Suggested fix: remove unused field
 	cases := []struct {
 		name        string
 		event       func() *cloudevents.Event
 		factory     func() *corev1.ConfigMap
 		validate    func(t *testing.T, obj *corev1.ConfigMap, err error)
-		expectedErr bool
 	}{

64-96: Consider asserting DeletionTimestamp behavior for deletion-with-data case.

When the deletion extension is present but data is also present, the implementation skips setting DeletionTimestamp from the extension and relies entirely on DataAs. Since testConfigMap has no DeletionTimestamp, the decoded object won't have one either. It may be worth adding an assertion that obj.GetDeletionTimestamp() == nil to make this behavior explicit, or testing with a testConfigMap that does have a DeletionTimestamp to verify it round-trips through the data payload.

pkg/cloudevents/server/grpc/broker_resync_test.go (4)

76-81: Unchecked type assertion in setResource will panic on missing extension.

Line 79 performs evt.Extensions()[cetypes.ExtensionResourceID].(string) without the comma-ok form. If a test ever passes an event without ExtensionResourceID (or with a non-string value), this panics with an unhelpful stack trace. Since this is test-only code the risk is contained, but a guard would improve debuggability.

♻️ Suggested fix
 func (s *testResyncService) setResource(evt *cloudevents.Event) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
-	resourceID := evt.Extensions()[cetypes.ExtensionResourceID].(string)
+	resourceID, ok := evt.Extensions()[cetypes.ExtensionResourceID].(string)
+	if !ok {
+		panic(fmt.Sprintf("test bug: event missing or non-string %s extension", cetypes.ExtensionResourceID))
+	}
 	s.resources[resourceID] = evt
 }

280-293: Receive goroutine also performs an unchecked type assertion.

Lines 283 and 470 do event.Extensions()[cetypes.ExtensionResourceID].(string) inside the Receive callback. If the broker ever sends an event without this extension (e.g., an error/control event), this panics inside the goroutine, which will crash the test process rather than producing a clear failure message.

♻️ Suggested fix (apply to both occurrences)
 		if err := agentOptions.CloudEventsTransport.Receive(ctx, func(ctx context.Context, event cloudevents.Event) {
 			mu.Lock()
-			resourceID := event.Extensions()[cetypes.ExtensionResourceID].(string)
+			resourceID, ok := event.Extensions()[cetypes.ExtensionResourceID].(string)
+			if !ok {
+				mu.Unlock()
+				return
+			}
 			receivedEvents[resourceID] = &event
 			mu.Unlock()

Also applies to: 467-480


491-496: Errors from createEventFn / createWorkEvent are silently discarded.

Throughout the test, event creation errors are ignored with _, _ := pattern (e.g., lines 297–299, 491–493, 535, 574, 669, 722–724). If a codec change causes these to fail, the test would pass a nil event to svc.setResource, which would then panic at the type assertion — masking the real issue.

Use t.Helper() + t.Fatal or a local helper that fails immediately on creation errors.

♻️ Example helper
mustCreateEvent := func(t *testing.T, fn func(string, string, int64, *metav1.Time) (*cloudevents.Event, error), cluster, id string, ver int64, ts *metav1.Time) *cloudevents.Event {
	t.Helper()
	evt, err := fn(cluster, id, ver, ts)
	if err != nil {
		t.Fatalf("failed to create event %s: %v", id, err)
	}
	return evt
}

Also applies to: 297-302


608-658: Deletion detection test relies on time.Sleep for synchronization.

Line 616 uses time.Sleep(100 * time.Millisecond) as a workaround for potential event delivery from prior subtests. This is fragile under CI load. Since each subtest uses unique resource IDs (res-deleted is not used elsewhere), the sleep may be unnecessary. If it is needed, it indicates the drain/clear pattern isn't sufficient, which could affect other subtests too.

Comment on lines +219 to +235
{
name: "regular event without data",
event: func() *cloudevents.Event {
evt := cloudevents.NewEvent()
return &evt
},
factory: func() *corev1.ConfigMap {
return &corev1.ConfigMap{}
},
validate: func(t *testing.T, obj *corev1.ConfigMap, err error) {
// Empty data should be handled gracefully
if err != nil {
t.Errorf("unexpected error: %v", err)
}
},
expectedErr: false,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

cloudevents sdk-go v2 DataAs nil empty data behavior documentation

💡 Result:

In cloudevents/sdk-go/v2, Event.DataAs(&out) is only documented as “attempts to populate the provided data object with the event payload” (the argument must be a pointer). It does not document any special-case behavior for “no data” / nil / empty payloads. [1]

What is documented is the mechanism:

  • Event.DataAs() decodes the event’s data using a registered datacodec Decoder selected from the event’s datacontenttype. [2]
  • datacodec.Decode(ctx, contentType, in, out) returns an error if no decoder is registered for that content type. [3]

So the “nil vs empty” outcome is effectively decoder-defined:

  • For application/json, the JSON decoder’s contract is simply that it “takes in as []byte” and decodes it into out. If in is empty/nil, the underlying JSON unmarshal will fail (commonly with an “unexpected end of JSON input”-style error). [4]

Practical guidance (since this isn’t explicitly specified in docs):

  • If “missing data” should be treated as “no-op”, check len(event.Data()) == 0 before calling DataAs. (Event.Data() returns the raw data bytes.) [1][2]

Sources:
[1] CloudEvents SDK-Go v2 event.Event docs (Data(), DataAs()) (pkg.go.dev)
[2] “Event data structure” (explains DataAs() uses datacodec decoders) (cloudevents.github.io)
[3] event/datacodec docs (Decode behavior when no decoder registered) (pkg.go.dev)
[4] event/datacodec/json docs (JSON decoder takes in []byte) (pkg.go.dev)

Citations:


🏁 Script executed:

# Find the CloudEvents SDK version and check NewEvent() defaults
cd /tmp && git clone --depth 1 https://github.com/cloudevents/sdk-go 2>/dev/null || true
cd sdk-go && git log --oneline -1 2>/dev/null || echo "Clone check"

# Check what NewEvent() returns
rg "func NewEvent" -A 10 --type go

Repository: open-cluster-management-io/sdk-go

Length of output: 2213


🏁 Script executed:

# Now check the actual test file and decode implementation
rg "DecodeWithDeletionHandling" -B 5 -A 15 --type go | head -100

Repository: open-cluster-management-io/sdk-go

Length of output: 7130


🏁 Script executed:

# Check what happens with empty data and no content type
rg "DataAs" pkg/cloudevents/clients/utils/ -B 3 -A 3 --type go

Repository: open-cluster-management-io/sdk-go

Length of output: 914


Ensure empty events are handled safely without relying on undefined DataAs behavior.

The test expects DecodeWithDeletionHandling to succeed on a non-deletion event with no data. However, calling evt.DataAs(obj) with empty data is not guaranteed to succeed—the behavior is decoder-dependent and not specified by the CloudEvents SDK. For example, the JSON decoder fails on empty payloads.

The implementation already uses the safe pattern if len(evt.Data()) == 0 when handling deletion events. Apply the same check to non-deletion events before calling DataAs, or explicitly set valid test data to avoid undefined behavior.

🤖 Prompt for AI Agents
In `@pkg/cloudevents/clients/utils/decode_test.go` around lines 219 - 235, The
test "regular event without data" calls DecodeWithDeletionHandling which
currently may invoke evt.DataAs(obj) on an event with empty payload; change the
test to avoid undefined DataAs behavior by either (A) setting explicit non-empty
valid data on the event before calling DecodeWithDeletionHandling, or (B) update
the code path under test (DecodeWithDeletionHandling) to check if
len(evt.Data()) == 0 before calling evt.DataAs and treat empty-data events as
empty-success (mirroring deletion handling). Refer to
DecodeWithDeletionHandling, evt.Data(), and evt.DataAs when applying the fix so
the test does not rely on decoder-specific behavior.

func GetResourceVersionFromEvent(eventType types.CloudEventsType, evt cloudevents.Event) (int64, error) {
if eventType.CloudEventsDataType == workpayload.ManifestBundleEventDataType {
gen, err := cloudeventstypes.ToInteger(evt.Extensions()[types.ExtensionResourceVersion])
return int64(gen), err

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.

why we need to treat this differently? It seems it differs only for type conversion?

@skeeey skeeey Feb 9, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We need this for now because the cloudevents sdk SetExtension accepts any type for extension values, but when retrieving extensions, the sdk internally converts them to a specific type—even though the return type is any

I think we need to use the string as the resourcevesion extension in cloudevent to avoid type conversion, consider maestro already use int32 as this extension, I added todo for this, we may need a new pr to handle this

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/cloudevents/generic/utils/resourceversion.go`:
- Around line 24-29: The code redundantly calls obj.GetResourceVersion() again
when you've already stored it in rv; update the call to
evt.SetExtension(types.ExtensionResourceVersion, obj.GetResourceVersion()) to
use the cached rv variable instead (i.e., pass rv), ensuring you still return
early when rv == "" as currently implemented.

Comment thread pkg/cloudevents/generic/utils/resourceversion.go Outdated
Signed-off-by: Wei Liu <liuweixa@redhat.com>
@skeeey
skeeey force-pushed the grpc-broker-resync branch from 84f48f5 to 8309c40 Compare February 9, 2026 08:19
@skeeey

skeeey commented Feb 9, 2026

Copy link
Copy Markdown
Member Author

/assign @qiujian16

@qiujian16 qiujian16 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.

/approve
/lgtm

@openshift-ci openshift-ci Bot added the lgtm label Feb 10, 2026
@openshift-ci

openshift-ci Bot commented Feb 10, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: qiujian16, skeeey

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

The pull request process is described 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

@openshift-merge-bot
openshift-merge-bot Bot merged commit 797891e into open-cluster-management-io:main Feb 10, 2026
11 checks passed
@skeeey
skeeey deleted the grpc-broker-resync branch February 10, 2026 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants