Skip to content

Commit

Permalink
feat: add grpc config options and turn off keepalive for Lambda confi…
Browse files Browse the repository at this point in the history
…g" (#538)

* chore: DRY up grpc channel options construction

* finish rebasing

* always set default max message size for now

* add keepalive settings to sockets http handler

* actually add keepalive settings to sockets http handler

* add keepalive settings to prebuilt configs with transport strategy except for Lambda config

* correct placement of keepalive settings in configs

* remove unnecessary import

* remove unnecessary optional chaining

* add some unit tests for configs

* further DRY up grpc manager code using base GrpcManager class

* clean up GrpcManager a bit more

* completely remove grpc options function from utils

* move keepalive settings into sockets http handler options

* create default grpc options object if none provided to transport strategy

* undo unnecessary changes

* fix build warning: static grpc config should not have nullable grpc channel options object
  • Loading branch information
anitarua authored Mar 4, 2024
1 parent 7c9f16d commit 4db09f8
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 328 deletions.
14 changes: 13 additions & 1 deletion src/Momento.Sdk/Config/Configurations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ public static IConfiguration V1(ILoggerFactory? loggerFactory = null)
/// This config optimizes for lambda environments. In addition to the in region settings of
/// <see cref="Default"/>, this configures the clients to eagerly connect to the Momento service
/// to avoid the cold start penalty of establishing a connection on the first request.
/// NOTE: keep-alives are very important for long-lived server environments where there may be periods of time
/// when the connection is idle. However, they are very problematic for lambda environments where the lambda
/// runtime is continuously frozen and unfrozen, because the lambda may be frozen before the "ACK" is received
/// from the server. This can cause the keep-alive to timeout even though the connection is completely healthy.
/// Therefore, keep-alives should be disabled in lambda and similar environments.
/// </summary>
public class Lambda : Configuration
{
Expand All @@ -186,7 +191,14 @@ public static IConfiguration V1(ILoggerFactory? loggerFactory = null)
{
var config = Default.V1(loggerFactory);
var transportStrategy = config.TransportStrategy.WithSocketsHttpHandlerOptions(
SocketsHttpHandlerOptions.Of(pooledConnectionIdleTimeout: TimeSpan.FromMinutes(6)));
SocketsHttpHandlerOptions.Of(
pooledConnectionIdleTimeout: TimeSpan.FromMinutes(6),
enableMultipleHttp2Connections: true,
keepAlivePingTimeout: System.Threading.Timeout.InfiniteTimeSpan,
keepAlivePingDelay: System.Threading.Timeout.InfiniteTimeSpan,
keepAlivePermitWithoutCalls: false
)
);
return config.WithTransportStrategy(transportStrategy);
}

Expand Down
3 changes: 2 additions & 1 deletion src/Momento.Sdk/Config/TopicConfigurations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ public static ITopicConfiguration Latest(ILoggerFactory? loggerFactory = null)
var finalLoggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
ITopicTransportStrategy transportStrategy = new StaticTopicTransportStrategy(
loggerFactory: finalLoggerFactory,
grpcConfig: new StaticGrpcConfiguration(deadline: TimeSpan.FromMilliseconds(1100)));
grpcConfig: new StaticGrpcConfiguration(deadline: TimeSpan.FromMilliseconds(1100))
);
return new Default(finalLoggerFactory, transportStrategy);
}
}
Expand Down
63 changes: 63 additions & 0 deletions src/Momento.Sdk/Config/Transport/SocketsHttpHandlerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma warning disable 1591
using System;
using System.Net.Http;
using Momento.Sdk.Internal;
namespace Momento.Sdk.Config.Transport;

Expand All @@ -9,6 +10,36 @@ public class SocketsHttpHandlerOptions
public TimeSpan PooledConnectionIdleTimeout { get; } = DefaultPooledConnectionIdleTimeout;
public bool EnableMultipleHttp2Connections { get; } = true;

/// <summary>
/// Override the time to wait for a response from a keepalive or ping.
/// NOTE: keep-alives are very important for long-lived server environments where there may be periods of time
/// when the connection is idle. However, they are very problematic for lambda environments where the lambda
/// runtime is continuously frozen and unfrozen, because the lambda may be frozen before the "ACK" is received
/// from the server. This can cause the keep-alive to timeout even though the connection is completely healthy.
/// Therefore, keep-alives should be disabled in lambda and similar environments.
/// </summary>
public TimeSpan KeepAlivePingTimeout { get; } = TimeSpan.FromMilliseconds(1000);

/// <summary>
/// After a duration of this time the client/server pings its peer to see if the transport is still alive.
/// NOTE: keep-alives are very important for long-lived server environments where there may be periods of time
/// when the connection is idle. However, they are very problematic for lambda environments where the lambda
/// runtime is continuously frozen and unfrozen, because the lambda may be frozen before the "ACK" is received
/// from the server. This can cause the keep-alive to timeout even though the connection is completely healthy.
/// Therefore, keep-alives should be disabled in lambda and similar environments.
/// </summary>
public TimeSpan KeepAlivePingDelay { get; } = TimeSpan.FromMilliseconds(5000);

/// <summary>
/// Indicates if it permissible to send keepalive pings from the client without any outstanding streams.
/// NOTE: keep-alives are very important for long-lived server environments where there may be periods of time
/// when the connection is idle. However, they are very problematic for lambda environments where the lambda
/// runtime is continuously frozen and unfrozen, because the lambda may be frozen before the "ACK" is received
/// from the server. This can cause the keep-alive to timeout even though the connection is completely healthy.
/// Therefore, keep-alives should be disabled in lambda and similar environments.
/// </summary>
public bool KeepAlivePermitWithoutCalls { get; } = true;

public SocketsHttpHandlerOptions() { }
public SocketsHttpHandlerOptions(TimeSpan pooledConnectionIdleTimeout) : this(pooledConnectionIdleTimeout, true) { }
public SocketsHttpHandlerOptions(bool enableMultipleHttp2Connections) : this(DefaultPooledConnectionIdleTimeout, enableMultipleHttp2Connections) { }
Expand All @@ -19,6 +50,21 @@ public SocketsHttpHandlerOptions(TimeSpan pooledConnectionIdleTimeout, bool enab
PooledConnectionIdleTimeout = pooledConnectionIdleTimeout;
EnableMultipleHttp2Connections = enableMultipleHttp2Connections;
}
public SocketsHttpHandlerOptions(
TimeSpan pooledConnectionIdleTimeout,
bool enableMultipleHttp2Connections,
TimeSpan keepAlivePingTimeout,
TimeSpan keepAlivePingDelay,
bool keepAlivePermitWithoutCalls
)
{
Utils.ArgumentStrictlyPositive(pooledConnectionIdleTimeout, nameof(pooledConnectionIdleTimeout));
PooledConnectionIdleTimeout = pooledConnectionIdleTimeout;
EnableMultipleHttp2Connections = enableMultipleHttp2Connections;
KeepAlivePingTimeout = keepAlivePingTimeout;
KeepAlivePingDelay = keepAlivePingDelay;
KeepAlivePermitWithoutCalls = keepAlivePermitWithoutCalls;
}

public SocketsHttpHandlerOptions WithPooledConnectionIdleTimeout(TimeSpan pooledConnectionIdleTimeout)
{
Expand All @@ -45,6 +91,23 @@ public static SocketsHttpHandlerOptions Of(TimeSpan pooledConnectionIdleTimeout,
return new SocketsHttpHandlerOptions(pooledConnectionIdleTimeout, enableMultipleHttp2Connections);
}

public static SocketsHttpHandlerOptions Of(
TimeSpan pooledConnectionIdleTimeout,
bool enableMultipleHttp2Connections,
TimeSpan keepAlivePingTimeout,
TimeSpan keepAlivePingDelay,
bool keepAlivePermitWithoutCalls
)
{
return new SocketsHttpHandlerOptions(
pooledConnectionIdleTimeout,
enableMultipleHttp2Connections,
keepAlivePingTimeout,
keepAlivePingDelay,
keepAlivePermitWithoutCalls
);
}

public override bool Equals(object obj)

Check warning on line 111 in src/Momento.Sdk/Config/Transport/SocketsHttpHandlerOptions.cs

View workflow job for this annotation

GitHub Actions / build_csharp (ubuntu-latest, net6.0)

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).

Check warning on line 111 in src/Momento.Sdk/Config/Transport/SocketsHttpHandlerOptions.cs

View workflow job for this annotation

GitHub Actions / build_csharp (windows-latest, net462)

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).
{
if (obj == null || GetType() != obj.GetType())
Expand Down
29 changes: 27 additions & 2 deletions src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Net.Http;
using Grpc.Net.Client;
using Microsoft.Extensions.Logging;
using Momento.Sdk.Internal;
Expand All @@ -19,6 +20,12 @@ public class StaticGrpcConfiguration : IGrpcConfiguration
public GrpcChannelOptions GrpcChannelOptions { get; }
/// <inheritdoc/>
public SocketsHttpHandlerOptions SocketsHttpHandlerOptions { get; }
/// <inheritdoc/>
public TimeSpan KeepAlivePingTimeout { get; }
/// <inheritdoc/>
public TimeSpan KeepAlivePingDelay { get; }
/// <inheritdoc/>
public bool KeepAlivePermitWithoutCalls { get; }

/// <summary>
///
Expand All @@ -27,15 +34,33 @@ public class StaticGrpcConfiguration : IGrpcConfiguration
/// <param name="grpcChannelOptions">Customizations to low-level gRPC channel configuration</param>
/// <param name="minNumGrpcChannels">minimum number of gRPC channels to open</param>
/// <param name="socketsHttpHandlerOptions">Customizations to the SocketsHttpHandler</param>
public StaticGrpcConfiguration(TimeSpan deadline, GrpcChannelOptions? grpcChannelOptions = null, int minNumGrpcChannels = 1, SocketsHttpHandlerOptions? socketsHttpHandlerOptions = null)
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.GrpcChannelOptions = grpcChannelOptions ?? DefaultGrpcChannelOptions();
this.SocketsHttpHandlerOptions = socketsHttpHandlerOptions ?? new SocketsHttpHandlerOptions();
}

/// <summary>
/// The grpc default value for max_send_message_length is 4mb. This function returns default grpc options that increase max message size to 5mb in order to support cases where users have requested a limit increase up to our maximum item size of 5mb.
/// </summary>
/// <returns>GrpcChannelOptions</returns>
public static GrpcChannelOptions DefaultGrpcChannelOptions() {
const int DEFAULT_MAX_MESSAGE_SIZE = 5_243_000;
return new GrpcChannelOptions
{
MaxReceiveMessageSize = DEFAULT_MAX_MESSAGE_SIZE,
MaxSendMessageSize = DEFAULT_MAX_MESSAGE_SIZE
};
}

/// <inheritdoc/>
public IGrpcConfiguration WithDeadline(TimeSpan deadline)
{
Expand Down
58 changes: 5 additions & 53 deletions src/Momento.Sdk/Internal/AuthGrpcManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,65 +45,17 @@ public async Task<_GenerateDisposableTokenResponse> generateDisposableToken(_Gen

}

public class AuthGrpcManager : IDisposable
public class AuthGrpcManager : GrpcManager
{
private readonly GrpcChannel channel;
public IAuthClient Client { get; }

#if USE_GRPC_WEB
private readonly static string moniker = "dotnet-web";
#else
private readonly static string moniker = "dotnet";
#endif
private readonly string version = $"{moniker}:{GetAssembly(typeof(Momento.Sdk.Responses.CacheGetResponse)).GetName().Version.ToString()}";
// Some System.Environment.Version remarks to be aware of
// https://learn.microsoft.com/en-us/dotnet/api/system.environment.version?view=netstandard-2.0#remarks
private readonly string runtimeVersion = $"{moniker}:{System.Environment.Version}";

public AuthGrpcManager(IAuthConfiguration config, string authToken, string endpoint)
public AuthGrpcManager(IAuthConfiguration config, string authToken, string endpoint): base(config.TransportStrategy.GrpcConfig, config.LoggerFactory, authToken, endpoint, "AuthGrpcManager")
{
#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}";
#endif
var uri = $"https://{endpoint}";
var channelOptions = config.TransportStrategy.GrpcConfig.GrpcChannelOptions;
if (channelOptions.LoggerFactory == null)
{
channelOptions.LoggerFactory = config.LoggerFactory;
}

channelOptions.Credentials = ChannelCredentials.SecureSsl;
channelOptions.MaxReceiveMessageSize = Internal.Utils.DEFAULT_MAX_MESSAGE_SIZE;
channelOptions.MaxSendMessageSize = Internal.Utils.DEFAULT_MAX_MESSAGE_SIZE;

#if USE_GRPC_WEB
channelOptions.HttpHandler = new GrpcWebHandler(new HttpClientHandler());
#endif

channel = GrpcChannel.ForAddress(uri, channelOptions);

var headerTuples = new List<Tuple<string, string>>
{
new(Header.AuthorizationKey, authToken),
new(Header.AgentKey, version),
new(Header.RuntimeVersionKey, runtimeVersion)
};
var headers = headerTuples.Select(tuple => new Header(name: tuple.Item1, value: tuple.Item2)).ToList();

CallInvoker invoker = this.channel.CreateCallInvoker();

var middlewares = new List<IMiddleware> {
new HeaderMiddleware(config.LoggerFactory, headers)
new HeaderMiddleware(config.LoggerFactory, this.headers)
};

var client = new Token.TokenClient(invoker);
Client = new AuthClientWithMiddleware(client, middlewares, headerTuples);
}

public void Dispose()
{
this.channel.Dispose();
GC.SuppressFinalize(this);
var client = new Token.TokenClient(this.invoker);
Client = new AuthClientWithMiddleware(client, middlewares, this.headerTuples);
}
}
58 changes: 6 additions & 52 deletions src/Momento.Sdk/Internal/ControlGrpcManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,64 +71,18 @@ public async Task<_ListCachesResponse> ListCachesAsync(_ListCachesRequest reques
}
}

internal sealed class ControlGrpcManager : IDisposable
internal sealed class ControlGrpcManager : GrpcManager
{
private readonly GrpcChannel channel;
public IControlClient Client { get; }

#if USE_GRPC_WEB
private readonly static string moniker = "dotnet-web";
#else
private readonly static string moniker = "dotnet";
#endif
private readonly string version = $"{moniker}:{GetAssembly(typeof(Momento.Sdk.Responses.CacheGetResponse)).GetName().Version.ToString()}";
// Some System.Environment.Version remarks to be aware of
// https://learn.microsoft.com/en-us/dotnet/api/system.environment.version?view=netstandard-2.0#remarks
private readonly string runtimeVersion = $"{moniker}:{System.Environment.Version}";
private readonly ILogger _logger;

public ControlGrpcManager(IConfiguration config, string authToken, string endpoint)
public ControlGrpcManager(IConfiguration config, string authToken, string endpoint): base(config.TransportStrategy.GrpcConfig, config.LoggerFactory, authToken, endpoint, "ControlGrpcManager")
{
this._logger = config.LoggerFactory.CreateLogger<ControlGrpcManager>();
#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}";
#endif
var uri = $"https://{endpoint}";

var channelOptions = new GrpcChannelOptions
{
Credentials = ChannelCredentials.SecureSsl,
MaxReceiveMessageSize = Internal.Utils.DEFAULT_MAX_MESSAGE_SIZE,
MaxSendMessageSize = Internal.Utils.DEFAULT_MAX_MESSAGE_SIZE,
};
#if NET5_0_OR_GREATER

if (SocketsHttpHandler.IsSupported) // see: https://github.com/grpc/grpc-dotnet/blob/098dca892a3949ade411c3f2f66003f7b330dfd2/src/Shared/HttpHandlerFactory.cs#L28-L30
var middlewares = new List<IMiddleware>
{
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<Header> headers = new List<Header> { 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<IMiddleware> {
new HeaderMiddleware(config.LoggerFactory, headers)
new HeaderMiddleware(config.LoggerFactory, this.headers)
};

Client = new ControlClientWithMiddleware(new ScsControl.ScsControlClient(invoker), middlewares);
}

public void Dispose()
{
this.channel.Dispose();
GC.SuppressFinalize(this);
var client = new ScsControl.ScsControlClient(this.invoker);
Client = new ControlClientWithMiddleware(client, middlewares);
}
}
Loading

0 comments on commit 4db09f8

Please sign in to comment.