-
Notifications
You must be signed in to change notification settings - Fork 164
Unregister event handlers in RpcTargetInfo.DisposeAsync #1379
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -150,6 +150,80 @@ public void GenericServerEventSubscriptionLifetime() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.Null(this.server.ServerEventWithCustomArgsAccessor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Verifies that event handlers are unregistered when multiple connections are established and disposed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// This tests for a memory leak where event handlers would accumulate if not properly unregistered on disposal. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void EventHandlersUnregisteredOnMultipleConnectionDisposals() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use a shared server object across multiple connections to simulate the real-world scenario | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sharedServer = new Server(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Verify no handlers initially | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.Null(sharedServer.ServerEventAccessor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.Null(sharedServer.ServerEventWithCustomArgsAccessor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create and dispose multiple connections | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (int i = 0; i < 3; i++) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var streams = FullDuplexStream.CreateStreams(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var rpc = this.CreateJsonRpcWithTargetObject(streams.Item1, sharedServer); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rpc.StartListening(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Verify handler is registered | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.NotNull(sharedServer.ServerEventAccessor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.NotNull(sharedServer.ServerEventWithCustomArgsAccessor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Count the number of handlers attached | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int serverEventHandlerCount = sharedServer.ServerEventAccessor?.GetInvocationList().Length ?? 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int customArgsEventHandlerCount = sharedServer.ServerEventWithCustomArgsAccessor?.GetInvocationList().Length ?? 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Should only have one handler per event, not accumulating | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.Equal(1, serverEventHandlerCount); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.Equal(1, customArgsEventHandlerCount); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Dispose the connection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rpc.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rpc.Dispose(); | |
| rpc.Dispose(); | |
| streams.Item1.Dispose(); | |
| streams.Item2.Dispose(); |
Copilot
AI
Dec 18, 2025
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.
Local scope variable 'serverRpc' shadows TargetObjectEventsTests.serverRpc.
Copilot
AI
Dec 18, 2025
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.
Local scope variable 'clientRpc' shadows TargetObjectEventsTests.clientRpc.
| var clientRpc = new JsonRpc(streams.Item2); | |
| serverRpc.StartListening(); | |
| clientRpc.StartListening(); | |
| var localClientRpc = new JsonRpc(streams.Item2); | |
| serverRpc.StartListening(); | |
| localClientRpc.StartListening(); |
Copilot
AI
Dec 18, 2025
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.
The serverRpc and streams.Item1 should be explicitly disposed after the test completes to prevent resource leaks. Consider adding cleanup in a finally block or using a using statement.
| serverRpc.StartListening(); | |
| clientRpc.StartListening(); | |
| // Verify handler is registered | |
| Assert.NotNull(sharedServer.ServerEventAccessor); | |
| // Simulate connection drop by closing the stream without disposing JsonRpc | |
| streams.Item2.Dispose(); | |
| // Wait for the disconnection to be detected | |
| await serverRpc.Completion.WithCancellation(this.TimeoutToken); | |
| // Verify handlers are unregistered after stream closure | |
| Assert.Null(sharedServer.ServerEventAccessor); | |
| try | |
| { | |
| serverRpc.StartListening(); | |
| clientRpc.StartListening(); | |
| // Verify handler is registered | |
| Assert.NotNull(sharedServer.ServerEventAccessor); | |
| // Simulate connection drop by closing the stream without disposing JsonRpc | |
| streams.Item2.Dispose(); | |
| // Wait for the disconnection to be detected | |
| await serverRpc.Completion.WithCancellation(this.TimeoutToken); | |
| // Verify handlers are unregistered after stream closure | |
| Assert.Null(sharedServer.ServerEventAccessor); | |
| } | |
| finally | |
| { | |
| clientRpc.Dispose(); | |
| serverRpc.Dispose(); | |
| streams.Item1.Dispose(); | |
| streams.Item2.Dispose(); | |
| } |
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.
The call to UnregisterEventHandlersFromTargetObjects should be synchronized with SyncObject to ensure thread safety. The eventReceivers field is accessed and modified under the SyncObject lock elsewhere in the code (see AddLocalRpcTarget at line 166 and RevertAddLocalRpcTarget.Dispose at line 350), but this call occurs outside any lock. This could lead to race conditions if event handlers are being added concurrently during disposal.