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);
}