-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Modified Dns.GetHostAddressesAsync to be truly async #26850
Conversation
[In] ref NativeOverlapped overlapped, | ||
[In] GetAddrInfoExCompletionCallback callback, | ||
[Out] out IntPtr cancelHandle | ||
); |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) | ||
{ |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 _
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)) | ||
{ |
There was a problem hiding this comment.
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 | ||
{ |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this robust?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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]; | ||
} |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: var => IntPtr
There was a problem hiding this 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.
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.
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.
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.
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); | ||
} |
There was a problem hiding this comment.
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)); | |||
} |
There was a problem hiding this comment.
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); | ||
} |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
…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.