Skip to content

Commit

Permalink
feat: SDK configurables (#170)
Browse files Browse the repository at this point in the history
This adds the configurables data structures the project, as specified here. Concrete implementations and integration will happen in followup PRs.
  • Loading branch information
malandis authored Sep 22, 2022
1 parent d757c1b commit b033154
Show file tree
Hide file tree
Showing 17 changed files with 316 additions and 8 deletions.
3 changes: 1 addition & 2 deletions examples/MomentoApplication/MomentoApplication.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
</PropertyGroup>

<ItemGroup>
<!-- <PackageReference Include="Momento.Sdk" Version="0.26.0" /> -->
<ProjectReference Include="../../src/Momento.Sdk/Momento.Sdk.csproj" />
<PackageReference Include="Momento.Sdk" Version="0.26.0" />
</ItemGroup>
</Project>
26 changes: 26 additions & 0 deletions src/Momento.Sdk/Config/Configuration.cs
Original file line number Diff line number Diff line change
@@ -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<IMiddleware> Middlewares { get; }
public ITransportStrategy TransportStrategy { get; }

public Configuration(IRetryStrategy retryStrategy, ITransportStrategy transportStrategy)
: this(retryStrategy, new List<IMiddleware>(), transportStrategy)
{

}

public Configuration(IRetryStrategy retryStrategy, IList<IMiddleware> middlewares, ITransportStrategy transportStrategy)
{
this.RetryStrategy = retryStrategy;
this.Middlewares = middlewares;
this.TransportStrategy = transportStrategy;
}
}
101 changes: 101 additions & 0 deletions src/Momento.Sdk/Config/Configurations.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Laptop config provides defaults suitable for a medium-to-high-latency dev environment. Permissive timeouts, retries, potentially
/// a higher number of connections, etc.
/// </summary>
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);
}
}
}

/// <summary>
/// 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.
/// </summary>
public class InRegion
{
/// <summary>
/// This config prioritizes throughput and client resource utilization.
/// </summary>
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);
}
}
}

/// <summary>
/// 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.
/// </summary>
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);
}
}
}
}

/// <inheritDoc cref="Laptop" />
public static readonly IConfiguration DevConfig = Laptop.Latest;
/// <inheritDoc cref="InRegion.Default" />
public static readonly IConfiguration ProdConfig = InRegion.Default.Latest;
/// <inheritDoc cref="InRegion.LowLatency" />
public static readonly IConfiguration ProdLowLatencyConfig = InRegion.LowLatency.Latest;
}
17 changes: 17 additions & 0 deletions src/Momento.Sdk/Config/IConfiguration.cs
Original file line number Diff line number Diff line change
@@ -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;


/// <summary>
/// Contract for SDK configurables. A configuration must have a retry strategy, middlewares, and transport strategy.
/// </summary>
public interface IConfiguration
{
public IRetryStrategy RetryStrategy { get; }
public IList<IMiddleware> Middlewares { get; }
public ITransportStrategy TransportStrategy { get; }
}
23 changes: 23 additions & 0 deletions src/Momento.Sdk/Config/Middleware/IMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Threading.Tasks;
using Momento.Sdk.Config.Retry;

namespace Momento.Sdk.Config.Middleware;

/// <summary>
/// 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.
/// </summary>
public interface IMiddleware
{
public delegate Task<IGrpcResponse> MiddlewareFn(IGrpcRequest request);

// TODO: this should return another delegate, ie
// wrapRequest(middlewareFn) -> middlewareFn
/// <summary>
/// Called as a wrapper around each request; can be used to time the request and collect metrics etc.
/// </summary>
/// <param name="middlewareFn"></param>
/// <param name="request"></param>
/// <returns></returns>
public Task<IGrpcResponse> wrapRequest(MiddlewareFn middlewareFn, IGrpcRequest request);
}
17 changes: 17 additions & 0 deletions src/Momento.Sdk/Config/Middleware/PassThroughMiddleware.cs
Original file line number Diff line number Diff line change
@@ -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<IGrpcResponse> wrapRequest(IMiddleware.MiddlewareFn middlewareFn, IGrpcRequest request)
{
return await middlewareFn(request);
}
}
20 changes: 20 additions & 0 deletions src/Momento.Sdk/Config/Retry/FixedCountRetryStrategy.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 6 additions & 0 deletions src/Momento.Sdk/Config/Retry/IGrpcRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Momento.Sdk.Config.Retry;

public interface IGrpcRequest
{

}
6 changes: 6 additions & 0 deletions src/Momento.Sdk/Config/Retry/IGrpcResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Momento.Sdk.Config.Retry;

public interface IGrpcResponse
{

}
16 changes: 16 additions & 0 deletions src/Momento.Sdk/Config/Retry/IRetryStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Momento.Sdk.Config.Retry;

/// <summary>
/// Defines a contract for how and when to retry a request
/// </summary>
public interface IRetryStrategy
{
/// <summary>
/// Calculates whether or not to retry a request based on the type of request and number of attempts.
/// </summary>
/// <param name="grpcResponse"></param>
/// <param name="grpcRequest"></param>
/// <param name="attemptNumber"></param>
/// <returns>Returns number of milliseconds after which the request should be retried, or <see langword="null"/> if the request should not be retried.</returns>
public int? DetermineWhenToRetryRequest(IGrpcResponse grpcResponse, IGrpcRequest grpcRequest, int attemptNumber);
}
26 changes: 26 additions & 0 deletions src/Momento.Sdk/Config/Transport/IGrpcConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;

namespace Momento.Sdk.Config.Transport;

/// <summary>
/// Abstracts away the gRPC configuration tunables.
///
/// <see cref="IGrpcConfiguration.MaxSessionMemory" /> and <see cref="IGrpcConfiguration.UseLocalSubChannelPool" /> 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.
/// </summary>
public interface IGrpcConfiguration
{
public int NumChannels { get; }

public int MaxSessionMemory { get; }
public bool UseLocalSubChannelPool { get; }

/// <summary>
/// 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.
/// </summary>
public IDictionary<string, string> GrpcChannelConfig { get; }
}
10 changes: 10 additions & 0 deletions src/Momento.Sdk/Config/Transport/ITransportStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Momento.Sdk.Config.Transport;

/// <summary>
/// This is responsible for configuring network tunables.
/// </summary>
public interface ITransportStrategy
{
public int MaxConcurrentRequests { get; }
public IGrpcConfiguration GrpcConfig { get; }
}
33 changes: 33 additions & 0 deletions src/Momento.Sdk/Config/Transport/StaticTransportStrategy.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> GrpcChannelConfig { get; }

public StaticGrpcConfiguration(int numChannels, int maxSessionMemory, bool useLocalSubChannelPool)
{
this.NumChannels = numChannels;
this.MaxSessionMemory = maxSessionMemory;
this.UseLocalSubChannelPool = useLocalSubChannelPool;
this.GrpcChannelConfig = new Dictionary<string, string>();
}
}

public class StaticTransportStrategy : ITransportStrategy
{
public int MaxConcurrentRequests { get; }
public IGrpcConfiguration GrpcConfig { get; }

public StaticTransportStrategy(int maxConcurrentRequests, StaticGrpcConfiguration grpcConfig)
{
MaxConcurrentRequests = maxConcurrentRequests;
GrpcConfig = grpcConfig;
}
}
6 changes: 5 additions & 1 deletion src/Momento.Sdk/SimpleCacheClient.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,15 +17,18 @@ public class SimpleCacheClient : ISimpleCacheClient
{
private readonly ScsControlClient controlClient;
private readonly ScsDataClient dataClient;
protected readonly IConfiguration config;

/// <summary>
/// Client to perform operations against the Simple Cache Service.
/// </summary>
/// <param name="config">Configuration to use for the transport, retries, middlewares. See <see cref="Configurations"/> for out-of-the-box configuration choices, eg <see cref="Configurations.Laptop.Latest"/></param>
/// <param name="authToken">Momento JWT.</param>
/// <param name="defaultTtlSeconds">Default time to live for the item in cache.</param>
/// <param name="dataClientOperationTimeoutMilliseconds">Deadline (timeout) for communicating to the server. Defaults to 5 seconds.</param>
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);

Expand Down
4 changes: 3 additions & 1 deletion tests/Integration/Momento.Sdk.Tests/Fixtures.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Momento.Sdk.Config;

namespace Momento.Sdk.Tests;

/// <summary>
Expand All @@ -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
{
Expand Down
7 changes: 4 additions & 3 deletions tests/Integration/Momento.Sdk.Tests/SimpleCacheControlTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Momento.Sdk.Config;

namespace Momento.Sdk.Tests;

Expand All @@ -18,19 +19,19 @@ public SimpleCacheControlTest(SimpleCacheClientFixture fixture)
[Fact]
public void SimpleCacheClientConstructor_BadRequestTimeout_ThrowsException()
{
Assert.Throws<InvalidArgumentException>(() => new SimpleCacheClient(authToken, defaultTtlSeconds: 10, dataClientOperationTimeoutMilliseconds: 0));
Assert.Throws<InvalidArgumentException>(() => new SimpleCacheClient(Configurations.Laptop.Latest, authToken, defaultTtlSeconds: 10, dataClientOperationTimeoutMilliseconds: 0));
}

[Fact]
public void SimpleCacheClientConstructor_BadJWT_InvalidJwtException()
{
Assert.Throws<InvalidArgumentException>(() => new SimpleCacheClient("eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbiJ9.ZOgkTs", defaultTtlSeconds: 10));
Assert.Throws<InvalidArgumentException>(() => new SimpleCacheClient(Configurations.Laptop.Latest, "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbiJ9.ZOgkTs", defaultTtlSeconds: 10));
}

[Fact]
public void SimpleCacheClientConstructor_NullJWT_InvalidJwtException()
{
Assert.Throws<InvalidArgumentException>(() => new SimpleCacheClient(null!, defaultTtlSeconds: 10));
Assert.Throws<InvalidArgumentException>(() => new SimpleCacheClient(Configurations.Laptop.Latest, null!, defaultTtlSeconds: 10));
}

[Fact]
Expand Down
Loading

0 comments on commit b033154

Please sign in to comment.