From 79c070798ce287c44b825e6671649f3d09ea854c Mon Sep 17 00:00:00 2001 From: Michael Landis Date: Wed, 28 Feb 2024 11:36:05 -0800 Subject: [PATCH] feat: adds customization options for SocketsHttpHandler In `Grpc.Net.Client`, the default HttpHandler on .NET runtimes .NET 5.0 or greater is `SocketsHttpHandler`. In this PR we create a new options class to encapsulate customizations to the handler, most importantly the pooled connection timeout and whether to enable multiple http connections. The default value for the idle connection timeout is 1 minute. We find this too strict in lambda environments, where the container may freeze and thaw in greater than 1 minute spans but less than the server timeout (5 minutes as of writing). Therefore we set the lambda config pooled connection idle timeout higher, to 6 minutes. That way a lambda function will not needlessly reconnect when the connection is still open. --- src/Momento.Sdk/CacheClient.cs | 9 ++- src/Momento.Sdk/Config/Configuration.cs | 6 ++ src/Momento.Sdk/Config/Configurations.cs | 3 +- src/Momento.Sdk/Config/IConfiguration.cs | 7 ++ .../Config/Transport/IGrpcConfiguration.cs | 16 +++++ .../Config/Transport/ITransportStrategy.cs | 7 ++ .../Transport/SocketsHttpHandlerOptions.cs | 66 +++++++++++++++++++ .../Transport/StaticTransportStrategy.cs | 27 ++++++-- .../Internal/ControlGrpcManager.cs | 20 ++++-- src/Momento.Sdk/Internal/DataGrpcManager.cs | 19 ++++-- src/Momento.Sdk/Internal/ScsControlClient.cs | 9 +-- src/Momento.Sdk/Momento.Sdk.csproj | 2 +- 12 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 src/Momento.Sdk/Config/Transport/SocketsHttpHandlerOptions.cs diff --git a/src/Momento.Sdk/CacheClient.cs b/src/Momento.Sdk/CacheClient.cs index 0d718142..a47a9e03 100644 --- a/src/Momento.Sdk/CacheClient.cs +++ b/src/Momento.Sdk/CacheClient.cs @@ -35,7 +35,7 @@ private ScsDataClient DataClient protected readonly IConfiguration config; /// protected readonly ILogger _logger; - + /// /// Async factory function to construct a Momento CacheClient with an eager connection to the /// Momento server. Calling the CacheClient constructor directly will not establish a connection @@ -66,10 +66,9 @@ public static async Task CreateAsync(IConfiguration config, ICrede public CacheClient(IConfiguration config, ICredentialProvider authProvider, TimeSpan defaultTtl) { this.config = config; - var _loggerFactory = config.LoggerFactory; - this._logger = _loggerFactory.CreateLogger(); + this._logger = config.LoggerFactory.CreateLogger(); Utils.ArgumentStrictlyPositive(defaultTtl, "defaultTtl"); - this.controlClient = new(_loggerFactory, authProvider.AuthToken, authProvider.ControlEndpoint); + this.controlClient = new(config, authProvider.AuthToken, authProvider.ControlEndpoint); this.dataClients = new List(); int minNumGrpcChannels = this.config.TransportStrategy.GrpcConfig.MinNumGrpcChannels; int currentMaxConcurrentRequests = this.config.TransportStrategy.MaxConcurrentRequests; @@ -995,7 +994,7 @@ public async Task SetFetchAsync(string cacheName, string return await this.DataClient.SetFetchAsync(cacheName, setName); } - + /// public async Task SetSampleAsync(string cacheName, string setName, int limit) { diff --git a/src/Momento.Sdk/Config/Configuration.cs b/src/Momento.Sdk/Config/Configuration.cs index 407e0a37..9f8973ce 100644 --- a/src/Momento.Sdk/Config/Configuration.cs +++ b/src/Momento.Sdk/Config/Configuration.cs @@ -61,6 +61,12 @@ public IConfiguration WithTransportStrategy(ITransportStrategy transportStrategy return new Configuration(LoggerFactory, RetryStrategy, Middlewares, transportStrategy); } + /// + public IConfiguration WithSocketsHttpHandlerOptions(SocketsHttpHandlerOptions options) + { + return new Configuration(LoggerFactory, RetryStrategy, Middlewares, TransportStrategy.WithSocketsHttpHandlerOptions(options)); + } + /// /// Add the specified middlewares to an existing instance of Configuration object in addition to already specified middlewares. /// diff --git a/src/Momento.Sdk/Config/Configurations.cs b/src/Momento.Sdk/Config/Configurations.cs index dc1f640e..81d619da 100644 --- a/src/Momento.Sdk/Config/Configurations.cs +++ b/src/Momento.Sdk/Config/Configurations.cs @@ -184,7 +184,8 @@ private Lambda(ILoggerFactory loggerFactory, IRetryStrategy retryStrategy, ITran /// public static IConfiguration Latest(ILoggerFactory? loggerFactory = null) { - return Default.V1(loggerFactory); + return Default.V1(loggerFactory).WithSocketsHttpHandlerOptions( + SocketsHttpHandlerOptions.Of(pooledConnectionIdleTimeout: TimeSpan.FromMinutes(6))); } } } diff --git a/src/Momento.Sdk/Config/IConfiguration.cs b/src/Momento.Sdk/Config/IConfiguration.cs index e1b7998e..20f306d7 100644 --- a/src/Momento.Sdk/Config/IConfiguration.cs +++ b/src/Momento.Sdk/Config/IConfiguration.cs @@ -43,6 +43,13 @@ public interface IConfiguration /// Configuration object with custom transport strategy provided public IConfiguration WithTransportStrategy(ITransportStrategy transportStrategy); + /// + /// Creates a new instance of the Configuration object, updated to use the specified SocketHttpHandler options. + /// + /// Customizations to the SocketsHttpHandler + /// + public IConfiguration WithSocketsHttpHandlerOptions(SocketsHttpHandlerOptions options); + /// /// Creates a new instance of the Configuration object, updated to use the specified client timeout. /// diff --git a/src/Momento.Sdk/Config/Transport/IGrpcConfiguration.cs b/src/Momento.Sdk/Config/Transport/IGrpcConfiguration.cs index d2e1e758..11f4751f 100644 --- a/src/Momento.Sdk/Config/Transport/IGrpcConfiguration.cs +++ b/src/Momento.Sdk/Config/Transport/IGrpcConfiguration.cs @@ -33,6 +33,15 @@ public interface IGrpcConfiguration /// public GrpcChannelOptions GrpcChannelOptions { get; } + /// + /// Override the SocketsHttpHandler's options. + /// This is irrelevant if the client is using the web client or the HttpClient (older .NET runtimes). + /// + /// + /// This is not part of the gRPC config because it is not part of . + /// + public SocketsHttpHandlerOptions SocketsHttpHandlerOptions { get; } + /// /// Copy constructor to override the Deadline /// @@ -54,4 +63,11 @@ public interface IGrpcConfiguration /// /// A new IGrpcConfiguration with the specified channel options public IGrpcConfiguration WithGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions); + + /// + /// Copy constructor to override the SocketsHttpHandler's options. + /// + /// + /// + public IGrpcConfiguration WithSocketsHttpHandlerOptions(SocketsHttpHandlerOptions idleTimeout); } diff --git a/src/Momento.Sdk/Config/Transport/ITransportStrategy.cs b/src/Momento.Sdk/Config/Transport/ITransportStrategy.cs index 78849ec5..3031f837 100644 --- a/src/Momento.Sdk/Config/Transport/ITransportStrategy.cs +++ b/src/Momento.Sdk/Config/Transport/ITransportStrategy.cs @@ -36,6 +36,13 @@ public interface ITransportStrategy /// A new ITransportStrategy with the specified grpcConfig public ITransportStrategy WithGrpcConfig(IGrpcConfiguration grpcConfig); + /// + /// Copy constructor to update the SocketsHttpHandler's options + /// + /// + /// + public ITransportStrategy WithSocketsHttpHandlerOptions(SocketsHttpHandlerOptions options); + /// /// Copy constructor to update the client timeout /// diff --git a/src/Momento.Sdk/Config/Transport/SocketsHttpHandlerOptions.cs b/src/Momento.Sdk/Config/Transport/SocketsHttpHandlerOptions.cs new file mode 100644 index 00000000..4393bcf6 --- /dev/null +++ b/src/Momento.Sdk/Config/Transport/SocketsHttpHandlerOptions.cs @@ -0,0 +1,66 @@ +#pragma warning disable 1591 +using System; +using Momento.Sdk.Internal; +namespace Momento.Sdk.Config.Transport; + +public class SocketsHttpHandlerOptions +{ + public static TimeSpan DefaultPooledConnectionIdleTimeout { get; } = TimeSpan.FromMinutes(1); + public TimeSpan PooledConnectionIdleTimeout { get; } = DefaultPooledConnectionIdleTimeout; + public bool EnableMultipleHttp2Connections { get; } = true; + + public SocketsHttpHandlerOptions() { } + public SocketsHttpHandlerOptions(TimeSpan pooledConnectionIdleTimeout) : this(pooledConnectionIdleTimeout, true) { } + public SocketsHttpHandlerOptions(bool enableMultipleHttp2Connections) : this(DefaultPooledConnectionIdleTimeout, enableMultipleHttp2Connections) { } + + public SocketsHttpHandlerOptions(TimeSpan pooledConnectionIdleTimeout, bool enableMultipleHttp2Connections) + { + Utils.ArgumentStrictlyPositive(pooledConnectionIdleTimeout, nameof(pooledConnectionIdleTimeout)); + PooledConnectionIdleTimeout = pooledConnectionIdleTimeout; + EnableMultipleHttp2Connections = enableMultipleHttp2Connections; + } + + public SocketsHttpHandlerOptions WithPooledConnectionIdleTimeout(TimeSpan pooledConnectionIdleTimeout) + { + return new SocketsHttpHandlerOptions(pooledConnectionIdleTimeout, EnableMultipleHttp2Connections); + } + + public SocketsHttpHandlerOptions WithEnableMultipleHttp2Connections(bool enableMultipleHttp2Connections) + { + return new SocketsHttpHandlerOptions(PooledConnectionIdleTimeout, enableMultipleHttp2Connections); + } + + public static SocketsHttpHandlerOptions Of(TimeSpan pooledConnectionIdleTimeout) + { + return new SocketsHttpHandlerOptions(pooledConnectionIdleTimeout); + } + + public static SocketsHttpHandlerOptions Of(bool enableMultipleHttp2Connections) + { + return new SocketsHttpHandlerOptions(enableMultipleHttp2Connections); + } + + public static SocketsHttpHandlerOptions Of(TimeSpan pooledConnectionIdleTimeout, bool enableMultipleHttp2Connections) + { + return new SocketsHttpHandlerOptions(pooledConnectionIdleTimeout, enableMultipleHttp2Connections); + } + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + + var other = (SocketsHttpHandlerOptions)obj; + return PooledConnectionIdleTimeout.Equals(other.PooledConnectionIdleTimeout) && + EnableMultipleHttp2Connections.Equals(other.EnableMultipleHttp2Connections); + } + + public override int GetHashCode() + { + return PooledConnectionIdleTimeout.GetHashCode() * 17 + EnableMultipleHttp2Connections.GetHashCode(); + } + + +} diff --git a/src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs b/src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs index e329f5e7..97d90dc0 100644 --- a/src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs +++ b/src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs @@ -17,6 +17,8 @@ public class StaticGrpcConfiguration : IGrpcConfiguration public int MinNumGrpcChannels { get; } /// public GrpcChannelOptions GrpcChannelOptions { get; } + /// + public SocketsHttpHandlerOptions SocketsHttpHandlerOptions { get; } /// /// @@ -24,30 +26,38 @@ public class StaticGrpcConfiguration : IGrpcConfiguration /// Maximum amount of time before a request will timeout /// Customizations to low-level gRPC channel configuration /// minimum number of gRPC channels to open - public StaticGrpcConfiguration(TimeSpan deadline, GrpcChannelOptions? grpcChannelOptions = null, int minNumGrpcChannels = 1) + /// Customizations to the SocketsHttpHandler + public StaticGrpcConfiguration(TimeSpan deadline, GrpcChannelOptions? grpcChannelOptions = null, int minNumGrpcChannels = 1, SocketsHttpHandlerOptions? socketsHttpHandlerOptions = null) { Utils.ArgumentStrictlyPositive(deadline, nameof(deadline)); this.Deadline = deadline; this.MinNumGrpcChannels = minNumGrpcChannels; this.GrpcChannelOptions = grpcChannelOptions ?? new GrpcChannelOptions(); + this.SocketsHttpHandlerOptions = socketsHttpHandlerOptions ?? new SocketsHttpHandlerOptions(); } /// public IGrpcConfiguration WithDeadline(TimeSpan deadline) { - return new StaticGrpcConfiguration(deadline, this.GrpcChannelOptions, this.MinNumGrpcChannels); + return new StaticGrpcConfiguration(deadline, GrpcChannelOptions, MinNumGrpcChannels, SocketsHttpHandlerOptions); } /// public IGrpcConfiguration WithMinNumGrpcChannels(int minNumGrpcChannels) { - return new StaticGrpcConfiguration(this.Deadline, this.GrpcChannelOptions, minNumGrpcChannels); + return new StaticGrpcConfiguration(Deadline, GrpcChannelOptions, minNumGrpcChannels, SocketsHttpHandlerOptions); } /// public IGrpcConfiguration WithGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) { - return new StaticGrpcConfiguration(this.Deadline, grpcChannelOptions, this.MinNumGrpcChannels); + return new StaticGrpcConfiguration(Deadline, grpcChannelOptions, MinNumGrpcChannels); + } + + /// + public IGrpcConfiguration WithSocketsHttpHandlerOptions(SocketsHttpHandlerOptions options) + { + return new StaticGrpcConfiguration(Deadline, GrpcChannelOptions, MinNumGrpcChannels, options); } /// @@ -61,7 +71,8 @@ public override bool Equals(object obj) var other = (StaticGrpcConfiguration)obj; return Deadline.Equals(other.Deadline) && - MinNumGrpcChannels == other.MinNumGrpcChannels; + MinNumGrpcChannels == other.MinNumGrpcChannels && + SocketsHttpHandlerOptions.Equals(other.SocketsHttpHandlerOptions); // TODO: gRPC doesn't implement a to equals for this //GrpcChannelOptions.Equals(other.GrpcChannelOptions); } @@ -121,6 +132,12 @@ public ITransportStrategy WithGrpcConfig(IGrpcConfiguration grpcConfig) return new StaticTransportStrategy(_loggerFactory, MaxConcurrentRequests, grpcConfig); } + /// + public ITransportStrategy WithSocketsHttpHandlerOptions(SocketsHttpHandlerOptions options) + { + return new StaticTransportStrategy(_loggerFactory, MaxConcurrentRequests, GrpcConfig.WithSocketsHttpHandlerOptions(options)); + } + /// public ITransportStrategy WithClientTimeout(TimeSpan clientTimeout) { diff --git a/src/Momento.Sdk/Internal/ControlGrpcManager.cs b/src/Momento.Sdk/Internal/ControlGrpcManager.cs index 7a1bdbfd..e357ba39 100644 --- a/src/Momento.Sdk/Internal/ControlGrpcManager.cs +++ b/src/Momento.Sdk/Internal/ControlGrpcManager.cs @@ -4,12 +4,13 @@ using System.Threading.Tasks; using Grpc.Core; using Grpc.Net.Client; -#if USE_GRPC_WEB using System.Net.Http; +#if USE_GRPC_WEB using Grpc.Net.Client.Web; #endif using Microsoft.Extensions.Logging; using Momento.Protos.ControlClient; +using Momento.Sdk.Config; using Momento.Sdk.Config.Middleware; using Momento.Sdk.Internal.Middleware; using static System.Reflection.Assembly; @@ -86,8 +87,9 @@ internal sealed class ControlGrpcManager : IDisposable private readonly string runtimeVersion = $"{moniker}:{System.Environment.Version}"; private readonly ILogger _logger; - public ControlGrpcManager(ILoggerFactory loggerFactory, string authToken, string endpoint) + public ControlGrpcManager(IConfiguration config, string authToken, string endpoint) { + this._logger = config.LoggerFactory.CreateLogger(); #if USE_GRPC_WEB // Note: all web SDK requests are routed to a `web.` subdomain to allow us flexibility on the server endpoint = $"web.{endpoint}"; @@ -98,20 +100,24 @@ public ControlGrpcManager(ILoggerFactory loggerFactory, string authToken, string Credentials = ChannelCredentials.SecureSsl, MaxReceiveMessageSize = Internal.Utils.DEFAULT_MAX_MESSAGE_SIZE, MaxSendMessageSize = Internal.Utils.DEFAULT_MAX_MESSAGE_SIZE, -#if USE_GRPC_WEB - HttpHandler = new GrpcWebHandler(new HttpClientHandler()), +#if NET5_0_OR_GREATER + HttpHandler = new System.Net.Http.SocketsHttpHandler + { + EnableMultipleHttp2Connections = config.TransportStrategy.GrpcConfig.SocketsHttpHandlerOptions.EnableMultipleHttp2Connections, + PooledConnectionIdleTimeout = config.TransportStrategy.GrpcConfig.SocketsHttpHandlerOptions.PooledConnectionIdleTimeout + } +#elif USE_GRPC_WEB + HttpHandler = new GrpcWebHandler(new HttpClientHandler()) #endif }); List
headers = new List
{ new Header(name: Header.AuthorizationKey, value: authToken), new Header(name: Header.AgentKey, value: version), new Header(name: Header.RuntimeVersionKey, value: runtimeVersion) }; CallInvoker invoker = this.channel.CreateCallInvoker(); var middlewares = new List { - new HeaderMiddleware(loggerFactory, headers) + new HeaderMiddleware(config.LoggerFactory, headers) }; Client = new ControlClientWithMiddleware(new ScsControl.ScsControlClient(invoker), middlewares); - - this._logger = loggerFactory.CreateLogger(); } public void Dispose() diff --git a/src/Momento.Sdk/Internal/DataGrpcManager.cs b/src/Momento.Sdk/Internal/DataGrpcManager.cs index 5f7444ef..09960dc0 100644 --- a/src/Momento.Sdk/Internal/DataGrpcManager.cs +++ b/src/Momento.Sdk/Internal/DataGrpcManager.cs @@ -5,8 +5,8 @@ using System.Threading.Tasks; using Grpc.Core; using Grpc.Net.Client; -#if USE_GRPC_WEB using System.Net.Http; +#if USE_GRPC_WEB using Grpc.Net.Client.Web; #endif using Microsoft.Extensions.Logging; @@ -182,7 +182,7 @@ public async Task<_SetSampleResponse> SetSampleAsync(_SetSampleRequest request, var wrapped = await _middlewares.WrapRequest(request, callOptions, (r, o) => _generatedClient.SetSampleAsync(r, o)); return await wrapped.ResponseAsync; } - + public async Task<_SetLengthResponse> SetLengthAsync(_SetLengthRequest request, CallOptions callOptions) { var wrapped = await _middlewares.WrapRequest(request, callOptions, (r, o) => _generatedClient.SetLengthAsync(r, o)); @@ -269,6 +269,7 @@ public class DataGrpcManager : IDisposable internal DataGrpcManager(IConfiguration config, string authToken, string endpoint) { + this._logger = config.LoggerFactory.CreateLogger(); #if USE_GRPC_WEB // Note: all web SDK requests are routed to a `web.` subdomain to allow us flexibility on the server endpoint = $"web.{endpoint}"; @@ -282,16 +283,20 @@ internal DataGrpcManager(IConfiguration config, string authToken, string endpoin channelOptions.Credentials = ChannelCredentials.SecureSsl; channelOptions.MaxReceiveMessageSize = Internal.Utils.DEFAULT_MAX_MESSAGE_SIZE; channelOptions.MaxSendMessageSize = Internal.Utils.DEFAULT_MAX_MESSAGE_SIZE; - -#if USE_GRPC_WEB + +#if NET5_0_OR_GREATER + channelOptions.HttpHandler = new SocketsHttpHandler + { + EnableMultipleHttp2Connections = config.TransportStrategy.GrpcConfig.SocketsHttpHandlerOptions.EnableMultipleHttp2Connections, + PooledConnectionIdleTimeout = config.TransportStrategy.GrpcConfig.SocketsHttpHandlerOptions.PooledConnectionIdleTimeout + }; +#elif USE_GRPC_WEB channelOptions.HttpHandler = new GrpcWebHandler(new HttpClientHandler()); #endif this.channel = GrpcChannel.ForAddress(uri, channelOptions); List
headers = new List
{ new Header(name: Header.AuthorizationKey, value: authToken), new Header(name: Header.AgentKey, value: version), new Header(name: Header.RuntimeVersionKey, value: runtimeVersion) }; - this._logger = config.LoggerFactory.CreateLogger(); - CallInvoker invoker = this.channel.CreateCallInvoker(); var middlewares = config.Middlewares.Concat( @@ -305,7 +310,7 @@ internal DataGrpcManager(IConfiguration config, string authToken, string endpoin var client = new Scs.ScsClient(invoker); Client = new DataClientWithMiddleware(client, middlewares); } - + internal async Task EagerConnectAsync(TimeSpan eagerConnectionTimeout) { _logger.LogDebug("Attempting eager connection to server"); diff --git a/src/Momento.Sdk/Internal/ScsControlClient.cs b/src/Momento.Sdk/Internal/ScsControlClient.cs index c7bf653a..9ec7e952 100644 --- a/src/Momento.Sdk/Internal/ScsControlClient.cs +++ b/src/Momento.Sdk/Internal/ScsControlClient.cs @@ -3,6 +3,7 @@ using Grpc.Core; using Microsoft.Extensions.Logging; using Momento.Protos.ControlClient; +using Momento.Sdk.Config; using Momento.Sdk.Exceptions; using Momento.Sdk.Responses; @@ -17,12 +18,12 @@ internal sealed class ScsControlClient : IDisposable private readonly ILogger _logger; private readonly CacheExceptionMapper _exceptionMapper; - public ScsControlClient(ILoggerFactory loggerFactory, string authToken, string endpoint) + public ScsControlClient(IConfiguration config, string authToken, string endpoint) { - this.grpcManager = new ControlGrpcManager(loggerFactory, authToken, endpoint); + this.grpcManager = new ControlGrpcManager(config, authToken, endpoint); this.authToken = authToken; - this._logger = loggerFactory.CreateLogger(); - this._exceptionMapper = new CacheExceptionMapper(loggerFactory); + this._logger = config.LoggerFactory.CreateLogger(); + this._exceptionMapper = new CacheExceptionMapper(config.LoggerFactory); } public async Task CreateCacheAsync(string cacheName) diff --git a/src/Momento.Sdk/Momento.Sdk.csproj b/src/Momento.Sdk/Momento.Sdk.csproj index e5372f84..3d9aba5a 100644 --- a/src/Momento.Sdk/Momento.Sdk.csproj +++ b/src/Momento.Sdk/Momento.Sdk.csproj @@ -2,7 +2,7 @@ - netstandard2.0;net461 + netstandard2.0;net6.0;net461 latest enable