Restore blocking mode after successful ConnectAsync on Unix#124200
Restore blocking mode after successful ConnectAsync on Unix#124200liveans wants to merge 2 commits intodotnet:mainfrom
Conversation
|
Tagging subscribers to this area: @karelz, @dotnet/ncl |
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs
Show resolved
Hide resolved
🤖 Copilot Code Review — PR #124200Holistic AssessmentMotivation: The optimization goal is valid. After Approach: The implementation adds Summary: Detailed Findings
|
ba10f3d to
49a3f53
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs:688
- The InvokeCallback override only invokes the callback and calls RestoreBlocking when buffer.Length is 0. However, when a connect operation fails (ErrorCode != Success) and a buffer was provided (via DoOperationConnectEx), the callback will never be invoked and RestoreBlocking won't be called.
This can occur when using ConnectEx with a buffer on Unix systems. The callback must always be invoked when the operation is complete, regardless of whether there's a buffer or whether the operation succeeded or failed. RestoreBlocking should also be called on error paths to restore the socket to blocking mode after a failed connection attempt.
Consider updating the logic to:
- Always call RestoreBlocking when the connect fails (ErrorCode != Success), regardless of buffer.Length
- Always invoke the callback when the operation is complete and no follow-up Send is needed
public override void InvokeCallback(bool allowPooling)
{
var cb = Callback!;
int bt = BytesTransferred;
Memory<byte> sa = SocketAddress;
SocketError ec = ErrorCode;
Memory<byte> buffer = Buffer;
if (buffer.Length == 0)
{
AssociatedContext._socket.RestoreBlocking();
// Invoke callback only when we are completely done.
// In case data were provided for Connect we may or may not send them all.
// If we did not we will need follow-up with Send operation
cb(bt, sa, SocketFlags.None, ec);
}
}
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
49a3f53 to
b8c9054
Compare
|
/azp run runtime-libraries-coreclr outerloop |
|
Azure Pipelines successfully started running 1 pipeline(s). |
b8c9054 to
387bea9
Compare
|
/azp run runtime-libraries-coreclr outerloop |
|
Azure Pipelines successfully started running 1 pipeline(s). |
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Show resolved
Hide resolved
787ddea to
ffc7528
Compare
ffc7528 to
5e1064e
Compare
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.Unix.cs
Outdated
Show resolved
Hide resolved
5e1064e to
38e662f
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
38e662f to
b8d76ea
Compare
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.Unix.cs
Outdated
Show resolved
Hide resolved
When ConnectAsync is called with a non-empty buffer, use OnSendComplete wrapper to call RestoreBlocking after the follow-up SendToAsync completes. Also invoke the callback on failed connect with buffer (pre-existing bug). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| using Socket accepted = listener.Accept(); | ||
|
|
||
| await client.SendAsync(new byte[] { 1, 2, 3 }, SocketFlags.None); |
There was a problem hiding this comment.
using Socket accepted = listener.Accept(); declares accepted but never uses it, which will produce an unused local warning (treated as error in this repo) and break the build. Either use the variable (e.g., assert/not-null or perform a read/write on it) or avoid the local entirely while still ensuring the accepted socket is disposed.
| if (errorCode == SocketError.Success && remains > 0) | ||
| { | ||
| errorCode = SendToAsync(buffer.Slice(sentBytes), 0, remains, SocketFlags.None, Memory<byte>.Empty, ref sentBytes, callback!, default); | ||
| } | ||
|
|
||
| if (buffer.Length == 0 || errorCode != SocketError.IOPending) | ||
| { | ||
| _socket.RestoreBlocking(); | ||
| } |
There was a problem hiding this comment.
In the fast-path where TryStartConnect succeeds and buffer.Length > 0, the code calls SendToAsync(..., callback) directly. If that send returns SocketError.IOPending, completion will happen via the send operation callback, but RestoreBlocking() will never be invoked (the current method only restores when errorCode != IOPending). To match the PR intent, ensure the callback used for this send path restores blocking before invoking the original completion callback.
| @@ -678,11 +687,19 @@ | |||
|
|
|||
| if (buffer.Length == 0) | |||
| { | |||
| AssociatedContext._socket.RestoreBlocking(); | |||
|
|
|||
| // Invoke callback only when we are completely done. | |||
| // In case data were provided for Connect we may or may not send them all. | |||
| // If we did not we will need follow-up with Send operation | |||
| cb(bt, sa, SocketFlags.None, ec); | |||
| } | |||
| else if (ec != SocketError.Success) | |||
| { | |||
| // Connect failed — SendToAsync was never started. | |||
| AssociatedContext._socket.RestoreBlocking(); | |||
| cb(bt, sa, SocketFlags.None, ec); | |||
| } | |||
| } | |||
There was a problem hiding this comment.
ConnectOperation.DoTryComplete starts SendToAsync when connect completes and Buffer.Length > 0, but it doesn't handle the case where SendToAsync completes synchronously (SocketError.Success). In that case, the send callback isn't invoked, and ConnectOperation.InvokeCallback also won't invoke the completion callback (because buffer.Length > 0 && ec == Success), so the operation can complete without any user callback and without restoring blocking. Handle synchronous send completion explicitly (e.g., by treating it as fully done and invoking the completion callback / restoring blocking, or by updating state so InvokeCallback runs the completion path).
Summary
On Unix, after a successful
ConnectAsynccompletion, this PR restores the underlying socket to blocking mode if the user hasn't explicitly setSocket.Blocking = false. Thisoptimizes subsequent synchronous operations (
Send/Receive) by using native blocking syscalls instead of emulating blocking on top of epoll/kqueue.When
SendAsync/ReceiveAsync(or other async I/O operations) are called later, the socket is switched back to non-blocking mode via the existingSetHandleNonBlocking()mechanism.
Motivation
Currently, once any async operation is performed on a Unix socket, the underlying file descriptor is permanently set to non-blocking mode via
fcntl(O_NONBLOCK). Subsequentsynchronous operations must then emulate blocking behavior in managed code using epoll/kqueue, which has performance overhead compared to native blocking syscalls.
This change benefits the common usage pattern: