Skip to content

fix: tolerate missing/invalidated stream subscription handles in grain activation#169

Open
d-barker wants to merge 1 commit into
OrleansContrib:masterfrom
d-barker:bugfix/client-and-group-grain-throws-on-activate
Open

fix: tolerate missing/invalidated stream subscription handles in grain activation#169
d-barker wants to merge 1 commit into
OrleansContrib:masterfrom
d-barker:bugfix/client-and-group-grain-throws-on-activate

Conversation

@d-barker

@d-barker d-barker commented Jun 16, 2026

Copy link
Copy Markdown

SignalR.Orleans grains stored stream subscription handles and, on reactivation, indexed the result of GetAllSubscriptionHandles() with [0]. When a silo restarts (or a subscription was never persisted), GetAllSubscriptionHandles() returns an empty collection, so [0] threw IndexOutOfRangeException and the grain failed to activate.

ClientGrain had the same issue as the connection groups grain; where reactivation would fail with an empty collection.

Changes:

  • ConnectionGroupGrain (await ...GetAllSubscriptionHandles())[0] with a private GetSubscriptionHandleAsync(...) helper that returns handles.FirstOrDefault().
  • ClientGrain: (await ...GetAllSubscriptionHandles())[0] with a private GetSubscriptionHandleAsync(...) helper that returns handles.FirstOrDefault().
  • On activation, if no handle is found, log a warning and re-subscribe to the client/server disconnect stream instead of throwing.

Files:

  • src/SignalR.Orleans/Clients/ClientGrain.cs
  • src/SignalR.Orleans/ConnectionGroups/ConnectionGroupGrain.cs

refs: #168

…n activation

SignalR.Orleans grains stored stream subscription handles and, on reactivation,
indexed the result of GetAllSubscriptionHandles() with [0]. When a silo
restarts (or a subscription was never persisted), GetAllSubscriptionHandles()
returns an empty collection, so [0] threw IndexOutOfRangeException and the grain
failed to activate.

ClientGrain additionally mis-handled the resume path: it assigned the resumed
handle to a *local* variable that shadowed the field, so the live handle was
never stored. Because ResumeAsync() invalidates the handle it is called on and
returns a new one, the next OnDisconnect() called UnsubscribeAsync() on a stale
handle and threw "Handle is no longer valid. It has been used to unsubscribe or
resume."

Changes:
- Replace `(await ...GetAllSubscriptionHandles())[0]` with a private
  GetSubscriptionHandleAsync(...) helper that returns handles.FirstOrDefault().
- On activation, if no handle is found, log a warning and re-subscribe to the
  client/server disconnect stream instead of throwing.
- ClientGrain: capture and store the new handle returned by ResumeAsync() in the
  _serverDisconnectedSubscription field (fixes the shadowed-local bug and the
  "Handle is no longer valid" exception on disconnect).

Files:
- src/SignalR.Orleans/Clients/ClientGrain.cs
- src/SignalR.Orleans/ConnectionGroups/ConnectionGroupGrain.cs
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.

1 participant