-
Notifications
You must be signed in to change notification settings - Fork 29
🐛 respawn cloudevents receiver after spontaneous Receive error #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
hbhushan3
wants to merge
1
commit into
open-cluster-management-io:main
Choose a base branch
from
hbhushan3:fix/receiver-goroutine-resubscribe
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package clients | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "sync" | ||
| "sync/atomic" | ||
| "testing" | ||
| "time" | ||
|
|
||
| cloudevents "github.com/cloudevents/sdk-go/v2" | ||
|
|
||
| "open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options" | ||
| "open-cluster-management.io/sdk-go/pkg/cloudevents/generic/utils" | ||
| ) | ||
|
|
||
| // recoveringTransport is a CloudEventTransport whose first Receive call returns | ||
| // a transient error spontaneously (without firing ErrorChan), and whose | ||
| // subsequent Receive calls block until the context is canceled. This models | ||
| // the production failure mode where an inbound gRPC subscribe stream returns | ||
| // e.g. codes.Canceled / codes.Unavailable while the transport-level connection | ||
| // monitor never observes the failure. | ||
| type recoveringTransport struct { | ||
| mu sync.Mutex | ||
| errCh chan error | ||
| receiveCalls atomic.Int32 | ||
| subscribeCalls atomic.Int32 | ||
| } | ||
|
|
||
| func newRecoveringTransport() *recoveringTransport { | ||
| return &recoveringTransport{errCh: make(chan error, 1)} | ||
| } | ||
|
|
||
| func (t *recoveringTransport) Connect(ctx context.Context) error { return nil } | ||
| func (t *recoveringTransport) Send(ctx context.Context, evt cloudevents.Event) error { | ||
| return nil | ||
| } | ||
| func (t *recoveringTransport) Subscribe(ctx context.Context) error { | ||
| t.subscribeCalls.Add(1) | ||
| return nil | ||
| } | ||
|
|
||
| func (t *recoveringTransport) Receive(ctx context.Context, fn options.ReceiveHandlerFn) error { | ||
| n := t.receiveCalls.Add(1) | ||
| if n == 1 { | ||
| // First invocation: return a transient error spontaneously. | ||
| // Mirrors the behavior of the gRPC transport when the inbound | ||
| // subscribe stream is canceled mid-flight without the client-side | ||
| // connection monitor detecting it via ErrorChan. | ||
| return fmt.Errorf("rpc error: code = Canceled desc = context canceled") | ||
| } | ||
| // Subsequent invocations: block until the receiver context is canceled. | ||
| <-ctx.Done() | ||
| return ctx.Err() | ||
| } | ||
|
|
||
| func (t *recoveringTransport) Close(ctx context.Context) error { return nil } | ||
| func (t *recoveringTransport) ErrorChan() <-chan error { return t.errCh } | ||
|
|
||
| // TestReceiverRecoveryAfterSpontaneousReceiveError verifies that when | ||
| // transport.Receive returns an error on its own (without the transport's | ||
| // ErrorChan firing), the receiver lifecycle does not silently stay dead. | ||
| // The baseClient must trigger a resubscribe so that a new Receive goroutine | ||
| // is spawned and event processing resumes. | ||
| // | ||
| // Regression test for the production issue where ARO HCP's aro-hcp-backend | ||
| // stopped processing maestro bundle status updates for hours because the | ||
| // receiver goroutine launched inside (*baseClient).subscribe exited on a | ||
| // transient gRPC Canceled error and was never restarted. | ||
| func TestReceiverRecoveryAfterSpontaneousReceiveError(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| tr := newRecoveringTransport() | ||
| c := newBaseClient("test-client", tr, utils.EventRateLimit{}) | ||
|
|
||
| if err := c.connect(ctx); err != nil { | ||
| t.Fatalf("connect: %v", err) | ||
| } | ||
|
|
||
| c.subscribe(ctx, func(ctx context.Context, evt cloudevents.Event) {}) | ||
|
|
||
| // Wait for at least two Receive invocations: the first returns the | ||
| // transient error, the second proves recovery (a new Receive goroutine | ||
| // was spawned via the resubscribe path). | ||
| deadline := time.Now().Add(10 * time.Second) | ||
| for time.Now().Before(deadline) { | ||
| if tr.receiveCalls.Load() >= 2 { | ||
| return | ||
| } | ||
| time.Sleep(20 * time.Millisecond) | ||
| } | ||
|
|
||
| t.Fatalf("receiver did not recover: receive=%d subscribe=%d (expected receive>=2)", | ||
| tr.receiveCalls.Load(), tr.subscribeCalls.Load()) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: open-cluster-management-io/sdk-go
Length of output: 50390
🏁 Script executed:
Repository: open-cluster-management-io/sdk-go
Length of output: 3815
Call
transport.Close(ctx)in the recovery path before re-triggering subscribe.When a transport's
Receive()returns spontaneously (e.g., transient gRPC Unavailable while parent context is alive), the recovery code sendsstopReceiverSignaland re-tickles subscribe. However, it does not calltransport.Close(), leaving the previousOpenInbound()goroutine stuck.In the gRPC protocol implementation,
OpenInbound()blocks at a select waiting for eithersubCtx.Done()orcloseChan(line 145). IfClose()is not called,closeChanis never signaled, soOpenInbound()never reaches thecancel()call (line 149) that terminates the receiver and heartbeat watcher goroutines. This results in:Add
c.transport.Close(receiverCtx)before sending the resubscribe signal to ensure proper cleanup of the previous subscription's internal goroutines and connection.🤖 Prompt for AI Agents