Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Enable instance Socket.{Begin}Connect{Async} methods on Unix #16373

Merged
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:socket_connect
Feb 25, 2017
Merged

Enable instance Socket.{Begin}Connect{Async} methods on Unix #16373
stephentoub merged 3 commits into
dotnet:masterfrom
stephentoub:socket_connect

Conversation

@stephentoub

Copy link
Copy Markdown
Member

BSD Sockets don't allow using a socket for a connection that has already failed in a previous connect. This has caused us to significantly hamper the Socket.{Begin}Connect{Async} instance methods on Socket, in two ways:

  1. If you call any instance connect overload and it fails, whereas on Windows you can try to connect again, on Unix we throw a PlatformNotSupportedException.
  2. If you call any instance connect method that could potentially try to connect to multiple addresses (e.g. a string host, an IPAddress[], or a DnsEndPoint), whereas on Windows that allowed, on Unix we throw a PlatformNotSupportedException (because after trying the first address we will then be trying to connect again on a socket that failed to connect).

There are multiple options for dealing with this, all with their own pros and cons:
A) Use a "shadow" socket to first try to make the connection, and if it succeeds, assume that another connection will succeed and try to make it on the "real" socket. We actually originally went down this path prior to .NET Core 1.0 and it had significant problems, both with performance (multiple connections any time a single one was needed) and with server expectations (getting two connections when only one was expected).
B) Prohibit usage of any {Begin}Connect{Async} APIs that could potentially deal with multiple addresses. This is what we've shipped in .NET Core 1.x.
C) In such APIs, check whether the number of addresses is exactly 1, allowing it if there is, throwing otherwise. This has several issues, including problems with dual mode (e.g. localhost will often resolve to both ::1 and 127.0.0.1), non-determinism based on how many addresses a server resolves to, etc.
D) Regardless of the number of addresses, pretend there was only one. This has similar issues to (C).
E) Use a "shadow" socket to make each connection, and then on successful connection substitute it in for the original underneath the Socket. This enables trying to connect to multiple addresses, but it means that any configuration performed on the initial socket won't be applied to the subsequent sockets.
F) Same as (E), but keeping track of some dialable set of options that can be propagated from socket to socket.

This commit implements (F). (F) still isn't perfect, but it allows the common path (minimal configuration) to work while still supporting multiple endpoints and being deterministic:

  • SafeCloseSocket on Unix now tracks whether the Socket.Handle property was accessed and whether any non-tracked configuration was set. If any of that is done, then as with today. We were already tracking (in order to provide an error message) whether a connect attempt failed; that was previously tracked on the SocketAsyncContext, now it's tracked on the SafeCloseSocket handle.
  • Whenever an attempt is made to {Begin}Connect{Async} with multiple possible endpoints, we check whether the handle was exposed or untracked configuration was used. If it was, we fail as we do today. If it wasn't, we allow it.
  • When making a connection (either via a user's call, or as part of a multi-endpoint connect operation), if there was a previously failed connection attempt, we replace the handle with a new one, transferring over the tracked state from the old handle to the new one.
  • Removed a bunch of stale code leftover from when approach (A) above was implemented; apparently when we switched to (B) this code was never removed
  • Removed now defunct [PlatformSpecific] attribution, and added some more tests

With (F), we also no longer need the workaround we had in TcpClient, so I recombined it back down to a single implementation built on top of Socket. This means, for example, that whereas previously on Unix you couldn't use TcpClient's instance Connect methods with a DNS endpoint after accessing its Client Socket, now you can. And we don't have to do any of the shadow value tracking we were previously doing.

In addition, I took care of some other minor cleanup I spotted along the way:

  • One or two cases where API usage was ifdef'd out; now that these APIs are back for 2.0, we can use them again, e.g. IPAddress.Address
  • Fixed a few places where exception stacks were being lost due to exceptions getting rethrown; changed these to use ExceptionDispatchInfo to maintain the stacks (this helped me to debug a few things better, which is how I noticed in the first place)
  • Removed unnecessary pragma usage that was a holdover from older versions of the code
  • Adding code to a bunch of tests to Dispose of sockets
  • Adding a few more tests (there's still a lot of uncovered code in Sockets unfortunately; we really need to address that, separately)

Fixes https://github.com/dotnet/corefx/issues/8768
cc: @NickCraver, @tmds, @glennc, @geoffkizer, @danroth27, @richlander, @pgavlin, @halter73

@stephentoub

Copy link
Copy Markdown
Member Author

@dotnet-bot test outerloop CentOS7.1 Debug
@dotnet-bot test outerloop OpenSUSE42.1 Debug
@dotnet-bot test outerloop PortableLinux Debug
@dotnet-bot test outerloop RHEL7.2 Debug
@dotnet-bot test outerloop Ubuntu14.04 Release
@dotnet-bot test outerloop Ubuntu16.10 Debug
@dotnet-bot test outerloop OSX Debug
@dotnet-bot test outerloop Windows 10 Debug

@stephentoub

Copy link
Copy Markdown
Member Author

@geoffkizer, @pgavlin, @CIPop, could you guys help review this?

@NickCraver NickCraver left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is awesome - the huge amount of effort here is much appreciated. I left 2 minor comments on a nit and connect allocations for multi-family, otherwise very much looking forward to this landing.

// replicating the socket's file descriptor appropriately. Similarly, if it's
// only targeting a single address, but it already experienced a failure in a
// previous connect call, then this is logically part of a multi endpoint connect,
// and the same logic applies. Either wya, in such a situation we throw.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

way*

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Will fix

if (Socket.OSSupportsIPv6)
{
ipv6Socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this spin up both early, or do so more lazily underneath? e.g. if the first address is one family, we're allocating a socket for the other family needlessly. I'd imagine that's the majority case - the first attempt succeeding.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Probably. This is how it was in Windows and in desktop, but I don't currently see any reason why it can't be made lazy. Will fix.

if (_handle.IsTrackedOption(TrackedSocketOptions.ReceiveTimeout)) receiveTimeout = ReceiveTimeout;
if (_handle.IsTrackedOption(TrackedSocketOptions.SendBufferSize)) sendSize = SendBufferSize;
if (_handle.IsTrackedOption(TrackedSocketOptions.SendTimeout)) sendTimeout = SendTimeout;
if (_handle.IsTrackedOption(TrackedSocketOptions.Ttl)) ttl = Ttl;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why read all the values out, create the new socket, and then put all the values in the new one? Seems like it would be simpler to create the new socket and then copy each one individually.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

All of these options are properties on Socket, some with a fair amount of non-trivial code behind them. Getting the value is easy via the property, but doing so accesses the underlying handle, and there can of course be only one handle at a time. So to do what you propose, I'd need to skip using these properties and instead duplicate all of the code necessary to get/set all of these directly against the handle.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ah, now I understand. Thanks.

break;
}
catch (Exception ex)
catch (Exception ex) when (!ExceptionCheck.IsFatal(ex))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we need the check for IsFatal? I thought we were going to get rid of this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'd be fine deleting them all in a separate pass (IIRC at this point IsFatal really just checks for OOMs). For now I'll keep it to avoid changing any of that logic in this PR; all I did here was take advantage of exception filters to neaten up the code slightly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ok by me. Separately, do you think these should be deleted? If so can we file an issue?

if (error != SocketError.Success && error != SocketError.WouldBlock)
{
_connectFailed = true;
_socket.LastConnectFailed = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since this function modifies state on the SafeCloseSocket, would it make more sense for it to live on that class?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Probably. I'll move it unless there's any good reason to keep it here.

@geoffkizer

Copy link
Copy Markdown

Generally LGTM. A few nits above.

@stephentoub

Copy link
Copy Markdown
Member Author

Thanks for the reviews, @NickCraver and @geoffkizer.

BSD Sockets don't allow using a socket for a connection that has already failed in a previous connect.  This has caused us to significantly hamper the Socket.{Begin}Connect{Async} instance methods on Socket, in two ways:
1) If you call any instance connect overload and it fails, whereas on Windows you can try to connect again, on Unix we throw a PlatformNotSupportedException.
2) If you call any instance connect method that could potentially try to connect to multiple addresses (e.g. a string host, an IPAddress[], or a DnsEndPoint), whereas on Windows that allowed, on Unix we throw a PlatformNotSupportedException (because after trying the first address we will then be trying to connect again on a socket that failed to connect).

There are multiple options for dealing with this, all with their own pros and cons:
A) Use a "shadow" socket to first try to make the connection, and if it succeeds, assume that another connection will succeed and try to make it on the "real" socket.  We actually originally went down this path prior to .NET Core 1.0 and it had significant problems, both with performance (multiple connections any time a single one was needed) and with server expectations (getting two connections when only one was expected).
B) Prohibit usage of any {Begin}Connect{Async} APIs that could potentially deal with multiple addresses.  This is what we've shipped in .NET Core 1.x.
C) In such APIs, check whether the number of addresses is exactly 1, allowing it if there is, throwing otherwise.  This has several issues, including problems with dual mode (e.g. localhost will often resolve to both ::1 and 127.0.0.1), non-determinism based on how many addresses a server resolves to, etc.
D) Regardless of the number of addresses, pretend there was only one.  This has similar issues to (C).
E) Use a "shadow" socket to make each connection, and then on successful connection substitute it in for the original underneath the Socket.  This enables trying to connect to multiple addresses, but it means that any configuration performed on the initial socket won't be applied to the subsequent sockets.
F) Same as (E), but keeping track of some dialable set of options that can be propagated from socket to socket.

This commit implements (F).  (F) still isn't perfect, but it allows the common path (minimal configuration) to work while still supporting multiple endpoints and being deterministic:
- SafeCloseSocket on Unix now tracks whether the Socket.Handle property was accessed and whether any non-tracked configuration was set.  If any of that is done, then as with today.  We were already tracking (in order to provide an error message) whether a connect attempt failed; that was previously tracked on the SocketAsyncContext, now it's tracked on the SafeCloseSocket handle.
- Whenever an attempt is made to {Begin}Connect{Async} with multiple possible endpoints, we check whether the handle was exposed or untracked configuration was used.  If it was, we fail as we do today.  If it wasn't, we allow it.
- When making a connection (either via a user's call, or as part of a multi-endpoint connect operation), if there was a previously failed connection attempt, we replace the handle with a new one, transferring over the tracked state from the old handle to the new one.
- Removed a bunch of stale code leftover from when approach (A) above was implemented; apparently when we switched to (B) this code was never removed
- Removed now defunct [PlatformSpecific] attribution, and added some more tests

In addition, I took care of some other minor cleanup I spotted along the way:
- One or two cases where API usage was ifdef'd out; now that these APIs are back for 2.0, we can use them again, e.g. IPAddress.Address
- Fixed a few places where exception stacks were being lost due to exceptions getting rethrown; changed these to use ExceptionDispatchInfo to maintain the stacks (this helped me to debug a few things better, which is how I noticed in the first place)
- Removed unnecessary pragma usage that was a holdover from older versions of the code
- Adding code to a bunch of tests to Dispose of sockets
We previously had to specialize TcpClient for Unix to enable {Begin}Connect{Async} to support multiple endpoints.  Now that Socket does implicitly, we can remove that specialization, and have a single implementation for TcpClient (TcpClient only exposes surface area to tweak configuration tracked by the Unix Socket implementation).  In doing so, this further opens up TcpClient usage on Unix, e.g. previously accessing the Client Socket prior to calling Connect would disable the ability to use multiple endpoints, but now it works.
@stephentoub

Copy link
Copy Markdown
Member Author

@dotnet-bot test outerloop CentOS7.1 Debug
@dotnet-bot test outerloop OpenSUSE42.1 Debug
@dotnet-bot test outerloop PortableLinux Debug
@dotnet-bot test outerloop RHEL7.2 Debug
@dotnet-bot test outerloop Ubuntu14.04 Release
@dotnet-bot test outerloop Ubuntu16.10 Debug
@dotnet-bot test outerloop OSX Debug
@dotnet-bot test outerloop Windows 10 Debug

@stephentoub

Copy link
Copy Markdown
Member Author

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restore Socket.ConnectAsync via hostname on *nix platforms.

5 participants