Skip to content
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

Implement Socket.DuplicateAndClose() for Windows #1858

Merged
merged 18 commits into from
Feb 4, 2020

Conversation

antonfirsov
Copy link
Member

@antonfirsov antonfirsov commented Jan 17, 2020

Fixes #1760.

The implementation is based on Framework code, with the following differences:

  • Breaking change: When trying to bind a socket to a ThreadPool (thus IOCP port), if the socket has been previously bound to IOCP, an InvalidOperationException is thrown, instead of an undocumented ArgumentException.
  • SocketInformation is not [Serializable] and does not contain a snapshot of RemoteEndpoint. Instead, the endpoint is being restored using getsockname in all cases.

_rightEndPoint = ep.Create(socketAddress);
}
catch
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks suspicious that we silently suppress all exceptions here. What is the motivation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I seems to be a bad pattern inherited from the old framework code. Would make sense to replace IPEndPointExtensions.Create() with a non-throwing IPEndPointExtensions.TryCreate(), but I did not want to apply changes of that extent in this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this fails, the socket will be in a bad state. I'd rather we just let it throw.

That said, I don't know why this would fail as I'd expect socket duplication to not work if the address is wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that apply for all call sites? If we change the pattern, it's better to alter usages everywhere.
I would prefer to change it in a separate clean-up PR everywhere rather than breaking the (bad) consistency on a single call site.

Copy link
Contributor

@scalablecory scalablecory Jan 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you point to what other code you're comparing to? UDP would function mostly fine like this but to my knowledge this would be the first API where we can have a connected socket without an endpoint set, and the other common connection-oriented APIs (send/recv) don't set it. LocalEndPoint and RemoteEndPoint would both just be null forever.

Failing fast here would be better than failing down the line with a null endpoint, especially if it's "free" because the exception is already getting thrown.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same exception swallowing pattern is present in Send/Receive implementations. I don't get the purpose of those calls though, since we are not doing anything with the remoteEp instances being returned.

Failing fast would be indeed better here, however the handling of (undocumented & impossiblish) exceptions coming from internals looks quite ad-hoc in existing code. Some call sites just let them leak out, others prefer to swallow them. Shouldn't be there a guideline point about this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the try-catch, but I'd really like to have better understanding. The code was taken from Framework.

Assert.Equal(blocking, clone.Blocking);
}

public class NotSupportedOnUnix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better not to nest one test class into another. It impairs readability.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A hierarchical pattern seems actually more readable for me in many cases: helps avoiding long test names, organizing stuff into groups instead. However in this particular case it would be definitely better to ungroup them.

I wonder if we have a written guideline for test code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I've seen this pattern elsewhere in our tests.

Our unit testing is modeled after a book... I believe The Art of Unit Testing. @davidsh can confirm.

@antonfirsov
Copy link
Member Author

I think I addressed most review findings, except the ones dealing with existing patterns (WSASocket return value and the dumb catch blocks).

Should I sneak fixes for those into this PR, or shall we do it separately instead?

@antonfirsov antonfirsov added the breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. label Jan 17, 2020
Copy link
Contributor

@scalablecory scalablecory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a couple things, otherwise looks good.

@antonfirsov
Copy link
Member Author

/azp run runtime-libraries outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Contributor

@jkotalik jkotalik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we been able to pass these bits off to internal teams to verify?

@@ -0,0 +1,52 @@
using System;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: copyright

{
socketInformation = new SocketInformation
{
ProtocolInformation = new byte[Interop.Winsock.WSAProtocolInfo.Size]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: stackalloc instead of newing up this array

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing public API, that uses byte[].

@antonfirsov
Copy link
Member Author

antonfirsov commented Jan 27, 2020

Pushed a change to deal with handle inheritance. The solution is not trivial because of the esoteric behavior of WSASocket. I had to use SetHandleInformation. (See comments.)

@jkotalik thanks for your engagement here, it prevented a bug!

@@ -59,6 +59,7 @@ internal ThreadPoolBoundHandle GetOrAllocateThreadPoolBoundHandle(bool trySkipCo
catch (Exception exception) when (!ExceptionCheck.IsFatal(exception))
{
bool closed = IsClosed;
bool alreadyBound = !IsInvalid && !IsClosed && (exception is ArgumentException);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't we get an ArgumentException as well if the handle wasn't opened for overlapped I/O? We're assuming that's the case then?

Copy link
Member Author

@antonfirsov antonfirsov Feb 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this based this on my understanding of the code in ClrThreadPoolBoundHandle.Windows.cs:

if (ex.HResult == HResults.E_HANDLE) // Bad handle
throw new ArgumentException(SR.Argument_InvalidHandle, nameof(handle));
if (ex.HResult == HResults.E_INVALIDARG) // Handle already bound or sync handle
throw new ArgumentException(SR.Argument_AlreadyBoundOrSyncHandle, nameof(handle));

I assume that the handle can not be invalid since we checked it before. Based on the comment, and the exception message, the only other possible reason is: "Handle already bound or sync handle". Let me know, if I'm missing something!

//
// This constructor works in conjunction with DuplicateAndClose, which is not supported on Unix.
// See comments in DuplicateAndClose.
//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we use this style of commenting elsewhere in System.Net.Sockets, with a blank comment line above and below? We've generally moved away from that elsewhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Occasionally yes, it was was used in the methods I changed. Getting rid of it.

}

if (_handle.IsInvalid)
{
Copy link
Member

@stephentoub stephentoub Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should dispose of state here that may have been created, namely the _handle. Even if it's invalid, it'll likely still trigger finalization. Probably want to null it out as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like an unnecessary check, since SocketPal.CreateSocket already validates handle. Added the disposal there, getting rid of this branch here.


public static (Socket, Socket) CreateConnectedSocketPair()
{
using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any potential difference in the handling of different address families? Wondering if we need tests for IPv6 sockets as well, or if Windows doesn't discriminate and a test for IPv4 sockets will be sufficient.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it looks quite arbitrary whether a functionality is also tested against IPV6 in socket test code. I added a few IPV6 test cases for the duplication to make sure it is covered. See the parameters of DuplicateAndClose_TcpServerHandler which is emulating most common user scenario.

Assert.Equal(TestMessage, receivedMessage);
}

[OuterLoop] // long-running
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What makes it long-running?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the same reason CreateSocket.CtorAndAccept_SocketNotKeptAliveViaInheritance is running for about 2 sec in debug on my PC. There are nested child processes + pipes. I'm also wondering why is it that much.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should figure out why it's taking 2 seconds. It shouldn't be that long.

Copy link
Member Author

@antonfirsov antonfirsov Feb 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this also concerns other tests, I'd rather take care of it later separately. I'm preparing a test fix PR anyways.

@@ -30,8 +30,8 @@ Global
{8CBA022C-635F-4C8D-9D29-CD8AAC68C8E6}.Debug|Any CPU.Build.0 = netcoreapp5.0-Windows_NT-Debug|Any CPU
{8CBA022C-635F-4C8D-9D29-CD8AAC68C8E6}.Release|Any CPU.ActiveCfg = netcoreapp5.0-Windows_NT-Release|Any CPU
{8CBA022C-635F-4C8D-9D29-CD8AAC68C8E6}.Release|Any CPU.Build.0 = netcoreapp5.0-Windows_NT-Release|Any CPU
{43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Debug|Any CPU.ActiveCfg = netcoreapp5.0-Unix-Debug|Any CPU
{43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Debug|Any CPU.Build.0 = netcoreapp5.0-Unix-Debug|Any CPU
{43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Debug|Any CPU.ActiveCfg = netcoreapp5.0-Windows_NT-Debug|Any CPU
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephentoub all other solutions default to Windows_NT. I suppose you altered this by accident in #2063.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I modified it, it was VS modifying it automatically. I'd need to look at the various guids, but there is a unix build in this project, so it would make sense for there to be unix entries here.

@antonfirsov
Copy link
Member Author

/azp run runtime-libraries outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@antonfirsov antonfirsov merged commit a716fa2 into dotnet:master Feb 4, 2020
@karelz karelz added this to the 5.0.0 milestone Aug 18, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 11, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Net.Sockets breaking-change Issue or PR that represents a breaking API or functional change over a prerelease.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reintroduce Socket.DuplicateAndClose() on Windows
8 participants