diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.AnyOS.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.AnyOS.cs index e8f6e91218da93..90aa7a9bd8e530 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.AnyOS.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.AnyOS.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Diagnostics.Tracing; using System.Threading; @@ -21,24 +22,22 @@ internal sealed partial class HttpTelemetry private EventCounter? _http30RequestsQueueDurationCounter; [NonEvent] - public void Http11RequestLeftQueue(double timeOnQueueMilliseconds) + public void RequestLeftQueue(int versionMajor, TimeSpan duration) { - _http11RequestsQueueDurationCounter?.WriteMetric(timeOnQueueMilliseconds); - RequestLeftQueue(timeOnQueueMilliseconds, versionMajor: 1, versionMinor: 1); - } + Debug.Assert(versionMajor is 1 or 2 or 3); - [NonEvent] - public void Http20RequestLeftQueue(double timeOnQueueMilliseconds) - { - _http20RequestsQueueDurationCounter?.WriteMetric(timeOnQueueMilliseconds); - RequestLeftQueue(timeOnQueueMilliseconds, versionMajor: 2, versionMinor: 0); - } + EventCounter? counter = versionMajor switch + { + 1 => _http11RequestsQueueDurationCounter, + 2 => _http20RequestsQueueDurationCounter, + _ => _http30RequestsQueueDurationCounter + }; - [NonEvent] - public void Http30RequestLeftQueue(double timeOnQueueMilliseconds) - { - _http30RequestsQueueDurationCounter?.WriteMetric(timeOnQueueMilliseconds); - RequestLeftQueue(timeOnQueueMilliseconds, versionMajor: 3, versionMinor: 0); + double timeOnQueueMs = duration.TotalMilliseconds; + + counter?.WriteMetric(timeOnQueueMs); + + RequestLeftQueue(timeOnQueueMs, (byte)versionMajor, versionMinor: versionMajor == 1 ? (byte)1 : (byte)0); } protected override void OnEventCommand(EventCommandEventArgs command) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs index 33eadf13f725e4..77e91086e5af19 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs @@ -173,11 +173,6 @@ public async Task SendAsync(HttpRequestMessage request, lon QuicConnection? conn = _connection; if (conn != null) { - if (HttpTelemetry.Log.IsEnabled() && queueStartingTimestamp == 0) - { - queueStartingTimestamp = Stopwatch.GetTimestamp(); - } - quicStream = await conn.OpenOutboundStreamAsync(QuicStreamType.Bidirectional, cancellationToken).ConfigureAwait(false); requestStream = new Http3RequestStream(request, this, quicStream); @@ -198,9 +193,16 @@ public async Task SendAsync(HttpRequestMessage request, lon catch (QuicException e) when (e.QuicError != QuicError.OperationAborted) { } finally { - if (HttpTelemetry.Log.IsEnabled() && queueStartingTimestamp != 0) + if (queueStartingTimestamp != 0) { - HttpTelemetry.Log.Http30RequestLeftQueue(Stopwatch.GetElapsedTime(queueStartingTimestamp).TotalMilliseconds); + TimeSpan duration = Stopwatch.GetElapsedTime(queueStartingTimestamp); + + _pool.Settings._metrics!.RequestLeftQueue(Pool, duration, versionMajor: 3); + + if (HttpTelemetry.Log.IsEnabled()) + { + HttpTelemetry.Log.RequestLeftQueue(versionMajor: 3, duration); + } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs index bf2b4a0a90a2cf..337e416d2dfb9b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs @@ -42,20 +42,18 @@ public HttpConnectionBase(HttpConnectionPool pool) metrics.IdleConnections.Enabled || metrics.ConnectionDuration.Enabled) { + // While requests may report HTTP/1.0 as the protocol, we treat all HTTP/1.X connections as HTTP/1.1. string protocol = this is HttpConnection ? "HTTP/1.1" : this is Http2Connection ? "HTTP/2" : "HTTP/3"; - int port = pool.OriginAuthority.Port; - int defaultPort = pool.IsSecure ? HttpConnectionPool.DefaultHttpsPort : HttpConnectionPool.DefaultHttpPort; - _connectionMetrics = new ConnectionMetrics( metrics, protocol, pool.IsSecure ? "https" : "http", pool.OriginAuthority.HostValue, - port == defaultPort ? null : port); + pool.IsDefaultPort ? null : pool.OriginAuthority.Port); _connectionMetrics.ConnectionEstablished(); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index f86147c5967401..76f4c4acbb2975 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -236,10 +236,9 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK { // Precalculate ASCII bytes for Host header // Note that if _host is null, this is a (non-tunneled) proxy connection, and we can't cache the hostname. - hostHeader = - (_originAuthority.Port != (sslHostName == null ? DefaultHttpPort : DefaultHttpsPort)) ? - $"{_originAuthority.HostValue}:{_originAuthority.Port}" : - _originAuthority.HostValue; + hostHeader = IsDefaultPort + ? _originAuthority.HostValue + : $"{_originAuthority.HostValue}:{_originAuthority.Port}"; // Note the IDN hostname should always be ASCII, since it's already been IDNA encoded. byte[] hostHeaderLine = new byte[6 + hostHeader.Length + 2]; // Host: foo\r\n @@ -363,6 +362,7 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials; public byte[]? HostHeaderLineBytes => _hostHeaderLineBytes; public CredentialCache? PreAuthCredentials { get; } + public bool IsDefaultPort => OriginAuthority.Port == (IsSecure ? DefaultHttpsPort : DefaultHttpPort); /// /// An ASCII origin string per RFC 6454 Section 6.2, in format <scheme>://<host>[:<port>] @@ -381,7 +381,7 @@ public byte[] Http2AltSvcOriginUri sb.Append(IsSecure ? "https://" : "http://") .Append(_originAuthority.IdnHost); - if (_originAuthority.Port != (IsSecure ? DefaultHttpsPort : DefaultHttpPort)) + if (!IsDefaultPort) { sb.Append(CultureInfo.InvariantCulture, $":{_originAuthority.Port}"); } @@ -998,16 +998,10 @@ private async ValueTask GetHttp3ConnectionAsync(HttpRequestMess ThrowGetVersionException(request, 3, reasonException); } - long queueStartingTimestamp = HttpTelemetry.Log.IsEnabled() ? Stopwatch.GetTimestamp() : 0; + long queueStartingTimestamp = HttpTelemetry.Log.IsEnabled() || Settings._metrics!.RequestsQueueDuration.Enabled ? Stopwatch.GetTimestamp() : 0; ValueTask connectionTask = GetHttp3ConnectionAsync(request, authority, cancellationToken); - if (HttpTelemetry.Log.IsEnabled() && connectionTask.IsCompleted) - { - // We avoid logging RequestLeftQueue if a stream was available immediately (synchronously) - queueStartingTimestamp = 0; - } - Http3Connection connection = await connectionTask.ConfigureAwait(false); HttpResponseMessage response = await connection.SendAsync(request, queueStartingTimestamp, cancellationToken).ConfigureAwait(false); @@ -1089,7 +1083,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn if (!TryGetPooledHttp2Connection(request, out Http2Connection? connection, out http2ConnectionWaiter) && http2ConnectionWaiter != null) { - connection = await http2ConnectionWaiter.WaitForConnectionAsync(async, cancellationToken).ConfigureAwait(false); + connection = await http2ConnectionWaiter.WaitForConnectionAsync(this, async, cancellationToken).ConfigureAwait(false); } Debug.Assert(connection is not null || !_http2Enabled); @@ -1121,7 +1115,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn // Use HTTP/1.x. if (!TryGetPooledHttp11Connection(request, async, out HttpConnection? connection, out http11ConnectionWaiter)) { - connection = await http11ConnectionWaiter.WaitForConnectionAsync(async, cancellationToken).ConfigureAwait(false); + connection = await http11ConnectionWaiter.WaitForConnectionAsync(this, async, cancellationToken).ConfigureAwait(false); } connection.Acquire(); // In case we are doing Windows (i.e. connection-based) auth, we need to ensure that we hold on to this specific connection while auth is underway. @@ -1199,6 +1193,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn } private void CancelIfNecessary(HttpConnectionWaiter? waiter, bool requestCancelled) + where T : HttpConnectionBase? { int timeout = GlobalHttpSettings.SocketsHttpHandler.PendingConnectionTimeoutOnRequestCompletion; if (waiter?.ConnectionCancellationTokenSource is null || @@ -2430,6 +2425,7 @@ private void Trace(string? message, [CallerMemberName] string? memberName = null message); // message private struct RequestQueue + where T : HttpConnectionBase? { public struct QueueItem { @@ -2599,6 +2595,7 @@ public QueueItem PeekNextRequestForConnectionAttempt() } private sealed class HttpConnectionWaiter : TaskCompletionSourceWithCancellation + where T : HttpConnectionBase? { // When a connection attempt is pending, reference the connection's CTS, so we can tear it down if the initiating request is cancelled // or completes on a different connection. @@ -2607,8 +2604,17 @@ private sealed class HttpConnectionWaiter : TaskCompletionSourceWithCancellat // Distinguish connection cancellation that happens because the initiating request is cancelled or completed on a different connection. public bool CancelledByOriginatingRequestCompletion { get; set; } - public async ValueTask WaitForConnectionAsync(bool async, CancellationToken requestCancellationToken) + public ValueTask WaitForConnectionAsync(HttpConnectionPool pool, bool async, CancellationToken requestCancellationToken) { + return HttpTelemetry.Log.IsEnabled() || pool.Settings._metrics!.RequestsQueueDuration.Enabled + ? WaitForConnectionWithTelemetryAsync(pool, async, requestCancellationToken) + : WaitWithCancellationAsync(async, requestCancellationToken); + } + + private async ValueTask WaitForConnectionWithTelemetryAsync(HttpConnectionPool pool, bool async, CancellationToken requestCancellationToken) + { + Debug.Assert(typeof(T) == typeof(HttpConnection) || typeof(T) == typeof(Http2Connection)); + long startingTimestamp = Stopwatch.GetTimestamp(); try { @@ -2616,12 +2622,14 @@ public async ValueTask WaitForConnectionAsync(bool async, CancellationToken r } finally { + TimeSpan duration = Stopwatch.GetElapsedTime(startingTimestamp); + int versionMajor = typeof(T) == typeof(HttpConnection) ? 1 : 2; + + pool.Settings._metrics!.RequestLeftQueue(pool, duration, versionMajor); + if (HttpTelemetry.Log.IsEnabled()) { - if (typeof(T) == typeof(HttpConnection)) - HttpTelemetry.Log.Http11RequestLeftQueue(Stopwatch.GetElapsedTime(startingTimestamp).TotalMilliseconds); - else if (typeof(T) == typeof(Http2Connection)) - HttpTelemetry.Log.Http20RequestLeftQueue(Stopwatch.GetElapsedTime(startingTimestamp).TotalMilliseconds); + HttpTelemetry.Log.RequestLeftQueue(versionMajor, duration); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs index 5db3b7692c2b0a..91a4adccc7df9c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Diagnostics.Metrics; namespace System.Net.Http.Metrics @@ -19,5 +20,38 @@ internal sealed class SocketsHttpHandlerMetrics(Meter meter) name: "http-client-connection-duration", unit: "s", description: "The duration of outbound HTTP connections."); + + public readonly Histogram RequestsQueueDuration = meter.CreateHistogram( + name: "http-client-requests-queue-duration", + unit: "s", + description: "The amount of time requests spent on a queue waiting for an available connection."); + + public void RequestLeftQueue(HttpConnectionPool pool, TimeSpan duration, int versionMajor) + { + Debug.Assert(versionMajor is 1 or 2 or 3); + + if (RequestsQueueDuration.Enabled) + { + TagList tags = default; + + // While requests may report HTTP/1.0 as the protocol, we treat all HTTP/1.X connections as HTTP/1.1. + tags.Add("protocol", versionMajor switch + { + 1 => "HTTP/1.1", + 2 => "HTTP/2", + _ => "HTTP/3" + }); + + tags.Add("scheme", pool.IsSecure ? "https" : "http"); + tags.Add("host", pool.OriginAuthority.HostValue); + + if (!pool.IsDefaultPort) + { + tags.Add("port", pool.OriginAuthority.Port); + } + + RequestsQueueDuration.Record(duration.TotalSeconds, tags); + } + } } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs index c27ce9c3ff4818..29694a9fa361f4 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs @@ -28,6 +28,7 @@ protected static class InstrumentNames public const string CurrentConnections = "http-client-current-connections"; public const string IdleConnections = "http-client-current-idle-connections"; public const string ConnectionDuration = "http-client-connection-duration"; + public const string RequestsQueueDuration = "http-client-requests-queue-duration"; } protected HttpMetricsTestBase(ITestOutputHelper output) : base(output) @@ -97,10 +98,11 @@ protected static void VerifyConnectionCounter(string expectedName, string actual VerifyOptionalTag(tags, "protocol", protocol); } - protected static void VerifyConnectionDuration(string instrumentName, double measurement, KeyValuePair[] tags, Uri uri, string protocol) + protected static void VerifyConnectionDuration(string expectedName, string instrumentName, object measurement, KeyValuePair[] tags, Uri uri, string protocol) { - Assert.InRange(measurement, double.Epsilon, 60); - Assert.Equal(InstrumentNames.ConnectionDuration, instrumentName); + Assert.Equal(expectedName, instrumentName); + double value = Assert.IsType(measurement); + Assert.InRange(value, double.Epsilon, 60); VerifySchemeHostPortTags(tags, uri); VerifyOptionalTag(tags, "protocol", protocol); } @@ -152,10 +154,12 @@ public InstrumentRecorder(IMeterFactory meterFactory, string instrumentName) public void Dispose() => _meterListener.Dispose(); } + protected record RecordedCounter(string InstrumentName, object Value, KeyValuePair[] Tags); + protected sealed class MultiInstrumentRecorder : IDisposable { private readonly MeterListener _meterListener = new(); - private readonly ConcurrentQueue<(string InstrumentName, object Value, KeyValuePair[] Tags)> _values = new(); + private readonly ConcurrentQueue _values = new(); public MultiInstrumentRecorder() : this(meter: null) @@ -176,15 +180,15 @@ private MultiInstrumentRecorder(Meter? meter) }; _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, _) => - _values.Enqueue((instrument.Name, measurement, tags.ToArray()))); + _values.Enqueue(new RecordedCounter(instrument.Name, measurement, tags.ToArray()))); _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, _) => - _values.Enqueue((instrument.Name, measurement, tags.ToArray()))); + _values.Enqueue(new RecordedCounter(instrument.Name, measurement, tags.ToArray()))); _meterListener.Start(); } - public IReadOnlyList<(string InstrumentName, object Value, KeyValuePair[] Tags)> GetMeasurements() => _values.ToArray(); + public IReadOnlyList GetMeasurements() => _values.ToArray(); public void Dispose() => _meterListener.Dispose(); } } @@ -452,6 +456,7 @@ public Task CurrentRequests_Redirect_RecordedForEachHttpSpan() [ConditionalFact(typeof(SocketsHttpHandler), nameof(SocketsHttpHandler.IsSupported))] public async Task AllSocketsHttpHandlerCounters_Success_Recorded() { + TaskCompletionSource clientWaitingTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); TaskCompletionSource clientDisposedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); await LoopbackServerFactory.CreateClientAndServerAsync(async uri => @@ -463,26 +468,48 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => Handler.MeterFactory = _meterFactory; using HttpRequestMessage request = new(HttpMethod.Get, uri) { Version = UseVersion }; - using HttpResponseMessage response = await SendAsync(invoker, request); + Task sendAsyncTask = SendAsync(invoker, request); + clientWaitingTcs.SetResult(); + using HttpResponseMessage response = await sendAsyncTask; + await WaitForEnvironmentTicksToAdvance(); } clientDisposedTcs.SetResult(); + Action requestsQueueDuration = m => + VerifyConnectionDuration(InstrumentNames.RequestsQueueDuration, m.InstrumentName, m.Value, m.Tags, uri, ExpectedProtocolString); + + Action connectionNoLongerIdle = m => + VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, ExpectedProtocolString); + + Action check1 = requestsQueueDuration; + Action check2 = connectionNoLongerIdle; + + if (UseVersion.Major > 1) + { + // With HTTP/2 and HTTP/3, the IdleConnections counter is emitted before RequestsQueueDuration. + check1 = connectionNoLongerIdle; + check2 = requestsQueueDuration; + } + Assert.Collection(recorder.GetMeasurements(), m => VerifyCurrentRequest(m.InstrumentName, (long)m.Value, m.Tags, 1, uri), m => VerifyConnectionCounter(InstrumentNames.CurrentConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, ExpectedProtocolString), m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, ExpectedProtocolString), - m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, ExpectedProtocolString), + check1, // requestsQueueDuration and connectionNoLongerIdle in the appropriate order. + check2, m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, ExpectedProtocolString), m => VerifyCurrentRequest(m.InstrumentName, (long)m.Value, m.Tags, -1, uri), m => VerifyRequestDuration(m.InstrumentName, (double)m.Value, m.Tags, uri, ExpectedProtocolString, 200), m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, ExpectedProtocolString), m => VerifyConnectionCounter(InstrumentNames.CurrentConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, ExpectedProtocolString), - m => VerifyConnectionDuration(m.InstrumentName, (double)m.Value, m.Tags, uri, ExpectedProtocolString)); + m => VerifyConnectionDuration(InstrumentNames.ConnectionDuration, m.InstrumentName, m.Value, m.Tags, uri, ExpectedProtocolString)); }, async server => { + await clientWaitingTcs.Task.WaitAsync(TestHelper.PassingTestTimeout); + await server.AcceptConnectionAsync(async connection => { await connection.ReadRequestDataAsync(); @@ -845,6 +872,8 @@ public void AllSocketsHttpHandlerCounters_Success_Recorded() { RemoteExecutor.Invoke(static async Task () => { + TaskCompletionSource clientWaitingTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + using HttpMetricsTest_DefaultMeter test = new(null); await test.LoopbackServerFactory.CreateClientAndServerAsync(async uri => { @@ -853,7 +882,10 @@ await test.LoopbackServerFactory.CreateClientAndServerAsync(async uri => using (HttpClient client = test.CreateHttpClient()) { using HttpRequestMessage request = new(HttpMethod.Get, uri) { Version = test.UseVersion }; - using HttpResponseMessage response = await client.SendAsync(request); + Task sendAsyncTask = client.SendAsync(request); + clientWaitingTcs.SetResult(); + using HttpResponseMessage response = await sendAsyncTask; + await WaitForEnvironmentTicksToAdvance(); } @@ -861,16 +893,19 @@ await test.LoopbackServerFactory.CreateClientAndServerAsync(async uri => m => VerifyCurrentRequest(m.InstrumentName, (long)m.Value, m.Tags, 1, uri), m => VerifyConnectionCounter(InstrumentNames.CurrentConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, "HTTP/1.1"), m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, "HTTP/1.1"), + m => VerifyConnectionDuration(InstrumentNames.RequestsQueueDuration, m.InstrumentName, m.Value, m.Tags, uri, "HTTP/1.1"), m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, "HTTP/1.1"), m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, 1, uri, "HTTP/1.1"), m => VerifyCurrentRequest(m.InstrumentName, (long)m.Value, m.Tags, -1, uri), m => VerifyRequestDuration(m.InstrumentName, (double)m.Value, m.Tags, uri, "HTTP/1.1", 200), m => VerifyConnectionCounter(InstrumentNames.IdleConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, "HTTP/1.1"), m => VerifyConnectionCounter(InstrumentNames.CurrentConnections, m.InstrumentName, m.Value, m.Tags, -1, uri, "HTTP/1.1"), - m => VerifyConnectionDuration(m.InstrumentName, (double)m.Value, m.Tags, uri, "HTTP/1.1")); + m => VerifyConnectionDuration(InstrumentNames.ConnectionDuration, m.InstrumentName, m.Value, m.Tags, uri, "HTTP/1.1")); }, async server => { + await clientWaitingTcs.Task.WaitAsync(TestHelper.PassingTestTimeout); + await server.AcceptConnectionAsync(async connection => { await connection.ReadRequestDataAsync();