Skip to content

Commit

Permalink
feat: async factory function for eager connections
Browse files Browse the repository at this point in the history
This commit makes the following changes:

* Stop trying to do the eager connection stuff in the constructor
  of the CacheClient, because this can have surprising side effects,
  e.g. if we want to throw an error on failure.
* Remove the associated Configuration settings since we no longer
  support eager connections at construction time.
* Add a new factory function, CacheClient.CreateAsync, which is the
  new mechanism for create a client with an eager connection. This
  matches the pattern we established in the other SDKs that support
  eager connections.
* If the eager connection fails, we now throw a new ConnectionError
  rather than just logging a warning. This gives the consumer
  the ability to decide how to handle this type of error rather than
  us just swallowing it and removing their choice. In most cases,
  users would just end up hitting a timeout error on their next
  request after we gave up on the eager connection.

In the future we may add more configuration options for how to
handle eager connection failures, possibly including some
automatic retries. For now, this gives the user more control, which
seems extremely desirable given the number of times we have recently
seen users run into DNS throttling when they try to make a very
high volume of connections to Momento from lambdas.
  • Loading branch information
cprice404 committed Feb 23, 2024
1 parent 2836a65 commit 5916f73
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 60 deletions.
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);
}
}

0 comments on commit 5916f73

Please sign in to comment.