Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: async factory function for eager connections #535

Merged
merged 1 commit into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/Momento.Sdk/CacheClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ private ScsDataClient DataClient
protected readonly IConfiguration config;
/// <inheritdoc cref="Microsoft.Extensions.Logging.ILogger" />
protected readonly ILogger _logger;

/// <summary>
/// Async factory function to construct a Momento CacheClient with an eager connection to the
/// Momento server. Calling the CacheClient constructor directly will not establish a connection
/// immediately, but instead establish it lazily when the first request is issued. This factory
/// function will ensure that the connection is established before the first request.
/// </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="authProvider">Momento auth provider.</param>
/// <param name="defaultTtl">Default time to live for the item in cache.</param>
/// <param name="eagerConnectionTimeout">Maximum time to wait for eager connection.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="defaultTtl"/> is zero or negative.</exception>
/// <exception cref="ConnectionException">The eager connection could not be established within the specified <paramref name="eagerConnectionTimeout"/></exception>
public static async Task<ICacheClient> CreateAsync(IConfiguration config, ICredentialProvider authProvider, TimeSpan defaultTtl, TimeSpan eagerConnectionTimeout)
{
CacheClient cacheClient = new CacheClient(config, authProvider, defaultTtl);
await cacheClient.DataClient.EagerConnectAsync(eagerConnectionTimeout);
return cacheClient;
}


/// <summary>
Expand Down
6 changes: 1 addition & 5 deletions src/Momento.Sdk/Config/Configurations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,7 @@ private Lambda(ILoggerFactory loggerFactory, IRetryStrategy retryStrategy, ITran
/// <returns></returns>
public static IConfiguration Latest(ILoggerFactory? loggerFactory = null)
{
var config = Default.V1(loggerFactory);
var transportStrategy = config.TransportStrategy.WithEagerConnectionTimeout(
TimeSpan.FromSeconds(30)
);
return config.WithTransportStrategy(transportStrategy);
return Default.V1(loggerFactory);
}
}
}
Expand Down
19 changes: 0 additions & 19 deletions src/Momento.Sdk/Config/Transport/ITransportStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ public interface ITransportStrategy
/// </summary>
public int MaxConcurrentRequests { get; }

/// <summary>
/// If null, the client will only attempt to connect to the server lazily when the first request is executed.
/// If provided, the client will attempt to connect to the server immediately upon construction; if the connection
/// cannot be established within the specified TimeSpan, it will abort the connection attempt, log a warning,
/// and proceed with execution so that the application doesn't hang.
/// </summary>
public TimeSpan? EagerConnectionTimeout { get; }

/// <summary>
/// Configures the low-level gRPC settings for the Momento client's communication
/// with the Momento server.
Expand Down Expand Up @@ -50,15 +42,4 @@ public interface ITransportStrategy
/// <param name="clientTimeout"></param>
/// <returns>A new ITransportStrategy with the specified client timeout</returns>
public ITransportStrategy WithClientTimeout(TimeSpan clientTimeout);

/// <summary>
/// Copy constructor to enable eager connection to the server
/// </summary>
/// <param name="connectionTimeout">A timeout for attempting an eager connection to the server. When the client
/// is constructed, it will attempt to connect to the server immediately. If the connection
/// cannot be established within the specified TimeSpan, it will abort the connection attempt, log a warning,
/// and proceed with execution so that the application doesn't hang.
/// </param>
/// <returns>A new ITransportStrategy configured to eagerly connect to the server upon construction</returns>
public ITransportStrategy WithEagerConnectionTimeout(TimeSpan connectionTimeout);
}
15 changes: 15 additions & 0 deletions src/Momento.Sdk/Exceptions/ConnectionException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Momento.Sdk.Exceptions;

using System;

/// <summary>
/// Unable to connect to the server.
/// </summary>
public class ConnectionException : SdkException
{
/// <include file="../docs.xml" path='docs/class[@name="SdkException"]/constructor/*' />
public ConnectionException(string message, MomentoErrorTransportDetails transportDetails, Exception? e = null) : base(MomentoErrorCode.CONNECTION_ERROR, message, transportDetails, e)
{
this.MessageWrapper = "Unable to connect to the server; consider retrying. If the error persists, please contact us at [email protected]";
}
}
4 changes: 4 additions & 0 deletions src/Momento.Sdk/Exceptions/SdkException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ public enum MomentoErrorCode
/// </summary>
FAILED_PRECONDITION_ERROR,
/// <summary>
/// Unable to connect to the server
/// </summary>
CONNECTION_ERROR,
/// <summary>
/// Unknown error has occurred
/// </summary>
UNKNOWN_ERROR
Expand Down
34 changes: 18 additions & 16 deletions src/Momento.Sdk/Internal/DataGrpcManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Momento.Sdk.Config;
using Momento.Sdk.Config.Middleware;
using Momento.Sdk.Config.Retry;
using Momento.Sdk.Exceptions;
using Momento.Sdk.Internal.Middleware;
using static System.Reflection.Assembly;

Expand Down Expand Up @@ -299,24 +300,25 @@ internal DataGrpcManager(IConfiguration config, string authToken, string endpoin
).ToList();

var client = new Scs.ScsClient(invoker);

if (config.TransportStrategy.EagerConnectionTimeout != null)
Client = new DataClientWithMiddleware(client, middlewares);
}

internal async Task EagerConnectAsync(TimeSpan eagerConnectionTimeout)
{
_logger.LogDebug("Attempting eager connection to server");
var pingClient = new Ping.PingClient(this.channel);
try
{
TimeSpan eagerConnectionTimeout = config.TransportStrategy.EagerConnectionTimeout.Value;
_logger.LogDebug("TransportStrategy EagerConnection is enabled; attempting to connect to server");
var pingClient = new Ping.PingClient(this.channel);
try
{
pingClient.Ping(new _PingRequest(),
new CallOptions(deadline: DateTime.UtcNow.Add(eagerConnectionTimeout)));
}
catch (RpcException ex)
{
_logger.LogWarning($"Failed to eagerly connect to the server; continuing with execution in case failure is recoverable later: {ex}");
}
await pingClient.PingAsync(new _PingRequest(),
new CallOptions(deadline: DateTime.UtcNow.Add(eagerConnectionTimeout)));
}
catch (RpcException ex)
{
MomentoErrorTransportDetails transportDetails = new MomentoErrorTransportDetails(
new MomentoGrpcErrorDetails(ex.StatusCode, ex.Message, null)
);
throw new ConnectionException("Eager connection to server failed", transportDetails, ex);
}

Client = new DataClientWithMiddleware(client, middlewares);
}

public void Dispose()
Expand Down
5 changes: 5 additions & 0 deletions src/Momento.Sdk/Internal/ScsDataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public ScsDataClientBase(IConfiguration config, string authToken, string endpoin
this._logger = config.LoggerFactory.CreateLogger<ScsDataClient>();
this._exceptionMapper = new CacheExceptionMapper(config.LoggerFactory);
}

internal Task EagerConnectAsync(TimeSpan eagerConnectionTimeout)
{
return this.grpcManager.EagerConnectAsync(eagerConnectionTimeout);
}

protected Metadata MetadataWithCache(string cacheName)
{
Expand Down
16 changes: 3 additions & 13 deletions tests/Integration/Momento.Sdk.Tests/CacheEagerConnectionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,6 @@ public void CacheClientConstructor_LazyConnection()
var client = new CacheClient(config, authProvider, defaultTtl);
}

[Fact]
public void CacheClientConstructor_EagerConnection_Success()
{
var config = Configurations.Laptop.Latest(loggerFactory);
config = config.WithTransportStrategy(config.TransportStrategy.WithEagerConnectionTimeout(TimeSpan.FromSeconds(5)));
// just validating that we can construct the client when the eager connection is successful
var client = new CacheClient(config, authProvider, defaultTtl);
}

[Fact]
public void CacheClientConstructor_WithChannelsAndMaxConn_Success()
{
Expand All @@ -56,13 +47,12 @@ public void CacheClientConstructor_WithChannelsAndMaxConn_Success()
}

[Fact]
public void CacheClientConstructor_EagerConnection_BadEndpoint()
public async void CacheClientCreate_EagerConnection_BadEndpoint()
{
var config = Configurations.Laptop.Latest(loggerFactory);
config = config.WithTransportStrategy(config.TransportStrategy.WithEagerConnectionTimeout(TimeSpan.FromSeconds(2)));
var authProviderWithBadCacheEndpoint = authProvider.WithCacheEndpoint("cache.cell-external-beta-1.prod.a.momentohq.com:65000");
Console.WriteLine($"Hello developer! We are about to run a test that verifies that the cache client is still operational even if our eager connection (ping) fails. So you will see the test log a warning message about that. It's expected, don't worry!");
// validating that the constructor doesn't fail when the eager connection fails
var client = new CacheClient(config, authProviderWithBadCacheEndpoint, defaultTtl);

await Assert.ThrowsAsync<ConnectionException>(async () => await CacheClient.CreateAsync(config, authProviderWithBadCacheEndpoint, defaultTtl, TimeSpan.FromSeconds(2)));
}
}
7 changes: 0 additions & 7 deletions tests/Unit/Momento.Sdk.Tests/ConfigTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,4 @@ public void V1VConfigs_EqualLatest_HappyPath()
Assert.Equal(Configurations.InRegion.Default.Latest(), Configurations.InRegion.Default.V1());
Assert.Equal(Configurations.InRegion.LowLatency.Latest(), Configurations.InRegion.LowLatency.V1());
}

[Fact]
public void LambdaLatest_HasEagerConnectionTimeout_HappyPath()
{
var config = Configurations.InRegion.Lambda.Latest();
Assert.Equal(TimeSpan.FromSeconds(30), config.TransportStrategy.EagerConnectionTimeout);
}
}
Loading