1919using System ;
2020using System . Collections . Concurrent ;
2121using System . Collections . Generic ;
22+ using System . Linq ;
2223using System . Net . Http ;
2324using System . Threading ;
2425using Grpc . Core ;
@@ -40,9 +41,11 @@ public sealed class GrpcChannel : ChannelBase, IDisposable
4041
4142 private readonly ConcurrentDictionary < IMethod , GrpcMethodInfo > _methodInfoCache ;
4243 private readonly Func < IMethod , GrpcMethodInfo > _createMethodInfoFunc ;
44+ // Internal for testing
45+ internal readonly HashSet < IDisposable > ActiveCalls ;
4346
4447 internal Uri Address { get ; }
45- internal HttpClient HttpClient { get ; }
48+ internal HttpMessageInvoker HttpInvoker { get ; }
4649 internal int ? SendMaxMessageSize { get ; }
4750 internal int ? ReceiveMaxMessageSize { get ; }
4851 internal ILoggerFactory LoggerFactory { get ; }
@@ -63,20 +66,22 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
6366 {
6467 _methodInfoCache = new ConcurrentDictionary < IMethod , GrpcMethodInfo > ( ) ;
6568
66- // Dispose the HttpClient if...
67- // 1. No client was specified and so the channel created the HttpClient itself
68- // 2. User has specified a client and set DisposeHttpClient to true
69- _shouldDisposeHttpClient = channelOptions . HttpClient == null || channelOptions . DisposeHttpClient ;
69+ // Dispose the HTTP client/handler if...
70+ // 1. No client/handler was specified and so the channel created the client itself
71+ // 2. User has specified a client/handler and set DisposeHttpClient to true
72+ _shouldDisposeHttpClient = ( channelOptions . HttpClient == null && channelOptions . HttpHandler == null )
73+ || channelOptions . DisposeHttpClient ;
7074
7175 Address = address ;
72- HttpClient = channelOptions . HttpClient ?? CreateInternalHttpClient ( ) ;
76+ HttpInvoker = channelOptions . HttpClient ?? CreateInternalHttpInvoker ( channelOptions . HttpHandler ) ;
7377 SendMaxMessageSize = channelOptions . MaxSendMessageSize ;
7478 ReceiveMaxMessageSize = channelOptions . MaxReceiveMessageSize ;
7579 CompressionProviders = ResolveCompressionProviders ( channelOptions . CompressionProviders ) ;
7680 MessageAcceptEncoding = GrpcProtocolHelpers . GetMessageAcceptEncoding ( CompressionProviders ) ;
7781 LoggerFactory = channelOptions . LoggerFactory ?? NullLoggerFactory . Instance ;
7882 ThrowOperationCanceledOnCancellation = channelOptions . ThrowOperationCanceledOnCancellation ;
7983 _createMethodInfoFunc = CreateMethodInfo ;
84+ ActiveCalls = new HashSet < IDisposable > ( ) ;
8085
8186 if ( channelOptions . Credentials != null )
8287 {
@@ -90,21 +95,29 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
9095 }
9196 }
9297
93- private static HttpClient CreateInternalHttpClient ( )
98+ private static HttpMessageInvoker CreateInternalHttpInvoker ( HttpMessageHandler ? handler )
9499 {
95- var httpClient = new HttpClient ( ) ;
96-
97- // Long running server and duplex streaming gRPC requests may not
98- // return any messages for over 100 seconds, triggering a cancellation
99- // of HttpClient.SendAsync. Disable timeout in internally created
100- // HttpClient for channel.
101- //
102- // gRPC deadline should be the recommended way to timeout gRPC calls.
103- //
104- // https://github.com/dotnet/corefx/issues/41650
105- httpClient . Timeout = Timeout . InfiniteTimeSpan ;
106-
107- return httpClient ;
100+ // HttpMessageInvoker should always dispose handler if Disposed is called on it.
101+ // Decision to dispose invoker is controlled by _shouldDisposeHttpClient.
102+ var httpInvoker = new HttpMessageInvoker ( handler ?? new HttpClientHandler ( ) , disposeHandler : true ) ;
103+
104+ return httpInvoker ;
105+ }
106+
107+ internal void RegisterActiveCall ( IDisposable grpcCall )
108+ {
109+ lock ( ActiveCalls )
110+ {
111+ ActiveCalls . Add ( grpcCall ) ;
112+ }
113+ }
114+
115+ internal void FinishActiveCall ( IDisposable grpcCall )
116+ {
117+ lock ( ActiveCalls )
118+ {
119+ ActiveCalls . Remove ( grpcCall ) ;
120+ }
108121 }
109122
110123 internal GrpcMethodInfo GetCachedGrpcMethodInfo ( IMethod method )
@@ -261,6 +274,12 @@ public static GrpcChannel ForAddress(Uri address, GrpcChannelOptions channelOpti
261274 throw new ArgumentNullException ( nameof ( channelOptions ) ) ;
262275 }
263276
277+ if ( channelOptions . HttpClient != null && channelOptions . HttpHandler != null )
278+ {
279+ throw new ArgumentException ( $ "{ nameof ( GrpcChannelOptions . HttpClient ) } and { nameof ( GrpcChannelOptions . HttpHandler ) } have been configured. " +
280+ $ "Only one HTTP caller can be specified.") ;
281+ }
282+
264283 return new GrpcChannel ( address , channelOptions ) ;
265284 }
266285
@@ -275,9 +294,24 @@ public void Dispose()
275294 return ;
276295 }
277296
297+ lock ( ActiveCalls )
298+ {
299+ if ( ActiveCalls . Count > 0 )
300+ {
301+ // Disposing a call will remove it from ActiveCalls. Need to take a copy
302+ // to avoid enumeration from being modified
303+ var activeCallsCopy = ActiveCalls . ToArray ( ) ;
304+
305+ foreach ( var activeCall in activeCallsCopy )
306+ {
307+ activeCall . Dispose ( ) ;
308+ }
309+ }
310+ }
311+
278312 if ( _shouldDisposeHttpClient )
279313 {
280- HttpClient . Dispose ( ) ;
314+ HttpInvoker . Dispose ( ) ;
281315 }
282316 Disposed = true ;
283317 }
0 commit comments