From b033154088ce920a7f6cf3ffa77f74f3f8d0dca2 Mon Sep 17 00:00:00 2001 From: Michael Landis Date: Thu, 22 Sep 2022 13:52:22 -0700 Subject: [PATCH] feat: SDK configurables (#170) This adds the configurables data structures the project, as specified here. Concrete implementations and integration will happen in followup PRs. --- .../MomentoApplication.csproj | 3 +- src/Momento.Sdk/Config/Configuration.cs | 26 +++++ src/Momento.Sdk/Config/Configurations.cs | 101 ++++++++++++++++++ src/Momento.Sdk/Config/IConfiguration.cs | 17 +++ .../Config/Middleware/IMiddleware.cs | 23 ++++ .../Middleware/PassThroughMiddleware.cs | 17 +++ .../Config/Retry/FixedCountRetryStrategy.cs | 20 ++++ src/Momento.Sdk/Config/Retry/IGrpcRequest.cs | 6 ++ src/Momento.Sdk/Config/Retry/IGrpcResponse.cs | 6 ++ .../Config/Retry/IRetryStrategy.cs | 16 +++ .../Config/Transport/IGrpcConfiguration.cs | 26 +++++ .../Config/Transport/ITransportStrategy.cs | 10 ++ .../Transport/StaticTransportStrategy.cs | 33 ++++++ src/Momento.Sdk/SimpleCacheClient.cs | 6 +- .../Integration/Momento.Sdk.Tests/Fixtures.cs | 4 +- .../SimpleCacheControlTest.cs | 7 +- .../Momento.Sdk.Tests/SimpleCacheDataTest.cs | 3 +- 17 files changed, 316 insertions(+), 8 deletions(-) create mode 100644 src/Momento.Sdk/Config/Configuration.cs create mode 100644 src/Momento.Sdk/Config/Configurations.cs create mode 100644 src/Momento.Sdk/Config/IConfiguration.cs create mode 100644 src/Momento.Sdk/Config/Middleware/IMiddleware.cs create mode 100644 src/Momento.Sdk/Config/Middleware/PassThroughMiddleware.cs create mode 100644 src/Momento.Sdk/Config/Retry/FixedCountRetryStrategy.cs create mode 100644 src/Momento.Sdk/Config/Retry/IGrpcRequest.cs create mode 100644 src/Momento.Sdk/Config/Retry/IGrpcResponse.cs create mode 100644 src/Momento.Sdk/Config/Retry/IRetryStrategy.cs create mode 100644 src/Momento.Sdk/Config/Transport/IGrpcConfiguration.cs create mode 100644 src/Momento.Sdk/Config/Transport/ITransportStrategy.cs create mode 100644 src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs diff --git a/examples/MomentoApplication/MomentoApplication.csproj b/examples/MomentoApplication/MomentoApplication.csproj index 3db91ed6..4b042904 100644 --- a/examples/MomentoApplication/MomentoApplication.csproj +++ b/examples/MomentoApplication/MomentoApplication.csproj @@ -7,7 +7,6 @@ - - + diff --git a/src/Momento.Sdk/Config/Configuration.cs b/src/Momento.Sdk/Config/Configuration.cs new file mode 100644 index 00000000..864bbdc8 --- /dev/null +++ b/src/Momento.Sdk/Config/Configuration.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Momento.Sdk.Config.Middleware; +using Momento.Sdk.Config.Retry; +using Momento.Sdk.Config.Transport; + +namespace Momento.Sdk.Config; + +public class Configuration : IConfiguration +{ + public IRetryStrategy RetryStrategy { get; } + public IList Middlewares { get; } + public ITransportStrategy TransportStrategy { get; } + + public Configuration(IRetryStrategy retryStrategy, ITransportStrategy transportStrategy) + : this(retryStrategy, new List(), transportStrategy) + { + + } + + public Configuration(IRetryStrategy retryStrategy, IList middlewares, ITransportStrategy transportStrategy) + { + this.RetryStrategy = retryStrategy; + this.Middlewares = middlewares; + this.TransportStrategy = transportStrategy; + } +} diff --git a/src/Momento.Sdk/Config/Configurations.cs b/src/Momento.Sdk/Config/Configurations.cs new file mode 100644 index 00000000..589efa5d --- /dev/null +++ b/src/Momento.Sdk/Config/Configurations.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using Momento.Sdk.Config.Middleware; +using Momento.Sdk.Config.Retry; +using Momento.Sdk.Config.Transport; + +namespace Momento.Sdk.Config; + +public class Configurations +{ + /// + /// Laptop config provides defaults suitable for a medium-to-high-latency dev environment. Permissive timeouts, retries, potentially + /// a higher number of connections, etc. + /// + public class Laptop : Configuration + { + private Laptop(IRetryStrategy retryStrategy, ITransportStrategy transportStrategy) + : base(retryStrategy, transportStrategy) + { + + } + + public static Laptop Latest + { + get + { + /*retryableStatusCodes = DEFAULT_RETRYABLE_STATUS_CODES,*/ + IRetryStrategy retryStrategy = new FixedCountRetryStrategy(maxAttempts: 3); + ITransportStrategy transportStrategy = new StaticTransportStrategy( + maxConcurrentRequests: 1, + grpcConfig: new StaticGrpcConfiguration(numChannels: 6, maxSessionMemory: 128, useLocalSubChannelPool: true)); + return new Laptop(retryStrategy, transportStrategy); + } + } + } + + /// + /// InRegion provides defaults suitable for an environment where your client is running in the same region as the Momento + /// service. It has more agressive timeouts and retry behavior than the Laptop config. + /// + public class InRegion + { + /// + /// This config prioritizes throughput and client resource utilization. + /// + public class Default : Configuration + { + private Default(IRetryStrategy retryStrategy, ITransportStrategy transportStrategy) + : base(retryStrategy, transportStrategy) + { + + } + + public static Default Latest + { + get + { + /*retryableStatusCodes = DEFAULT_RETRYABLE_STATUS_CODES,*/ + IRetryStrategy retryStrategy = new FixedCountRetryStrategy(maxAttempts: 3); + ITransportStrategy transportStrategy = new StaticTransportStrategy( + maxConcurrentRequests: 1, + grpcConfig: new StaticGrpcConfiguration(numChannels: 6, maxSessionMemory: 128, useLocalSubChannelPool: true)); + return new Default(retryStrategy, transportStrategy); + } + } + } + + /// + /// This config prioritizes keeping p99.9 latencies as low as possible, potentially sacrificing + /// some throughput to achieve this. Use this configuration if the most important factor is to ensure that cache + /// unavailability doesn't force unacceptably high latencies for your own application. + /// + public class LowLatency : Configuration + { + private LowLatency(IRetryStrategy retryStrategy, ITransportStrategy transportStrategy) + : base(retryStrategy, transportStrategy) + { + + } + + public static LowLatency Latest + { + get + { + /*retryableStatusCodes = DEFAULT_RETRYABLE_STATUS_CODES,*/ + IRetryStrategy retryStrategy = new FixedCountRetryStrategy(maxAttempts: 3); + ITransportStrategy transportStrategy = new StaticTransportStrategy( + maxConcurrentRequests: 1, + grpcConfig: new StaticGrpcConfiguration(numChannels: 6, maxSessionMemory: 128, useLocalSubChannelPool: true)); + return new LowLatency(retryStrategy, transportStrategy); + } + } + } + } + + /// + public static readonly IConfiguration DevConfig = Laptop.Latest; + /// + public static readonly IConfiguration ProdConfig = InRegion.Default.Latest; + /// + public static readonly IConfiguration ProdLowLatencyConfig = InRegion.LowLatency.Latest; +} diff --git a/src/Momento.Sdk/Config/IConfiguration.cs b/src/Momento.Sdk/Config/IConfiguration.cs new file mode 100644 index 00000000..067ccd20 --- /dev/null +++ b/src/Momento.Sdk/Config/IConfiguration.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Momento.Sdk.Config.Middleware; +using Momento.Sdk.Config.Retry; +using Momento.Sdk.Config.Transport; + +namespace Momento.Sdk.Config; + + +/// +/// Contract for SDK configurables. A configuration must have a retry strategy, middlewares, and transport strategy. +/// +public interface IConfiguration +{ + public IRetryStrategy RetryStrategy { get; } + public IList Middlewares { get; } + public ITransportStrategy TransportStrategy { get; } +} diff --git a/src/Momento.Sdk/Config/Middleware/IMiddleware.cs b/src/Momento.Sdk/Config/Middleware/IMiddleware.cs new file mode 100644 index 00000000..8baa936f --- /dev/null +++ b/src/Momento.Sdk/Config/Middleware/IMiddleware.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Momento.Sdk.Config.Retry; + +namespace Momento.Sdk.Config.Middleware; + +/// +/// The Middleware interface allows the Configuration to provide a higher-order function that wraps all requests. +/// This allows future support for things like client-side metrics or other diagnostics helpers. +/// +public interface IMiddleware +{ + public delegate Task MiddlewareFn(IGrpcRequest request); + + // TODO: this should return another delegate, ie + // wrapRequest(middlewareFn) -> middlewareFn + /// + /// Called as a wrapper around each request; can be used to time the request and collect metrics etc. + /// + /// + /// + /// + public Task wrapRequest(MiddlewareFn middlewareFn, IGrpcRequest request); +} diff --git a/src/Momento.Sdk/Config/Middleware/PassThroughMiddleware.cs b/src/Momento.Sdk/Config/Middleware/PassThroughMiddleware.cs new file mode 100644 index 00000000..e301966c --- /dev/null +++ b/src/Momento.Sdk/Config/Middleware/PassThroughMiddleware.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using Momento.Sdk.Config.Retry; + +namespace Momento.Sdk.Config.Middleware; + +public class PassThroughMiddleware : IMiddleware +{ + public PassThroughMiddleware() + { + + } + + public async Task wrapRequest(IMiddleware.MiddlewareFn middlewareFn, IGrpcRequest request) + { + return await middlewareFn(request); + } +} diff --git a/src/Momento.Sdk/Config/Retry/FixedCountRetryStrategy.cs b/src/Momento.Sdk/Config/Retry/FixedCountRetryStrategy.cs new file mode 100644 index 00000000..fd083ebf --- /dev/null +++ b/src/Momento.Sdk/Config/Retry/FixedCountRetryStrategy.cs @@ -0,0 +1,20 @@ +namespace Momento.Sdk.Config.Retry; + +public class FixedCountRetryStrategy : IRetryStrategy +{ + public int MaxAttempts { get; private set; } + //FixedCountRetryStrategy(retryableStatusCodes = DEFAULT_RETRYABLE_STATUS_CODES, maxAttempts = 3), + public FixedCountRetryStrategy(int maxAttempts) + { + MaxAttempts = maxAttempts; + } + + public int? DetermineWhenToRetryRequest(IGrpcResponse grpcResponse, IGrpcRequest grpcRequest, int attemptNumber) + { + if (attemptNumber > MaxAttempts) + { + return null; + } + return 0; + } +} diff --git a/src/Momento.Sdk/Config/Retry/IGrpcRequest.cs b/src/Momento.Sdk/Config/Retry/IGrpcRequest.cs new file mode 100644 index 00000000..9f955c2e --- /dev/null +++ b/src/Momento.Sdk/Config/Retry/IGrpcRequest.cs @@ -0,0 +1,6 @@ +namespace Momento.Sdk.Config.Retry; + +public interface IGrpcRequest +{ + +} diff --git a/src/Momento.Sdk/Config/Retry/IGrpcResponse.cs b/src/Momento.Sdk/Config/Retry/IGrpcResponse.cs new file mode 100644 index 00000000..b070df77 --- /dev/null +++ b/src/Momento.Sdk/Config/Retry/IGrpcResponse.cs @@ -0,0 +1,6 @@ +namespace Momento.Sdk.Config.Retry; + +public interface IGrpcResponse +{ + +} diff --git a/src/Momento.Sdk/Config/Retry/IRetryStrategy.cs b/src/Momento.Sdk/Config/Retry/IRetryStrategy.cs new file mode 100644 index 00000000..3fc7b91a --- /dev/null +++ b/src/Momento.Sdk/Config/Retry/IRetryStrategy.cs @@ -0,0 +1,16 @@ +namespace Momento.Sdk.Config.Retry; + +/// +/// Defines a contract for how and when to retry a request +/// +public interface IRetryStrategy +{ + /// + /// Calculates whether or not to retry a request based on the type of request and number of attempts. + /// + /// + /// + /// + /// Returns number of milliseconds after which the request should be retried, or if the request should not be retried. + public int? DetermineWhenToRetryRequest(IGrpcResponse grpcResponse, IGrpcRequest grpcRequest, int attemptNumber); +} diff --git a/src/Momento.Sdk/Config/Transport/IGrpcConfiguration.cs b/src/Momento.Sdk/Config/Transport/IGrpcConfiguration.cs new file mode 100644 index 00000000..dc5b098f --- /dev/null +++ b/src/Momento.Sdk/Config/Transport/IGrpcConfiguration.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Momento.Sdk.Config.Transport; + +/// +/// Abstracts away the gRPC configuration tunables. +/// +/// and expose individual settings for gRPC channels. They are just here to ensure that +/// strategy implementations provide values for settings that we know to be important. These may vary by language +/// since the gRPC implementations in each language have subtly different behaviors. +/// +public interface IGrpcConfiguration +{ + public int NumChannels { get; } + + public int MaxSessionMemory { get; } + public bool UseLocalSubChannelPool { get; } + + /// + /// This is a dictionary that encapsulates the settings above, and may also include other channel-specific settings. + /// This allows strategy implementations to provide gRPC config key/value pairs for any available setting, even + /// if it's not one we've explicitly tried / recommended. The strategy implementation should implement this by + /// calling the functions above, along with allowing a mechanism for specifying additional key/value pairs. + /// + public IDictionary GrpcChannelConfig { get; } +} diff --git a/src/Momento.Sdk/Config/Transport/ITransportStrategy.cs b/src/Momento.Sdk/Config/Transport/ITransportStrategy.cs new file mode 100644 index 00000000..0e5dd898 --- /dev/null +++ b/src/Momento.Sdk/Config/Transport/ITransportStrategy.cs @@ -0,0 +1,10 @@ +namespace Momento.Sdk.Config.Transport; + +/// +/// This is responsible for configuring network tunables. +/// +public interface ITransportStrategy +{ + public int MaxConcurrentRequests { get; } + public IGrpcConfiguration GrpcConfig { get; } +} diff --git a/src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs b/src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs new file mode 100644 index 00000000..7408270f --- /dev/null +++ b/src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Momento.Sdk.Config.Transport; + + +public class StaticGrpcConfiguration : IGrpcConfiguration +{ + public int NumChannels { get; } + + public int MaxSessionMemory { get; } + public bool UseLocalSubChannelPool { get; } + public IDictionary GrpcChannelConfig { get; } + + public StaticGrpcConfiguration(int numChannels, int maxSessionMemory, bool useLocalSubChannelPool) + { + this.NumChannels = numChannels; + this.MaxSessionMemory = maxSessionMemory; + this.UseLocalSubChannelPool = useLocalSubChannelPool; + this.GrpcChannelConfig = new Dictionary(); + } +} + +public class StaticTransportStrategy : ITransportStrategy +{ + public int MaxConcurrentRequests { get; } + public IGrpcConfiguration GrpcConfig { get; } + + public StaticTransportStrategy(int maxConcurrentRequests, StaticGrpcConfiguration grpcConfig) + { + MaxConcurrentRequests = maxConcurrentRequests; + GrpcConfig = grpcConfig; + } +} diff --git a/src/Momento.Sdk/SimpleCacheClient.cs b/src/Momento.Sdk/SimpleCacheClient.cs index 7611b9a7..348a8ff3 100644 --- a/src/Momento.Sdk/SimpleCacheClient.cs +++ b/src/Momento.Sdk/SimpleCacheClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Momento.Sdk.Config; using Momento.Sdk.Exceptions; using Momento.Sdk.Internal; using Momento.Sdk.Responses; @@ -16,15 +17,18 @@ public class SimpleCacheClient : ISimpleCacheClient { private readonly ScsControlClient controlClient; private readonly ScsDataClient dataClient; + protected readonly IConfiguration config; /// /// Client to perform operations against the Simple Cache Service. /// + /// Configuration to use for the transport, retries, middlewares. See for out-of-the-box configuration choices, eg /// Momento JWT. /// Default time to live for the item in cache. /// Deadline (timeout) for communicating to the server. Defaults to 5 seconds. - public SimpleCacheClient(string authToken, uint defaultTtlSeconds, uint? dataClientOperationTimeoutMilliseconds = null) + public SimpleCacheClient(IConfiguration config, string authToken, uint defaultTtlSeconds, uint? dataClientOperationTimeoutMilliseconds = null) { + this.config = config; ValidateRequestTimeout(dataClientOperationTimeoutMilliseconds); Claims claims = JwtUtils.DecodeJwt(authToken); diff --git a/tests/Integration/Momento.Sdk.Tests/Fixtures.cs b/tests/Integration/Momento.Sdk.Tests/Fixtures.cs index 70da0504..64764895 100644 --- a/tests/Integration/Momento.Sdk.Tests/Fixtures.cs +++ b/tests/Integration/Momento.Sdk.Tests/Fixtures.cs @@ -1,3 +1,5 @@ +using Momento.Sdk.Config; + namespace Momento.Sdk.Tests; /// @@ -19,7 +21,7 @@ public SimpleCacheClientFixture() throw new NullReferenceException("TEST_AUTH_TOKEN environment variable must be set."); CacheName = Environment.GetEnvironmentVariable("TEST_CACHE_NAME") ?? throw new NullReferenceException("TEST_CACHE_NAME environment variable must be set."); - Client = new(AuthToken, defaultTtlSeconds: DefaultTtlSeconds); + Client = new(Configurations.Laptop.Latest, AuthToken, defaultTtlSeconds: DefaultTtlSeconds); try { diff --git a/tests/Integration/Momento.Sdk.Tests/SimpleCacheControlTest.cs b/tests/Integration/Momento.Sdk.Tests/SimpleCacheControlTest.cs index 77c88270..be14070b 100644 --- a/tests/Integration/Momento.Sdk.Tests/SimpleCacheControlTest.cs +++ b/tests/Integration/Momento.Sdk.Tests/SimpleCacheControlTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Momento.Sdk.Config; namespace Momento.Sdk.Tests; @@ -18,19 +19,19 @@ public SimpleCacheControlTest(SimpleCacheClientFixture fixture) [Fact] public void SimpleCacheClientConstructor_BadRequestTimeout_ThrowsException() { - Assert.Throws(() => new SimpleCacheClient(authToken, defaultTtlSeconds: 10, dataClientOperationTimeoutMilliseconds: 0)); + Assert.Throws(() => new SimpleCacheClient(Configurations.Laptop.Latest, authToken, defaultTtlSeconds: 10, dataClientOperationTimeoutMilliseconds: 0)); } [Fact] public void SimpleCacheClientConstructor_BadJWT_InvalidJwtException() { - Assert.Throws(() => new SimpleCacheClient("eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbiJ9.ZOgkTs", defaultTtlSeconds: 10)); + Assert.Throws(() => new SimpleCacheClient(Configurations.Laptop.Latest, "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbiJ9.ZOgkTs", defaultTtlSeconds: 10)); } [Fact] public void SimpleCacheClientConstructor_NullJWT_InvalidJwtException() { - Assert.Throws(() => new SimpleCacheClient(null!, defaultTtlSeconds: 10)); + Assert.Throws(() => new SimpleCacheClient(Configurations.Laptop.Latest, null!, defaultTtlSeconds: 10)); } [Fact] diff --git a/tests/Integration/Momento.Sdk.Tests/SimpleCacheDataTest.cs b/tests/Integration/Momento.Sdk.Tests/SimpleCacheDataTest.cs index 9813c729..ac554ce2 100644 --- a/tests/Integration/Momento.Sdk.Tests/SimpleCacheDataTest.cs +++ b/tests/Integration/Momento.Sdk.Tests/SimpleCacheDataTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Momento.Sdk.Config; namespace Momento.Sdk.Tests; @@ -177,7 +178,7 @@ public async Task GetBatchAsync_KeysAreString_HappyPath() public async Task GetBatchAsync_Failure() { // Set very small timeout for dataClientOperationTimeoutMilliseconds - using SimpleCacheClient simpleCacheClient = new SimpleCacheClient(authToken, DefaultTtlSeconds, 1); + using SimpleCacheClient simpleCacheClient = new SimpleCacheClient(Configurations.Laptop.Latest, authToken, DefaultTtlSeconds, 1); List keys = new() { Utils.NewGuidString(), Utils.NewGuidString(), Utils.NewGuidString(), Utils.NewGuidString() }; await Assert.ThrowsAsync(async () => await simpleCacheClient.GetBatchAsync(cacheName, keys)); }