-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CSHARP-5798: Implement test to track if any UnobservedTaskExceptions were raised while test run #1836
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
Conversation
…were raised while test run
| } | ||
| } | ||
|
|
||
| static void EnsureConnected(Socket socket) |
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.
Implementation of the method is inspired by the recommented by Microsoft way to check if the socket is alive:
https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.connected?view=net-10.0
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.
Pull request overview
This PR implements a fix for a corner case on .NET 6 macOS where socket disposal during connection can cause Connect to complete without errors, leaving the socket in a disposed state that causes subsequent operations to fail. The fix introduces verification that the socket is truly connected and prevents unnecessary disposal when the connection succeeds.
Key changes:
- Added
EnsureConnectedmethod to verify socket connectivity afterConnectcompletes - Introduced
ConnectOperationStateto track connection success and prevent disposal of successfully connected sockets - Refactored cancellation callback to check connection state before disposing socket
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| bool originalBlockingState = socket.Blocking; | ||
| socket.Blocking = false; | ||
|
|
||
| try | ||
| { | ||
| // Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal. | ||
| socket.Send(__ensureConnectedBuffer, 0, 0); | ||
| } | ||
| finally | ||
| { | ||
| // Restore original blocking state | ||
| socket.Blocking = originalBlockingState; |
Copilot
AI
Dec 8, 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 socket's blocking state is not restored if accessing socket.Blocking throws an ObjectDisposedException. If the socket is already disposed when entering EnsureConnected, the getter for originalBlockingState will throw, but the finally block won't execute to restore the state (though this is moot since the socket is disposed). More critically, if the socket gets disposed between line 229 and the try block, the finally block will attempt to set socket.Blocking on a disposed socket. Consider wrapping the entire method body in a try-catch to handle ObjectDisposedException gracefully.
| bool originalBlockingState = socket.Blocking; | |
| socket.Blocking = false; | |
| try | |
| { | |
| // Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal. | |
| socket.Send(__ensureConnectedBuffer, 0, 0); | |
| } | |
| finally | |
| { | |
| // Restore original blocking state | |
| socket.Blocking = originalBlockingState; | |
| try | |
| { | |
| bool originalBlockingState = socket.Blocking; | |
| socket.Blocking = false; | |
| try | |
| { | |
| // Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal. | |
| socket.Send(__ensureConnectedBuffer, 0, 0); | |
| } | |
| finally | |
| { | |
| // Restore original blocking state | |
| socket.Blocking = originalBlockingState; | |
| } | |
| } | |
| catch (ObjectDisposedException) | |
| { | |
| // Socket is already disposed, nothing to do. |
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.
It's OK for the method to throw ObjectDisposedExcpetion, we have try-catch in the outer code.
| // Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal. | ||
| socket.Send(__ensureConnectedBuffer, 0, 0); |
Copilot
AI
Dec 8, 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.
Sending 0 bytes may not reliably detect a disposed socket on all platforms. Consider documenting why this specific approach was chosen for the macOS .NET 6 issue, or explore alternative detection methods such as checking socket.Connected property or using socket.Poll() with a zero timeout. The current implementation's effectiveness should be validated across different platforms.
| // Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal. | |
| socket.Send(__ensureConnectedBuffer, 0, 0); | |
| // Use Poll and Connected to ensure the socket is actually connected. | |
| // This is a cross-platform way to check for a disposed or disconnected socket. | |
| if (!socket.Connected || !socket.Poll(0, SelectMode.SelectWrite)) | |
| { | |
| throw new SocketException((int)SocketError.NotConnected); | |
| } |
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.
Disagree. Sending the 0-bytes is done as per Microsoft's suggestion. Also socket.Connected does not makes any checks, but returns results based on the last operation results. Problem with socket.Poll() - we need to provide a small-timeout, what value should we use?
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.
That's OK. If ensuring the socket is alive takes too long - we want to interrupt the operation by disposing the socket.
| using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(state => | ||
| { | ||
| var operationState = (ConnectOperationState)state; | ||
| if (operationState.IsSucceeded) | ||
| { | ||
| return; | ||
| } | ||
| DisposeSocket(operationState.Socket); | ||
| }, callbackState); |
Copilot
AI
Dec 8, 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.
There's a potential race condition between setting callbackState.IsSucceeded = true on line 199 and checking operationState.IsSucceeded on line 176. If cancellation occurs between EnsureConnected completing and IsSucceeded being set, the socket could be disposed even though the connection succeeded. Consider setting IsSucceeded = true before calling EnsureConnected, or use a lock/interlocked operation to ensure thread-safe state updates.
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.
We cannot be sure if the connection was really established until we done the aliveness check, so we cannot set the value before calling EnsureConnected.
…were raised while test run
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.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| static void EnsureConnected(Socket socket) | ||
| { | ||
| bool originalBlockingState = socket.Blocking; |
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.
nit: var
| #else | ||
| socket.Connect(endPoint); | ||
| #endif | ||
| if (!callbackState.TryChangeStatusFromInProgress(OperationCallbackState<Socket>.OperationStatus.Done)) |
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.
Maybe add a unittest for this with mock socket?
BorisDog
left a comment
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.
LGTM
Fix for the corner case on net6 on MacOS, when disposal of socket while connecting makes Connect to complete without any errors, but the socket is being Disposed and any next operation will fail with ObjectDisposedException.
It might be related to this issue: dotnet/runtime#75889