Skip to content

Commit

Permalink
Support infinite idle connection timeout values (#2231)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Aug 6, 2023
1 parent cec98b2 commit 19e059f
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ public async ValueTask<Stream> GetStreamAsync(BalancerAddress address, Cancellat

var closeSocket = false;

if (DateTime.UtcNow > socketCreatedTime.Value.Add(_socketIdleTimeout))
if (_socketIdleTimeout != Timeout.InfiniteTimeSpan && DateTime.UtcNow > socketCreatedTime.Value.Add(_socketIdleTimeout))
{
SocketConnectivitySubchannelTransportLog.ClosingSocketFromIdleTimeoutOnCreateStream(_logger, _subchannel.Id, address, _socketIdleTimeout);
closeSocket = true;
Expand Down
23 changes: 22 additions & 1 deletion src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ private static HttpHandlerContext CalculateHandlerContext(ILogger logger, Uri ad

type = HttpHandlerType.SocketsHttpHandler;
connectTimeout = socketsHttpHandler.ConnectTimeout;
connectionIdleTimeout = socketsHttpHandler.PooledConnectionIdleTimeout;
connectionIdleTimeout = GetConnectionIdleTimeout(socketsHttpHandler);

// Check if the SocketsHttpHandler is being shared by channels.
// It has already been setup by another channel (i.e. ConnectCallback is set) then
Expand Down Expand Up @@ -312,6 +312,27 @@ private static HttpHandlerContext CalculateHandlerContext(ILogger logger, Uri ad
}

return new HttpHandlerContext(HttpHandlerType.Custom);

#if NET5_0_OR_GREATER
static TimeSpan? GetConnectionIdleTimeout(SocketsHttpHandler socketsHttpHandler)
{
// Check if either TimeSpan is InfiniteTimeSpan, and return the other one.
if (socketsHttpHandler.PooledConnectionIdleTimeout == Timeout.InfiniteTimeSpan)
{
return socketsHttpHandler.PooledConnectionLifetime;
}

if (socketsHttpHandler.PooledConnectionLifetime == Timeout.InfiniteTimeSpan)
{
return socketsHttpHandler.PooledConnectionIdleTimeout;
}

// Return the bigger TimeSpan.
return socketsHttpHandler.PooledConnectionIdleTimeout > socketsHttpHandler.PooledConnectionLifetime
? socketsHttpHandler.PooledConnectionIdleTimeout
: socketsHttpHandler.PooledConnectionLifetime;
}
#endif
}

#if NET5_0_OR_GREATER
Expand Down
45 changes: 40 additions & 5 deletions test/FunctionalTests/Balancer/ConnectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,9 @@ async Task<HelloReply> UnaryMethod(HelloRequest request, ServerCallContext conte
await ExceptionAssert.ThrowsAsync<OperationCanceledException>(() => connectTask).DefaultTimeout();
}

[Test]
public async Task Active_UnaryCall_ConnectionIdleTimeout_SocketRecreated()
[TestCase(0)] // TimeSpan.Zero
[TestCase(1000)] // 1 second
public async Task Active_UnaryCall_ConnectionIdleTimeout_SocketRecreated(int milliseconds)
{
// Ignore errors
SetExpectedErrorsFilter(writeContext =>
Expand All @@ -158,7 +159,7 @@ Task<HelloReply> UnaryMethod(HelloRequest request, ServerCallContext context)
// Arrange
using var endpoint = BalancerHelpers.CreateGrpcEndpoint<HelloRequest, HelloReply>(50051, UnaryMethod, nameof(UnaryMethod), loggerFactory: LoggerFactory);

var connectionIdleTimeout = TimeSpan.FromSeconds(1);
var connectionIdleTimeout = TimeSpan.FromMilliseconds(milliseconds);
var channel = await BalancerHelpers.CreateChannel(
LoggerFactory,
new PickFirstConfig(),
Expand All @@ -168,18 +169,52 @@ Task<HelloReply> UnaryMethod(HelloRequest request, ServerCallContext context)
Logger.LogInformation("Connecting channel.");
await channel.ConnectAsync();

await Task.Delay(connectionIdleTimeout);
// Wait for timeout plus a little extra to avoid issues from imprecise timers.
await Task.Delay(connectionIdleTimeout + TimeSpan.FromMilliseconds(50));

var client = TestClientFactory.Create(channel, endpoint.Method);
var response = await client.UnaryCall(new HelloRequest { Name = "Test!" }).ResponseAsync.DefaultTimeout();

// Assert
Assert.AreEqual("Test!", response.Message);

AssertHasLog(LogLevel.Debug, "ClosingSocketFromIdleTimeoutOnCreateStream", "Subchannel id '1' socket 127.0.0.1:50051 is being closed because it exceeds the idle timeout of 00:00:01.");
AssertHasLog(LogLevel.Debug, "ClosingSocketFromIdleTimeoutOnCreateStream");
AssertHasLog(LogLevel.Trace, "ConnectingOnCreateStream", "Subchannel id '1' doesn't have a connected socket available. Connecting new stream socket for 127.0.0.1:50051.");
}

public async Task Active_UnaryCall_InfiniteConnectionIdleTimeout_SocketNotClosed()
{
SetExpectedErrorsFilter(writeContext =>
{
return true;
});

Task<HelloReply> UnaryMethod(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = request.Name });
}

// Arrange
using var endpoint = BalancerHelpers.CreateGrpcEndpoint<HelloRequest, HelloReply>(50051, UnaryMethod, nameof(UnaryMethod), loggerFactory: LoggerFactory);

var channel = await BalancerHelpers.CreateChannel(
LoggerFactory,
new PickFirstConfig(),
new[] { endpoint.Address },
connectionIdleTimeout: Timeout.InfiniteTimeSpan).DefaultTimeout();

Logger.LogInformation("Connecting channel.");
await channel.ConnectAsync();

var client = TestClientFactory.Create(channel, endpoint.Method);
var response = await client.UnaryCall(new HelloRequest { Name = "Test!" }).ResponseAsync.DefaultTimeout();

// Assert
Assert.AreEqual("Test!", response.Message);

Assert.IsFalse(Logs.Any(l => l.EventId.Name == "ClosingSocketFromIdleTimeoutOnCreateStream"), "Shouldn't have a ClosingSocketFromIdleTimeoutOnCreateStream log.");
}

[Test]
public async Task Active_UnaryCall_ServerCloseOnKeepAlive_SocketRecreatedOnRequest()
{
Expand Down
8 changes: 4 additions & 4 deletions test/FunctionalTests/FunctionalTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,21 @@ protected void AssertHasLogRpcConnectionError(StatusCode statusCode, string deta
AssertHasLog(LogLevel.Information, "RpcConnectionError", $"Error status code '{statusCode}' with detail '{detail}' raised.");
}

protected void AssertHasLog(LogLevel logLevel, string name, string message, Func<Exception, bool>? exceptionMatch = null)
protected void AssertHasLog(LogLevel logLevel, string name, string? message = null, Func<Exception, bool>? exceptionMatch = null)
{
if (HasLog(logLevel, name, message, exceptionMatch))
{
return;
}

Assert.Fail($"No match. Log level = {logLevel}, name = {name}, message = '{message}'.");
Assert.Fail($"No match. Log level = {logLevel}, name = {name}, message = '{message ?? "(null)"}'.");
}

protected bool HasLog(LogLevel logLevel, string name, string message, Func<Exception, bool>? exceptionMatch = null)
protected bool HasLog(LogLevel logLevel, string name, string? message = null, Func<Exception, bool>? exceptionMatch = null)
{
return Logs.Any(r =>
{
var match = r.LogLevel == logLevel && r.EventId.Name == name && r.Message == message;
var match = r.LogLevel == logLevel && r.EventId.Name == name && (r.Message == message || message == null);
if (exceptionMatch != null)
{
match = match && r.Exception != null && exceptionMatch(r.Exception);
Expand Down
32 changes: 25 additions & 7 deletions test/Grpc.Net.Client.Tests/GrpcChannelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,17 +222,35 @@ public void Build_ConnectTimeout_ReadFromSocketsHttpHandler()
Assert.AreEqual(TimeSpan.FromSeconds(1), channel.ConnectTimeout);
}

[Test]
public void Build_ConnectionIdleTimeout_ReadFromSocketsHttpHandler()
[TestCase(-1, -1, -1)]
[TestCase(0, 0, 0)]
[TestCase(0, -1, 0)]
[TestCase(-1, 0, 0)]
[TestCase(1000, -1, 1000)]
[TestCase(-1, 1000, 1000)]
[TestCase(500, 1000, 1000)]
[TestCase(1000, 500, 1000)]
public void Build_ConnectionIdleTimeout_ReadFromSocketsHttpHandler(
int? pooledConnectionIdleTimeoutMs,
int? pooledConnectionLifetimeMs,
int expectedConnectionIdleTimeoutMs)
{
// Arrange & Act
var channel = GrpcChannel.ForAddress("https://localhost", CreateGrpcChannelOptions(o => o.HttpHandler = new SocketsHttpHandler
// Arrange
var handler = new SocketsHttpHandler();
if (pooledConnectionIdleTimeoutMs != null)
{
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(1)
}));
handler.PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(pooledConnectionIdleTimeoutMs.Value);
}
if (pooledConnectionLifetimeMs != null)
{
handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(pooledConnectionLifetimeMs.Value);
}

// Act
var channel = GrpcChannel.ForAddress("https://localhost", CreateGrpcChannelOptions(o => o.HttpHandler = handler));

// Assert
Assert.AreEqual(TimeSpan.FromSeconds(1), channel.ConnectionIdleTimeout);
Assert.AreEqual(TimeSpan.FromMilliseconds(expectedConnectionIdleTimeoutMs), channel.ConnectionIdleTimeout);
}
#endif

Expand Down

0 comments on commit 19e059f

Please sign in to comment.