Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Modified Dns.GetHostAddressesAsync to be truly async #26850

Merged
merged 10 commits into from
Feb 12, 2018

Conversation

JeffCyr
Copy link

@JeffCyr JeffCyr commented Feb 5, 2018

PR for issue dotnet/runtime#17224 (originally: https://github.com/dotnet/corefx/issues/8371)

Modified Dns.GetHostAddressesAsync to use GetAddrInfoEx when supported.

@dnfclas
Copy link

dnfclas commented Feb 5, 2018

CLA assistant check
All CLA requirements met.

[In] ref NativeOverlapped overlapped,
[In] GetAddrInfoExCompletionCallback callback,
[Out] out IntPtr cancelHandle
);
Copy link
Member

@stephentoub stephentoub Feb 5, 2018

Choose a reason for hiding this comment

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

Nit: We try to use the same names as in the native definition, so e.g.
name => pName
serviceName => pServiceName
namespaceId => lpNspId
etc.

{
internal unsafe delegate void GetAddrInfoExCompletionCallback([In] int error, [In] int bytes, [In] NativeOverlapped* overlapped);

[DllImport(Interop.Libraries.Ws2_32, ExactSpelling = true, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)]
Copy link
Member

Choose a reason for hiding this comment

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

Why the ThrowOnUnmappableChar = true? That's pretty rare to see.

Copy link
Author

Choose a reason for hiding this comment

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

I copied theses properties from the getaddrinfo declaration without thinking. It doesn't make sense since the call is Unicode, I will remove ThrowOnUnmappableChar and BestFitMapping properties.

[In] string serviceName,
[In] int namespaceId,
[In] IntPtr providerId,
[In] ref AddressInfoEx hints,
Copy link
Member

Choose a reason for hiding this comment

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

Can this use in instead of ref?

Copy link
Author

Choose a reason for hiding this comment

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

The build is passing but Visual Studio is complaining with "Feature 'readonly references' is not available in C# 7.0. Please use language version 7.2 or greater."

I suggest leaving 'ref', I don't think it would change anything for a p/invoke call anyway.

{
internal static partial class Winsock
{
internal unsafe delegate void GetAddrInfoExCompletionCallback([In] int error, [In] int bytes, [In] NativeOverlapped* overlapped);
Copy link
Member

@stephentoub stephentoub Feb 5, 2018

Choose a reason for hiding this comment

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

Nit:
GetAddrInfoExCompletionCallback => LPLOOKUPSERVICE_COMPLETION_ROUTINE
error => dwError
bytes => dwBytes
etc.

[Out] out IntPtr cancelHandle
);

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
Copy link
Member

Choose a reason for hiding this comment

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

These ReliabilityContract attributes don't have a meaning in .NET Core and can be removed.


[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[DllImport("ws2_32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern int GetAddrInfoExCancel([In] ref IntPtr cancelHandle);
Copy link
Member

Choose a reason for hiding this comment

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

Same ref => in question.

CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default);
if (NameResolutionPal.SupportsGetAddrInfoAsync && includeIPv6 && SocketProtocolSupportPal.OSSupportsIPv6 && address == null)
Copy link
Member

@stephentoub stephentoub Feb 5, 2018

Choose a reason for hiding this comment

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

Should NameResolutionPal.SupportsGetAddrInfoAsync factor in SocketProtocolSupportPal.OSSupportsIPv6 if that's part of what's required to use GetAddrInfoAsync, rather than accessing both at the call site? Or we can't do that easily because of layering?

Choose a reason for hiding this comment

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

More generally, why is OSSupportsIPv6 necessary for SupportsGetAddrInfoAsync?

Copy link
Author

@JeffCyr JeffCyr Feb 5, 2018

Choose a reason for hiding this comment

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

GetAddrInfoEx doesn't require the OS to support IPv6. But the previous implementation was using GetHostByName instead of GetAddrInfo is OSSupportsIPv6 is false.

I'm not sure why that choice was made originally, but I thought it safe to not change this behavior.

Copy link
Author

Choose a reason for hiding this comment

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

Just saw the explanation in InternalGetHostByName

// Note        : Whilst getaddrinfo is available on WinXP+, we only
//               use it if IPv6 is enabled (platform is part of that
//               decision). This is done to minimize the number of
//               possible tests that are needed.

Choose a reason for hiding this comment

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

Yeah, that comment is ancient. There's no reason for us to not always use getaddrinfo today, and it would simplify the code and the tests.

My concern is mainly that we not make things any worse than they currently are. So if you want to do the IPv6 checks, that's fine, but please add comments wherever you are doing so that refer back to the comment above, so it's clear why these checks are happening.

Choose a reason for hiding this comment

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

I created issue #26856 to track this.

TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default);
if (NameResolutionPal.SupportsGetAddrInfoAsync && includeIPv6 && SocketProtocolSupportPal.OSSupportsIPv6 && address == null)
{
Copy link
Member

Choose a reason for hiding this comment

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

Nit: can you add a comment about the conditions that let's us take this path?


internal readonly string hostName;
internal bool includeIPv6;
internal IPAddress address;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: these should be prefixed with _

Copy link
Member

Choose a reason for hiding this comment

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

Can these be made readonly?

Copy link
Author

Choose a reason for hiding this comment

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

This was existing code moved from DNS.cs, but I will make the changes

base(myObject, myState, myCallBack)
{
this.includeIPv6 = includeIPv6;
this.address = address;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: the "this"es shouldn't be necessary

@@ -0,0 +1,25 @@
namespace System.Net
{
internal class DnsResolveAsyncResult : ContextAwareResult
Copy link
Member

Choose a reason for hiding this comment

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

Nit: sealed

@@ -13,6 +13,8 @@ namespace System.Net
{
internal static partial class NameResolutionPal
{
public static bool SupportsGetAddrInfoAsync => false;
Copy link
Member

@stephentoub stephentoub Feb 5, 2018

Choose a reason for hiding this comment

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

Can you make this a const? That would help the compiler to eliminate branches at the call site when doing the unix build. (It can be const in the Unix build and a static property in the Windows one.)

private static bool TestGetAddrInfoEx()
{
using (SafeLibraryHandle libHandle = Interop.Kernel32.LoadLibraryExW(Interop.Libraries.Ws2_32, IntPtr.Zero, 0))
{
Copy link
Member

Choose a reason for hiding this comment

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

Can/should we test libHandle.IsInvalid before calling GetProcAddress? e.g.

return !libHandle.IsInvalid && Interop.Kernel32.GetProcAddress(...

try
{ }
finally
{
Copy link
Member

@stephentoub stephentoub Feb 5, 2018

Choose a reason for hiding this comment

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

This try/finally pattern is not needed. It's only relevant for thread aborts, which don't exist in .NET Core. This pattern can be removed throughout.

{
using (SafeLibraryHandle libHandle = Interop.Kernel32.LoadLibraryExW(Interop.Libraries.Ws2_32, IntPtr.Zero, 0))
{
return Interop.Kernel32.GetProcAddress(libHandle, nameof(Interop.Winsock.GetAddrInfoExCancel)) != IntPtr.Zero;
Copy link
Member

Choose a reason for hiding this comment

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

We're testing for GetAddrInfoExCancel because that serves as an indication for whether GetAddrInfoExW supports overlapped execution? Can you add a comment?

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 something we can assert to help validate that this is working? e.g. checking the Windows version and asserting that TestGetAddrInfoEx returns true on an appropriately recent version?

{ }
finally
{
// Can be casted directly to QueryContext* because the overlapped is its first field
Copy link
Member

Choose a reason for hiding this comment

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

Is this robust?

Copy link
Author

Choose a reason for hiding this comment

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

Why wouldn't it be? GetAddrInfoExContext is a struct with sequential layout and the Overlapped is its first field. It's a weird API but in c++ that's how it is used (see the asynchronous sample using CONTAINING_RECORD here https://msdn.microsoft.com/en-us/library/windows/desktop/ms738518(v=vs.85).aspx )

GetAddrInfoExState state = context->GetQueryState();

if (state == null || !state.SetCallbackStartedOrCanceled())
return;
Copy link
Member

Choose a reason for hiding this comment

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

Who cleans up the allocated context?

Copy link
Author

Choose a reason for hiding this comment

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

The context is freed at the end of ProcessResult or in GetAddrInfoExState's finalizer when the AppDomain is unloaded.

The callback should not be called twice, but the callback could be raised at the same time the AppDomain is unloaded, so this check ensure only one path will free the context.

I will add a comment.

{
ipAddress = CreateIPv4Address(result->ai_addr, result->ai_addrlen);
}
else if (SocketProtocolSupportPal.OSSupportsIPv6 && result->ai_family == AddressFamily.InterNetworkV6)
Copy link
Member

Choose a reason for hiding this comment

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

Will we ever get to this point if OSSupportsIPv6 is false?

Copy link
Author

Choose a reason for hiding this comment

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

Discussed above, I will leave the check here in case there is a code change in DNS.cs to call this method even if OS doesn't support IPv6

(socketAddress[7] << 24)
) & 0x00000000FFFFFFFF;

return new IPAddress(address);
Copy link
Member

@stephentoub stephentoub Feb 5, 2018

Choose a reason for hiding this comment

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

Could this be:

return new IPAddress(new Span<byte>(socketAddress, 4, IPAddressParserStatics.IPv4AddressBytes));

?

for (int i = 0; i < address.Length; i++)
{
address[i] = socketAddress[i + 8];
}
Copy link
Member

Choose a reason for hiding this comment

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

Let's avoid allocating an array here. This should be doable with span now, e.g.

return new IPAddress(new Span<byte>(socketAddress, 8, IPv6AddressBytes), scope);

private static unsafe IPAddress CreateIPv6Address(byte* socketAddress, int addressLength)
{
const int IPv6SocketAddressSize = 28;
const int IPv6AddressBytes = 16;
Copy link
Member

Choose a reason for hiding this comment

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

This is already defined on the internal IPAddressParserStatics class and should be usable here.

{
private IntPtr m_context;
private int m_callbackStartedOrCanceled;
private DnsResolveAsyncResult m_asyncResult;
Copy link
Member

Choose a reason for hiding this comment

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

Nits:
readonly on these?
m_ => _


private sealed unsafe class GetAddrInfoExState : CriticalFinalizerObject, IDisposable
{
private IntPtr m_context;
Copy link
Member

Choose a reason for hiding this comment

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

Can this be done as a SafeHandle-derived type rather than CriticalFinalizerObject?

Copy link
Author

Choose a reason for hiding this comment

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

There is no reference counting and this handle is not used in a pinvoke, so I didn't see the gain in using SafeHandle. I used CriticalFinalizerObject for the CER, but since they don't exist in .Net Core I can make it derive from Object instead.

m_asyncResult = asyncResult;
}

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
Copy link
Member

Choose a reason for hiding this comment

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

As noted earlier, these ReliabilityContract attributes can be removed.


public void CompleteAsyncResult(object o)
{
Task.Run(() => m_asyncResult.InvokeCallback(o));
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 need to queue the callback?

Assuming yes, this will allocate a delegate and closure. It can instead be written as:

Task.Factory.StartNew(s =>
{
    var t = (Tuple<DnsResolveAsyncResult, object>)s;
    t.Item1(t.Item2);
}, Tuple.Create(_asyncResult, o), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);

That will incur the overhead of allocating just the tuple rather than the overhead of allocating the closure for the state as well as the delegate to a method on that closure.

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
private void ReleaseResources(bool cancelQuery)
{
var ptr = Interlocked.Exchange(ref m_context, IntPtr.Zero);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: var => IntPtr

Overlapped = new NativeOverlapped();
Result = null;

var handle = GCHandle.Alloc(state, GCHandleType.Normal);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: var => GCHandle


public GetAddrInfoExState GetQueryState()
{
var stateHandle = Interlocked.Exchange(ref QueryStateHandle, IntPtr.Zero);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: var => IntPtr

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

This looks good, @JeffCyr. Thanks for doing this. Have you done any perf analysis, to see how throughput compares? The primary purpose here is scalability and playing nicely with others, so a small perf hit is ok, it'd just be nice to know if there is one.

@JeffCyr
Copy link
Author

JeffCyr commented Feb 8, 2018

Performance analysis

There is an overhead with GetAddrInfoEx, it is noticeable when requesting a hostname which is already in the local machine cache. However it is unlikely to be a performance bottleneck because an application will unlikely make thousands of dns queries per second.

The non-cached test shows the performance gain of this PR, the old implementation is throttled by the .Net ThreadPool thread creation rate (by starving it). The new implementation is unthrottled, the caveat is that it is now possible to reach the Response Rate Limiting of the DNS server, this may cause timeout or errors.

Sequential cached test

One thread sequentially makes 20 000 requests for the same (pre-cached) hostname.

Test request/s
Dns.GetHostEntry 5125 request/s
Dns.GetHostEntryAsync (old) 4115 request/s
Dns.GetHostEntryAsync (new) 3400 request/s

Parallel cached test

One thread starts 20 000 requests in parallel and waits for completion for the same (pre-cached) hostname.

Test request/s
Dns.GetHostEntryAsync (old) 20616 request/s
Dns.GetHostEntryAsync (new) 12996 request/s

Parallel non-cached test

One thread starts X requests in parallel and waits for completion, a random non-existant domain is used for each request.

X = 10 Time
Dns.GetHostEntryAsync (old) 00:00:01.6670757
Dns.GetHostEntryAsync (new) 00:00:00.8452532
X = 50 Time
Dns.GetHostEntryAsync (old) 00:00:04.9970314
Dns.GetHostEntryAsync (new) 00:00:00.8513915
X = 100 Time
Dns.GetHostEntryAsync (old) 00:00:09.9531006
Dns.GetHostEntryAsync (new) 00:00:00.9996548
X = 200 Time
Dns.GetHostEntryAsync (old) 00:00:16.6067046
Dns.GetHostEntryAsync (new) 00:00:01.1124727

@JeffCyr
Copy link
Author

JeffCyr commented Feb 9, 2018

@dotnet-bot test Outerloop Windows x64 Debug Build please


private static bool GetAddrInfoExSupportsOverlapped()
{
using (SafeLibraryHandle libHandle = Interop.Kernel32.LoadLibraryExW(Interop.Libraries.Ws2_32, IntPtr.Zero, 0))
Copy link
Member

Choose a reason for hiding this comment

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

Pass in LOAD_LIBRARY_SEARCH_SYSTEM32 as flags.

@stephentoub
Copy link
Member

Thanks for the perf data! I think this makes sense to go ahead with (once remaining feedback is addressed). @geoffkizer, any overarching concerns?

@JeffCyr
Copy link
Author

JeffCyr commented Feb 10, 2018

@dotnet-bot test Outerloop Windows x64 Debug Build please

@JeffCyr
Copy link
Author

JeffCyr commented Feb 10, 2018

Ok I addressed the last comments. I cleaned up the code a bit after removing the cancellation code. Ready for the final (I hope!) review.

internal static extern unsafe void FreeAddrInfoEx([In] AddressInfoEx* pAddrInfo);

[DllImport("ws2_32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern int GetAddrInfoExCancel([In] ref IntPtr lpHandle);
Copy link
Member

@stephentoub stephentoub Feb 11, 2018

Choose a reason for hiding this comment

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

This can be removed now, right? Looks like the only remaining usage is just using it for its name.

public static void GetIPv6Address(byte[] buffer, Span<byte> address, out uint scope)
{
GetIPv6Address(new ReadOnlySpan<byte>(buffer), address, out scope);
}
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 need this overload? I'd expect the call site passing in the byte[] to just work with the span-based overload due to the implicit array->span cast.

@@ -39,6 +39,11 @@ public static unsafe void SetPort(byte[] buffer, ushort port)
}

public static unsafe uint GetIPv4Address(byte[] buffer)
{
return GetIPv4Address(new ReadOnlySpan<byte>(buffer));
}
Copy link
Member

Choose a reason for hiding this comment

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

Same "do we need this overload" question

public static void GetIPv6Address(byte[] buffer, Span<byte> address, out uint scope)
{
GetIPv6Address(new ReadOnlySpan<byte>(buffer), address, out scope);
}
Copy link
Member

Choose a reason for hiding this comment

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

Ditto

public static GetAddrInfoExContext* AllocateContext()
{
var context = (GetAddrInfoExContext*)Marshal.AllocHGlobal(Size);
*context = new GetAddrInfoExContext();
Copy link
Member

Choose a reason for hiding this comment

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

Nit: might be slightly more clear if this were just *context = default;, but not a big deal.

return GCHandle.ToIntPtr(handle);
}

public static GetAddrInfoExState FromHandle(IntPtr handle)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: FromHandleAndFree? Might help make the cleanup more clear for someone reading the call site.

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

Looks great. Thanks, @JeffCyr! I left a few more small comments to try to help tidy thing up, but otherwise I think this can be merged.

@stephentoub stephentoub merged commit d3ff31e into dotnet:master Feb 12, 2018
@JeffCyr JeffCyr deleted the getaddrinfoex branch February 12, 2018 00:20
pjanotti pushed a commit to pjanotti/corefx that referenced this pull request Feb 12, 2018
* Modified Dns.GetHostAddressesAsync to be truly async

* Applied code review recommendations

* Unix build fix

* Unix build fix dotnet#2

* Unix build fix dotnet#3

* NETFX build fix

* Fixed useGetHostByName logic

* Simplified ProcessResult code

* Cleaned up cancel code

* cleanup
A-And pushed a commit to A-And/corefx that referenced this pull request Feb 21, 2018
* Modified Dns.GetHostAddressesAsync to be truly async

* Applied code review recommendations

* Unix build fix

* Unix build fix dotnet#2

* Unix build fix dotnet#3

* NETFX build fix

* Fixed useGetHostByName logic

* Simplified ProcessResult code

* Cleaned up cancel code

* cleanup
@karelz karelz added this to the 2.1.0 milestone Mar 10, 2018
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…6850)

* Modified Dns.GetHostAddressesAsync to be truly async

* Applied code review recommendations

* Unix build fix

* Unix build fix dotnet/corefx#2

* Unix build fix dotnet/corefx#3

* NETFX build fix

* Fixed useGetHostByName logic

* Simplified ProcessResult code

* Cleaned up cancel code

* cleanup


Commit migrated from dotnet/corefx@d3ff31e
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.

7 participants