diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1cd858b8..fc4118ab 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -45,7 +45,7 @@ jobs: run: make GRPC_WEB=${{ matrix.grpc-web }} build - name: Test - run: make test + run: make prod-test build_examples: runs-on: ubuntu-latest diff --git a/.github/workflows/on-push-to-main-branch.yaml b/.github/workflows/on-push-to-main-branch.yaml index 5f912c6d..41ab7404 100644 --- a/.github/workflows/on-push-to-main-branch.yaml +++ b/.github/workflows/on-push-to-main-branch.yaml @@ -32,7 +32,7 @@ jobs: run: make GRPC_WEB=${{ matrix.grpc-web }} build - name: Test - run: make test + run: make prod-test generate_readme: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 1fb3fa66..809b10b6 100644 --- a/Makefile +++ b/Makefile @@ -33,13 +33,11 @@ ifneq (,$(findstring NT,$(OS))) BUILD_TARGETS := build-dotnet6 build-dotnet-framework TEST_TARGETS := test-dotnet6 test-dotnet-framework TEST_TARGETS_AUTH_SERVICE := test-dotnet6-auth-service test-dotnet-framework-auth-service - TEST_TARGETS_CACHE_SERVICE := test-dotnet6-cache-service test-dotnet-framework-cache-service TEST_TARGETS_TOPICS_SERVICE := test-dotnet6-topics-service test-dotnet-framework-topics-service else BUILD_TARGETS := build-dotnet6 TEST_TARGETS := test-dotnet6 TEST_TARGETS_AUTH_SERVICE := test-dotnet6-auth-service - TEST_TARGETS_CACHE_SERVICE := test-dotnet6-cache-service TEST_TARGETS_TOPICS_SERVICE := test-dotnet6-topics-service endif @@ -98,6 +96,16 @@ restore: test: ${TEST_TARGETS} +## Run unit and integration tests with consistent reads (conditioned by OS) +prod-test: + @echo "running tests with consistent reads..." +ifeq (,$(findstring NT,$(OS))) + @CONSISTENT_READS=1 $(MAKE) ${TEST_TARGETS} +else + @set CONSISTENT_READS=1 && $(MAKE) ${TEST_TARGETS} +endif + + ## Run unit and integration tests on the .NET 6.0 runtime test-dotnet6: @echo "Running unit and integration tests on the .NET 6.0 runtime..." @@ -151,7 +159,12 @@ test-auth-service: ${TEST_TARGETS_AUTH_SERVICE} ## Run cache service tests -test-cache-service: ${TEST_TARGETS_CACHE_SERVICE} +test-cache-service: +ifeq (,$(findstring NT,$(OS))) + @CONSISTENT_READS=1 $(MAKE) test-dotnet6-cache-service +else + @set CONSISTENT_READS=1 && $(MAKE) test-dotnet6-cache-service test-dotnet-framework-cache-service +endif ## Run leaderboard service tests diff --git a/src/Momento.Sdk/Config/Configuration.cs b/src/Momento.Sdk/Config/Configuration.cs index 407e0a37..875c4c04 100644 --- a/src/Momento.Sdk/Config/Configuration.cs +++ b/src/Momento.Sdk/Config/Configuration.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Momento.Sdk.Config.Middleware; using Momento.Sdk.Config.Retry; using Momento.Sdk.Config.Transport; @@ -14,18 +13,41 @@ public class Configuration : IConfiguration { /// public ILoggerFactory LoggerFactory { get; } + /// public IRetryStrategy RetryStrategy { get; } + /// public IList Middlewares { get; } + /// public ITransportStrategy TransportStrategy { get; } + /// + public ReadConcern ReadConcern { get; } + /// - public Configuration(ILoggerFactory loggerFactory, IRetryStrategy retryStrategy, ITransportStrategy transportStrategy) + public Configuration(ILoggerFactory loggerFactory, IRetryStrategy retryStrategy, + ITransportStrategy transportStrategy) : this(loggerFactory, retryStrategy, new List(), transportStrategy) { + } + /// + /// Create a new instance of Configuration object with provided arguments: , , , and + /// + /// Defines a contract for how and when to retry a request + /// The Middleware interface allows the Configuration to provide a higher-order function that wraps all requests. + /// This is responsible for configuring network tunables. + /// This is responsible for configuring logging. + public Configuration(ILoggerFactory loggerFactory, IRetryStrategy retryStrategy, IList middlewares, + ITransportStrategy transportStrategy) + { + LoggerFactory = loggerFactory; + RetryStrategy = retryStrategy; + Middlewares = middlewares; + TransportStrategy = transportStrategy; + ReadConcern = ReadConcern.Balanced; } /// @@ -34,31 +56,34 @@ public Configuration(ILoggerFactory loggerFactory, IRetryStrategy retryStrategy, /// Defines a contract for how and when to retry a request /// The Middleware interface allows the Configuration to provide a higher-order function that wraps all requests. /// This is responsible for configuring network tunables. - /// This is responsible for configuraing logging. - public Configuration(ILoggerFactory loggerFactory, IRetryStrategy retryStrategy, IList middlewares, ITransportStrategy transportStrategy) + /// This is responsible for configuring logging. + /// The client-wide setting for read-after-write consistency. + public Configuration(ILoggerFactory loggerFactory, IRetryStrategy retryStrategy, IList middlewares, + ITransportStrategy transportStrategy, ReadConcern readConcern) { - this.LoggerFactory = loggerFactory; - this.RetryStrategy = retryStrategy; - this.Middlewares = middlewares; - this.TransportStrategy = transportStrategy; + LoggerFactory = loggerFactory; + RetryStrategy = retryStrategy; + Middlewares = middlewares; + TransportStrategy = transportStrategy; + ReadConcern = readConcern; } /// public IConfiguration WithRetryStrategy(IRetryStrategy retryStrategy) { - return new Configuration(LoggerFactory, retryStrategy, Middlewares, TransportStrategy); + return new Configuration(LoggerFactory, retryStrategy, Middlewares, TransportStrategy, ReadConcern); } /// public IConfiguration WithMiddlewares(IList middlewares) { - return new Configuration(LoggerFactory, RetryStrategy, middlewares, TransportStrategy); + return new Configuration(LoggerFactory, RetryStrategy, middlewares, TransportStrategy, ReadConcern); } /// public IConfiguration WithTransportStrategy(ITransportStrategy transportStrategy) { - return new Configuration(LoggerFactory, RetryStrategy, Middlewares, transportStrategy); + return new Configuration(LoggerFactory, RetryStrategy, Middlewares, transportStrategy, ReadConcern); } /// @@ -72,12 +97,13 @@ public Configuration WithAdditionalMiddlewares(IList additionalMidd retryStrategy: RetryStrategy, middlewares: Middlewares.Concat(additionalMiddlewares).ToList(), transportStrategy: TransportStrategy, - loggerFactory: LoggerFactory + loggerFactory: LoggerFactory, + readConcern: ReadConcern ); } /// - /// Add the specified client timeout to an existing instance of Configuration object as an addiion to the existing transport strategy. + /// Add the specified client timeout to an existing instance of Configuration object as an addition to the existing transport strategy. /// /// The amount of time to wait before cancelling the request. /// Configuration object with client timeout provided @@ -87,7 +113,20 @@ public Configuration WithClientTimeout(TimeSpan clientTimeout) retryStrategy: RetryStrategy, middlewares: Middlewares, transportStrategy: TransportStrategy.WithClientTimeout(clientTimeout), - loggerFactory: LoggerFactory + loggerFactory: LoggerFactory, + readConcern: ReadConcern + ); + } + + /// + public IConfiguration WithReadConcern(ReadConcern readConcern) + { + return new Configuration( + retryStrategy: RetryStrategy, + middlewares: Middlewares, + transportStrategy: TransportStrategy, + loggerFactory: LoggerFactory, + readConcern: readConcern ); } @@ -106,9 +145,10 @@ public override bool Equals(object obj) var other = (Configuration)obj; return RetryStrategy.Equals(other.RetryStrategy) && - Middlewares.SequenceEqual(other.Middlewares) && - TransportStrategy.Equals(other.TransportStrategy) && - LoggerFactory.Equals(other.LoggerFactory); + Middlewares.SequenceEqual(other.Middlewares) && + TransportStrategy.Equals(other.TransportStrategy) && + LoggerFactory.Equals(other.LoggerFactory) && + ReadConcern.Equals(other.ReadConcern); } /// @@ -116,4 +156,4 @@ public override int GetHashCode() { return base.GetHashCode(); } -} +} \ No newline at end of file diff --git a/src/Momento.Sdk/Config/IConfiguration.cs b/src/Momento.Sdk/Config/IConfiguration.cs index e1b7998e..2f8c07b0 100644 --- a/src/Momento.Sdk/Config/IConfiguration.cs +++ b/src/Momento.Sdk/Config/IConfiguration.cs @@ -21,6 +21,8 @@ public interface IConfiguration public IList Middlewares { get; } /// public ITransportStrategy TransportStrategy { get; } + /// + public ReadConcern ReadConcern { get; } /// /// Creates a new instance of the Configuration object, updated to use the specified retry strategy. @@ -49,4 +51,11 @@ public interface IConfiguration /// The amount of time to wait before cancelling the request. /// Configuration object with custom client timeout provided public IConfiguration WithClientTimeout(TimeSpan clientTimeout); + + /// + /// Creates a new instance of the Configuration object, updated to use the specified read concern. + /// + /// The read concern setting. + /// Configuration object with custom read concern provided + public IConfiguration WithReadConcern(ReadConcern readConcern); } diff --git a/src/Momento.Sdk/Config/ReadConcern.cs b/src/Momento.Sdk/Config/ReadConcern.cs new file mode 100644 index 00000000..5e9d2d0f --- /dev/null +++ b/src/Momento.Sdk/Config/ReadConcern.cs @@ -0,0 +1,41 @@ +using System; + +namespace Momento.Sdk.Config; + +/// +/// The read consistency setting for the cache client. Consistent guarantees read after write consistency, but applies a +/// 6x multiplier to your operation usage. +/// +public enum ReadConcern +{ + /// + /// Balanced is the default read concern for the cache client. + /// + Balanced, + /// + /// Consistent read concern guarantees read after write consistency. + /// + Consistent +} + +/// +/// Extension methods for the ReadConcern enum. +/// +public static class ReadConcernExtensions +{ + /// + /// Converts the read concern to a string value. + /// + /// to convert to a string + /// + /// if given an unknown read concern + public static string ToStringValue(this ReadConcern readConcern) + { + return readConcern switch + { + ReadConcern.Balanced => "balanced", + ReadConcern.Consistent => "consistent", + _ => throw new ArgumentOutOfRangeException() + }; + } +} \ No newline at end of file diff --git a/src/Momento.Sdk/Internal/DataGrpcManager.cs b/src/Momento.Sdk/Internal/DataGrpcManager.cs index eafc610a..73242ec4 100644 --- a/src/Momento.Sdk/Internal/DataGrpcManager.cs +++ b/src/Momento.Sdk/Internal/DataGrpcManager.cs @@ -4,11 +4,6 @@ using System.Linq; using System.Threading.Tasks; using Grpc.Core; -using Grpc.Net.Client; -using System.Net.Http; -#if USE_GRPC_WEB -using Grpc.Net.Client.Web; -#endif using Microsoft.Extensions.Logging; using Momento.Protos.CacheClient; using Momento.Protos.CachePing; @@ -17,7 +12,6 @@ using Momento.Sdk.Config.Retry; using Momento.Sdk.Exceptions; using Momento.Sdk.Internal.Middleware; -using static System.Reflection.Assembly; namespace Momento.Sdk.Internal; @@ -256,10 +250,13 @@ public class DataGrpcManager : GrpcManager internal DataGrpcManager(IConfiguration config, string authToken, string endpoint): base(config.TransportStrategy.GrpcConfig, config.LoggerFactory, authToken, endpoint, "DataGrpcManager") { + var readConcernHeader = new Header(Header.ReadConcern, config.ReadConcern.ToStringValue()); + headers.Add(readConcernHeader); + var middlewares = config.Middlewares.Concat( new List { new RetryMiddleware(config.LoggerFactory, config.RetryStrategy), - new HeaderMiddleware(config.LoggerFactory, this.headers), + new HeaderMiddleware(config.LoggerFactory, headers), new MaxConcurrentRequestsMiddleware(config.LoggerFactory, config.TransportStrategy.MaxConcurrentRequests) } ).ToList(); @@ -280,7 +277,7 @@ await pingClient.PingAsync(new _PingRequest(), catch (RpcException ex) { MomentoErrorTransportDetails transportDetails = new MomentoErrorTransportDetails( - new MomentoGrpcErrorDetails(ex.StatusCode, ex.Message, null) + new MomentoGrpcErrorDetails(ex.StatusCode, ex.Message) ); throw new ConnectionException("Eager connection to server failed", transportDetails, ex); } diff --git a/src/Momento.Sdk/Internal/Middleware/HeaderMiddleware.cs b/src/Momento.Sdk/Internal/Middleware/HeaderMiddleware.cs index cb0aa8ee..03a074b0 100644 --- a/src/Momento.Sdk/Internal/Middleware/HeaderMiddleware.cs +++ b/src/Momento.Sdk/Internal/Middleware/HeaderMiddleware.cs @@ -5,6 +5,7 @@ using Grpc.Core; using Grpc.Core.Interceptors; using Microsoft.Extensions.Logging; +using Momento.Sdk.Config; using Momento.Sdk.Config.Middleware; namespace Momento.Sdk.Internal.Middleware @@ -14,6 +15,7 @@ internal class Header public const string AuthorizationKey = "authorization"; public const string AgentKey = "agent"; public const string RuntimeVersionKey = "runtime-version"; + public const string ReadConcern = "read-concern"; public readonly List onceOnlyHeaders = new List { Header.AgentKey, Header.RuntimeVersionKey }; public string Name; public string Value; diff --git a/tests/Integration/Momento.Sdk.Tests/Fixtures.cs b/tests/Integration/Momento.Sdk.Tests/Fixtures.cs index 4b93565e..e541a724 100644 --- a/tests/Integration/Momento.Sdk.Tests/Fixtures.cs +++ b/tests/Integration/Momento.Sdk.Tests/Fixtures.cs @@ -20,19 +20,29 @@ public class CacheClientFixture : IDisposable public CacheClientFixture() { AuthProvider = new EnvMomentoTokenProvider("MOMENTO_API_KEY"); + + // Enable consistent reads if the CONSISTENT_READS env var is set to anything + var consistentReads = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CONSISTENT_READS")); + + var config = Configurations.Laptop.Latest(LoggerFactory.Create(builder => + { + builder.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + }); + builder.AddFilter("Grpc.Net.Client", LogLevel.Error); + builder.SetMinimumLevel(LogLevel.Information); + })); + + if (consistentReads) + { + config = config.WithReadConcern(ReadConcern.Consistent); + } + CacheName = $"dotnet-integration-{Utils.NewGuidString()}"; - Client = new TestCacheClient(Configurations.Laptop.Latest(LoggerFactory.Create(builder => - { - builder.AddSimpleConsole(options => - { - options.IncludeScopes = true; - options.SingleLine = true; - options.TimestampFormat = "hh:mm:ss "; - }); - builder.AddFilter("Grpc.Net.Client", LogLevel.Error); - builder.SetMinimumLevel(LogLevel.Information); - })), - AuthProvider, defaultTtl: DefaultTtl); + Client = new TestCacheClient(config, AuthProvider, defaultTtl: DefaultTtl); Utils.CreateCacheForTest(Client, CacheName); }