Modified Dns.GetHostAddressesAsync to be truly async#26850
Modified Dns.GetHostAddressesAsync to be truly async#26850stephentoub merged 10 commits intodotnet:masterfrom
Conversation
| [In] ref NativeOverlapped overlapped, | ||
| [In] GetAddrInfoExCompletionCallback callback, | ||
| [Out] out IntPtr cancelHandle | ||
| ); |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
Why the ThrowOnUnmappableChar = true? That's pretty rare to see.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Can this use in instead of ref?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Nit:
GetAddrInfoExCompletionCallback => LPLOOKUPSERVICE_COMPLETION_ROUTINE
error => dwError
bytes => dwBytes
etc.
| [Out] out IntPtr cancelHandle | ||
| ); | ||
|
|
||
| [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] |
There was a problem hiding this comment.
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); |
| CancellationToken.None, | ||
| TaskCreationOptions.DenyChildAttach, | ||
| TaskScheduler.Default); | ||
| if (NameResolutionPal.SupportsGetAddrInfoAsync && includeIPv6 && SocketProtocolSupportPal.OSSupportsIPv6 && address == null) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
More generally, why is OSSupportsIPv6 necessary for SupportsGetAddrInfoAsync?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I created issue #26856 to track this.
| TaskCreationOptions.DenyChildAttach, | ||
| TaskScheduler.Default); | ||
| if (NameResolutionPal.SupportsGetAddrInfoAsync && includeIPv6 && SocketProtocolSupportPal.OSSupportsIPv6 && address == null) | ||
| { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Nit: these should be prefixed with _
There was a problem hiding this comment.
This was existing code moved from DNS.cs, but I will make the changes
| base(myObject, myState, myCallBack) | ||
| { | ||
| this.includeIPv6 = includeIPv6; | ||
| this.address = address; |
There was a problem hiding this comment.
Nit: the "this"es shouldn't be necessary
| @@ -0,0 +1,25 @@ | |||
| namespace System.Net | |||
| { | |||
| internal class DnsResolveAsyncResult : ContextAwareResult | |||
| { | ||
| internal static partial class NameResolutionPal | ||
| { | ||
| public static bool SupportsGetAddrInfoAsync => false; |
There was a problem hiding this comment.
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)) | ||
| { |
There was a problem hiding this comment.
Can/should we test libHandle.IsInvalid before calling GetProcAddress? e.g.
return !libHandle.IsInvalid && Interop.Kernel32.GetProcAddress(...| try | ||
| { } | ||
| finally | ||
| { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
We're testing for GetAddrInfoExCancel because that serves as an indication for whether GetAddrInfoExW supports overlapped execution? Can you add a comment?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Who cleans up the allocated context?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Will we ever get to this point if OSSupportsIPv6 is false?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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]; | ||
| } |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Nits:
readonly on these?
m_ => _
|
|
||
| private sealed unsafe class GetAddrInfoExState : CriticalFinalizerObject, IDisposable | ||
| { | ||
| private IntPtr m_context; |
There was a problem hiding this comment.
Can this be done as a SafeHandle-derived type rather than CriticalFinalizerObject?
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
As noted earlier, these ReliabilityContract attributes can be removed.
|
|
||
| public void CompleteAsyncResult(object o) | ||
| { | ||
| Task.Run(() => m_asyncResult.InvokeCallback(o)); |
There was a problem hiding this comment.
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); |
| Overlapped = new NativeOverlapped(); | ||
| Result = null; | ||
|
|
||
| var handle = GCHandle.Alloc(state, GCHandleType.Normal); |
|
|
||
| public GetAddrInfoExState GetQueryState() | ||
| { | ||
| var stateHandle = Interlocked.Exchange(ref QueryStateHandle, IntPtr.Zero); |
stephentoub
left a comment
There was a problem hiding this comment.
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.
Performance analysisThere 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 testOne thread sequentially makes 20 000 requests for the same (pre-cached) hostname.
Parallel cached testOne thread starts 20 000 requests in parallel and waits for completion for the same (pre-cached) hostname.
Parallel non-cached testOne thread starts X requests in parallel and waits for completion, a random non-existant domain is used for each request.
|
9d7914f to
a9a3403
Compare
|
@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)) |
There was a problem hiding this comment.
Pass in LOAD_LIBRARY_SEARCH_SYSTEM32 as flags.
|
Thanks for the perf data! I think this makes sense to go ahead with (once remaining feedback is addressed). @geoffkizer, any overarching concerns? |
|
@dotnet-bot test Outerloop Windows x64 Debug Build please |
|
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); |
There was a problem hiding this comment.
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); | ||
| } |
There was a problem hiding this comment.
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.
| public static unsafe uint GetIPv4Address(byte[] buffer) | ||
| { | ||
| return GetIPv4Address(new ReadOnlySpan<byte>(buffer)); | ||
| } |
There was a problem hiding this comment.
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); | ||
| } |
| public static GetAddrInfoExContext* AllocateContext() | ||
| { | ||
| var context = (GetAddrInfoExContext*)Marshal.AllocHGlobal(Size); | ||
| *context = new GetAddrInfoExContext(); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Nit: FromHandleAndFree? Might help make the cleanup more clear for someone reading the call site.
stephentoub
left a comment
There was a problem hiding this comment.
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.
…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
PR for issue dotnet/runtime#17224 (originally: https://github.com/dotnet/corefx/issues/8371)
Modified Dns.GetHostAddressesAsync to use GetAddrInfoEx when supported.