Skip to content

Commit cc7b97f

Browse files
authored
Client factory handler supports multiple HTTP/2 connections (#1037)
1 parent b10480c commit cc7b97f

File tree

9 files changed

+174
-73
lines changed

9 files changed

+174
-73
lines changed

src/Grpc.Net.Client/Grpc.Net.Client.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
<ItemGroup>
1818
<Compile Include="..\Shared\CommonGrpcProtocolHelpers.cs" Link="Internal\CommonGrpcProtocolHelpers.cs" />
1919
<Compile Include="..\Shared\DefaultDeserializationContext.cs" Link="Internal\DefaultDeserializationContext.cs" />
20+
<Compile Include="..\Shared\HttpHandlerFactory.cs" Link="Internal\HttpHandlerFactory.cs" />
21+
<Compile Include="..\Shared\TelemetryHeaderHandler.cs" Link="Internal\TelemetryHeaderHandler.cs" />
2022
</ItemGroup>
2123

2224
<ItemGroup>

src/Grpc.Net.Client/GrpcChannel.cs

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
using Grpc.Core;
2525
using Grpc.Net.Client.Internal;
2626
using Grpc.Net.Compression;
27+
using Grpc.Shared;
2728
using Microsoft.Extensions.Logging;
2829
using Microsoft.Extensions.Logging.Abstractions;
2930

@@ -100,31 +101,11 @@ private static HttpMessageInvoker CreateInternalHttpInvoker(HttpMessageHandler?
100101
// Decision to dispose invoker is controlled by _shouldDisposeHttpClient.
101102
if (handler == null)
102103
{
103-
#if NET5_0
104-
if (SocketsHttpHandler.IsSupported)
105-
{
106-
handler = new SocketsHttpHandler
107-
{
108-
EnableMultipleHttp2Connections = true
109-
};
110-
}
111-
else
112-
{
113-
handler = new HttpClientHandler();
114-
}
115-
#else
116-
handler = new HttpClientHandler();
117-
#endif
104+
handler = HttpHandlerFactory.CreatePrimaryHandler();
118105
}
119106

120107
#if NET5_0
121-
// HttpClientHandler has an internal handler that sets request telemetry header.
122-
// If the handler is SocketsHttpHandler then we know that the header will never be set
123-
// so wrap with a telemetry header setting handler.
124-
if (IsSocketsHttpHandler(handler))
125-
{
126-
handler = new TelemetryHeaderHandler(handler);
127-
}
108+
handler = HttpHandlerFactory.EnsureTelemetryHandler(handler);
128109
#endif
129110

130111
// Use HttpMessageInvoker instead of HttpClient because it is faster
@@ -134,30 +115,6 @@ private static HttpMessageInvoker CreateInternalHttpInvoker(HttpMessageHandler?
134115
return httpInvoker;
135116
}
136117

137-
#if NET5_0
138-
private static bool IsSocketsHttpHandler(HttpMessageHandler handler)
139-
{
140-
if (handler is SocketsHttpHandler)
141-
{
142-
return true;
143-
}
144-
145-
HttpMessageHandler? currentHandler = handler;
146-
DelegatingHandler? delegatingHandler;
147-
while ((delegatingHandler = currentHandler as DelegatingHandler) != null)
148-
{
149-
currentHandler = delegatingHandler.InnerHandler;
150-
151-
if (currentHandler is SocketsHttpHandler)
152-
{
153-
return true;
154-
}
155-
}
156-
157-
return false;
158-
}
159-
#endif
160-
161118
internal void RegisterActiveCall(IDisposable grpcCall)
162119
{
163120
lock (ActiveCalls)

src/Grpc.Net.ClientFactory/Grpc.Net.ClientFactory.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
<TargetFrameworks>netstandard2.1;net5.0</TargetFrameworks>
1010
</PropertyGroup>
1111

12+
<ItemGroup>
13+
<Compile Include="..\Shared\HttpHandlerFactory.cs" Link="Internal\HttpHandlerFactory.cs" />
14+
<Compile Include="..\Shared\TelemetryHeaderHandler.cs" Link="Internal\TelemetryHeaderHandler.cs" />
15+
</ItemGroup>
16+
1217
<ItemGroup>
1318
<!-- PrivateAssets set to None to ensure the build targets/props are propagated to parent project -->
1419
<ProjectReference Include="..\Grpc.Net.Client\Grpc.Net.Client.csproj" PrivateAssets="None" />

src/Grpc.Net.ClientFactory/GrpcClientServiceExtensions.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
using Grpc.Core;
2525
using Grpc.Net.ClientFactory;
2626
using Grpc.Net.ClientFactory.Internal;
27+
using Grpc.Shared;
2728
using Microsoft.Extensions.DependencyInjection.Extensions;
2829
using Microsoft.Extensions.Options;
2930

@@ -334,7 +335,16 @@ private static IHttpClientBuilder AddGrpcHttpClient<TClient>(this IServiceCollec
334335
throw new ArgumentNullException(nameof(services));
335336
}
336337

337-
services.AddHttpClient(name, configureTypedClient);
338+
services
339+
.AddHttpClient(name, configureTypedClient)
340+
.ConfigurePrimaryHttpMessageHandler(() =>
341+
{
342+
var handler = HttpHandlerFactory.CreatePrimaryHandler();
343+
#if NET5_0
344+
handler = HttpHandlerFactory.EnsureTelemetryHandler(handler);
345+
#endif
346+
return handler;
347+
});
338348

339349
var builder = new DefaultHttpClientBuilder(services, name);
340350

src/Shared/HttpHandlerFactory.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
using System.Net.Http;
20+
21+
namespace Grpc.Shared
22+
{
23+
internal static class HttpHandlerFactory
24+
{
25+
public static HttpMessageHandler CreatePrimaryHandler()
26+
{
27+
#if NET5_0
28+
// If we're in .NET 5 and SocketsHttpHandler is supported (it's not in Blazor WebAssembly)
29+
// then create SocketsHttpHandler with EnableMultipleHttp2Connections set to true. That will
30+
// allow a gRPC channel to create new connections if the maximum allow concurrency is exceeded.
31+
if (SocketsHttpHandler.IsSupported)
32+
{
33+
return new SocketsHttpHandler
34+
{
35+
EnableMultipleHttp2Connections = true
36+
};
37+
}
38+
#endif
39+
40+
return new HttpClientHandler();
41+
}
42+
43+
#if NET5_0
44+
public static HttpMessageHandler EnsureTelemetryHandler(HttpMessageHandler handler)
45+
{
46+
// HttpClientHandler has an internal handler that sets request telemetry header.
47+
// If the handler is SocketsHttpHandler then we know that the header will never be set
48+
// so wrap with a handler that is responsible for setting the telemetry header.
49+
if (IsSocketsHttpHandler(handler))
50+
{
51+
return new TelemetryHeaderHandler(handler);
52+
}
53+
54+
return handler;
55+
}
56+
57+
private static bool IsSocketsHttpHandler(HttpMessageHandler handler)
58+
{
59+
if (handler is SocketsHttpHandler)
60+
{
61+
return true;
62+
}
63+
64+
HttpMessageHandler? currentHandler = handler;
65+
DelegatingHandler? delegatingHandler;
66+
while ((delegatingHandler = currentHandler as DelegatingHandler) != null)
67+
{
68+
currentHandler = delegatingHandler.InnerHandler;
69+
70+
if (currentHandler is SocketsHttpHandler)
71+
{
72+
return true;
73+
}
74+
}
75+
76+
return false;
77+
}
78+
#endif
79+
}
80+
}

src/Grpc.Net.Client/Internal/TelemetryHeaderHandler.cs renamed to src/Shared/TelemetryHeaderHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
using System.Threading;
2828
using System.Threading.Tasks;
2929

30-
namespace Grpc.Net.Client.Internal
30+
namespace Grpc.Shared
3131
{
3232
internal sealed class TelemetryHeaderHandler : DelegatingHandler
3333
{

test/FunctionalTests/Client/TelemetryTests.cs

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,42 @@
1616

1717
#endregion
1818

19+
using System;
20+
using System.Net.Http;
21+
using System.Threading.Tasks;
1922
using Greet;
2023
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
2124
using Grpc.Core;
2225
using Grpc.Net.Client;
23-
using Grpc.Tests.Shared;
26+
using Microsoft.Extensions.DependencyInjection;
27+
using Microsoft.Extensions.Logging;
2428
using NUnit.Framework;
25-
using System.Net.Http;
26-
using System.Threading.Tasks;
2729

2830
namespace Grpc.AspNetCore.FunctionalTests.Client
2931
{
3032
[TestFixture]
3133
public class TelemetryTests : FunctionalTestBase
3234
{
33-
[Test]
34-
public async Task InternalHandler_UnaryCall_TelemetryHeaderSentWithRequest()
35+
[TestCase(ClientType.Channel)]
36+
[TestCase(ClientType.ClientFactory)]
37+
public async Task InternalHandler_UnaryCall_TelemetryHeaderSentWithRequest(ClientType clientType)
3538
{
36-
await TestTelemetryHeaderIsSet(handler: null);
39+
await TestTelemetryHeaderIsSet(clientType, handler: null);
3740
}
3841

3942
#if NET5_0
40-
[Test]
41-
public async Task SocketsHttpHandler_UnaryCall_TelemetryHeaderSentWithRequest()
43+
[TestCase(ClientType.Channel)]
44+
[TestCase(ClientType.ClientFactory)]
45+
public async Task Channel_SocketsHttpHandler_UnaryCall_TelemetryHeaderSentWithRequest(ClientType clientType)
4246
{
43-
await TestTelemetryHeaderIsSet(handler: new SocketsHttpHandler());
47+
await TestTelemetryHeaderIsSet(clientType, handler: new SocketsHttpHandler());
4448
}
4549

46-
[Test]
47-
public async Task SocketsHttpHandlerWrapped_UnaryCall_TelemetryHeaderSentWithRequest()
50+
[TestCase(ClientType.Channel)]
51+
[TestCase(ClientType.ClientFactory)]
52+
public async Task Channel_SocketsHttpHandlerWrapped_UnaryCall_TelemetryHeaderSentWithRequest(ClientType clientType)
4853
{
49-
await TestTelemetryHeaderIsSet(handler: new TestDelegatingHandler(new SocketsHttpHandler()));
54+
await TestTelemetryHeaderIsSet(clientType, handler: new TestDelegatingHandler(new SocketsHttpHandler()));
5055
}
5156

5257
private class TestDelegatingHandler : DelegatingHandler
@@ -57,7 +62,7 @@ public TestDelegatingHandler(HttpMessageHandler innerHandler) : base(innerHandle
5762
}
5863
#endif
5964

60-
private async Task TestTelemetryHeaderIsSet(HttpMessageHandler? handler)
65+
private async Task TestTelemetryHeaderIsSet(ClientType clientType, HttpMessageHandler? handler)
6166
{
6267
string? telemetryHeader = null;
6368
Task<HelloReply> UnaryTelemetryHeader(HelloRequest request, ServerCallContext context)
@@ -75,23 +80,58 @@ Task<HelloReply> UnaryTelemetryHeader(HelloRequest request, ServerCallContext co
7580

7681
// Arrange
7782
var method = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(UnaryTelemetryHeader);
78-
79-
var options = new GrpcChannelOptions
80-
{
81-
LoggerFactory = LoggerFactory,
82-
HttpHandler = handler
83-
};
84-
85-
// Want to test the behavior of the default, internally created handler.
86-
// Only supply the URL to a manually created GrpcChannel.
87-
var channel = GrpcChannel.ForAddress(Fixture.GetUrl(TestServerEndpointName.Http2), options);
88-
var client = TestClientFactory.Create(channel, method);
83+
var client = CreateClient(clientType, method, handler);
8984

9085
// Act
9186
await client.UnaryCall(new HelloRequest());
9287

9388
// Assert
9489
Assert.IsNotNull(telemetryHeader);
9590
}
91+
92+
private TestClient<HelloRequest, HelloReply> CreateClient(ClientType clientType, Method<HelloRequest, HelloReply> method, HttpMessageHandler? handler)
93+
{
94+
switch (clientType)
95+
{
96+
case ClientType.Channel:
97+
{
98+
var options = new GrpcChannelOptions
99+
{
100+
LoggerFactory = LoggerFactory,
101+
HttpHandler = handler
102+
};
103+
104+
// Want to test the behavior of the default, internally created handler.
105+
// Only supply the URL to a manually created GrpcChannel.
106+
var channel = GrpcChannel.ForAddress(Fixture.GetUrl(TestServerEndpointName.Http2), options);
107+
return TestClientFactory.Create(channel, method);
108+
}
109+
case ClientType.ClientFactory:
110+
{
111+
var serviceCollection = new ServiceCollection();
112+
serviceCollection.AddSingleton<ILoggerFactory>(LoggerFactory);
113+
serviceCollection
114+
.AddGrpcClient<TestClient<HelloRequest, HelloReply>>(options =>
115+
{
116+
options.Address = Fixture.GetUrl(TestServerEndpointName.Http2);
117+
})
118+
.ConfigureGrpcClientCreator(invoker =>
119+
{
120+
return TestClientFactory.Create(invoker, method);
121+
});
122+
var services = serviceCollection.BuildServiceProvider();
123+
124+
return services.GetRequiredService<TestClient<HelloRequest, HelloReply>>();
125+
}
126+
default:
127+
throw new InvalidOperationException("Unexpected value.");
128+
}
129+
}
130+
131+
public enum ClientType
132+
{
133+
Channel,
134+
ClientFactory
135+
}
96136
}
97137
}

test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<FrameworkReference Include="Microsoft.AspNetCore.App" />
2121
<ProjectReference Include="..\..\src\Grpc.Net.Client\Grpc.Net.Client.csproj" />
2222
<ProjectReference Include="..\..\src\Grpc.Net.Client.Web\Grpc.Net.Client.Web.csproj" />
23+
<ProjectReference Include="..\..\src\Grpc.Net.ClientFactory\Grpc.Net.ClientFactory.csproj" />
2324

2425
<ProjectReference Include="..\..\testassets\FunctionalTestsWebsite\FunctionalTestsWebsite.csproj" />
2526

test/Grpc.Net.ClientFactory.Tests/GrpcHttpClientBuilderExtensionsTests.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public async Task AddInterceptor_MultipleInstances_ExecutedInOrder()
8484
{
8585
// Arrange
8686
var list = new List<int>();
87+
var testHttpMessageHandler = new TestHttpMessageHandler();
8788

8889
var services = new ServiceCollection();
8990
services
@@ -94,7 +95,7 @@ public async Task AddInterceptor_MultipleInstances_ExecutedInOrder()
9495
.AddInterceptor(() => new CallbackInterceptor(o => list.Add(1)))
9596
.AddInterceptor(() => new CallbackInterceptor(o => list.Add(2)))
9697
.AddInterceptor(() => new CallbackInterceptor(o => list.Add(3)))
97-
.ConfigurePrimaryHttpMessageHandler(() => new TestHttpMessageHandler());
98+
.ConfigurePrimaryHttpMessageHandler(() => testHttpMessageHandler);
9899

99100
var serviceProvider = services.BuildServiceProvider(validateScopes: true);
100101

@@ -105,6 +106,7 @@ public async Task AddInterceptor_MultipleInstances_ExecutedInOrder()
105106
var response = await client.SayHelloAsync(new HelloRequest()).ResponseAsync.DefaultTimeout();
106107

107108
// Assert
109+
Assert.IsTrue(testHttpMessageHandler.Invoked);
108110
Assert.IsNotNull(response);
109111
Assert.AreEqual(3, list.Count);
110112
Assert.AreEqual(1, list[0]);
@@ -338,8 +340,12 @@ public DerivedGreeterClient(CallInvoker callInvoker) : base(callInvoker)
338340

339341
private class TestHttpMessageHandler : HttpMessageHandler
340342
{
343+
public bool Invoked { get; private set; }
344+
341345
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
342346
{
347+
Invoked = true;
348+
343349
// Get stream from request content so gRPC client serializes request message
344350
_ = await request.Content!.ReadAsStreamAsync();
345351

0 commit comments

Comments
 (0)