Skip to content

Commit a57f1e5

Browse files
authored
Fix using disposed token in connect and resulting status (#1873)
1 parent 0964116 commit a57f1e5

File tree

6 files changed

+48
-3
lines changed

6 files changed

+48
-3
lines changed

src/Grpc.Net.Client/Balancer/Internal/ErrorPicker.cs

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
#if SUPPORT_LOAD_BALANCING
2020
using System;
21+
using System.Diagnostics;
2122
using Grpc.Core;
2223

2324
namespace Grpc.Net.Client.Balancer.Internal
@@ -28,6 +29,7 @@ internal class ErrorPicker : SubchannelPicker
2829

2930
public ErrorPicker(Status status)
3031
{
32+
Debug.Assert(status.StatusCode != StatusCode.OK, "Error status code must not be OK.");
3133
_status = status;
3234
}
3335

src/Grpc.Net.Client/Balancer/Internal/ISubchannelTransport.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,22 @@ internal interface ISubchannelTransport : IDisposable
4646
internal sealed class ConnectContext
4747
{
4848
private readonly CancellationTokenSource _cts;
49+
private readonly CancellationToken _token;
4950
private bool _disposed;
5051

5152
// This flag allows the transport to determine why the cancellation token was canceled.
5253
// - Explicit cancellation, e.g. the channel was disposed.
5354
// - Connection timeout, e.g. SocketsHttpHandler.ConnectTimeout was exceeded.
5455
public bool IsConnectCanceled { get; private set; }
5556

56-
public CancellationToken CancellationToken => _cts.Token;
57+
public CancellationToken CancellationToken => _token;
5758

5859
public ConnectContext(TimeSpan connectTimeout)
5960
{
6061
_cts = new CancellationTokenSource(connectTimeout);
62+
63+
// Take a copy of the token to avoid ObjectDisposedException when accessing _cts.Token after CTS is disposed.
64+
_token = _cts.Token;
6165
}
6266

6367
public void CancelConnect()

src/Grpc.Net.Client/Balancer/Subchannel.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ private async Task ConnectTransportAsync()
313313
{
314314
SubchannelLog.ConnectError(_logger, Id, ex);
315315

316-
UpdateConnectivityState(ConnectivityState.TransientFailure, "Error connecting to subchannel.");
316+
UpdateConnectivityState(ConnectivityState.TransientFailure, new Status(StatusCode.Unavailable, "Error connecting to subchannel.", ex));
317317
}
318318
finally
319319
{

test/Grpc.Net.Client.Tests/Balancer/ConnectionManagerTests.cs

+37
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,43 @@ public async Task PickAsync_WaitForReadyWithDrop_ThrowsError()
144144
Assert.AreEqual(StatusCode.DataLoss, ex.StatusCode);
145145
}
146146

147+
[Test]
148+
public async Task PickAsync_ErrorConnectingToSubchannel_ThrowsError()
149+
{
150+
// Arrange
151+
var services = new ServiceCollection();
152+
services.AddNUnitLogger();
153+
var serviceProvider = services.BuildServiceProvider();
154+
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
155+
156+
var resolver = new TestResolver(loggerFactory);
157+
resolver.UpdateAddresses(new List<BalancerAddress>
158+
{
159+
new BalancerAddress("localhost", 80)
160+
});
161+
162+
var transportFactory = new TestSubchannelTransportFactory((s, c) =>
163+
{
164+
return Task.FromException<ConnectivityState>(new Exception("Test error!"));
165+
});
166+
var clientChannel = CreateConnectionManager(loggerFactory, resolver, transportFactory);
167+
clientChannel.ConfigureBalancer(c => new PickFirstBalancer(c, loggerFactory));
168+
169+
// Act
170+
_ = clientChannel.ConnectAsync(waitForReady: false, CancellationToken.None).ConfigureAwait(false);
171+
172+
var pickTask = clientChannel.PickAsync(
173+
new PickContext { Request = new HttpRequestMessage() },
174+
waitForReady: false,
175+
CancellationToken.None).AsTask();
176+
177+
// Assert
178+
var ex = await ExceptionAssert.ThrowsAsync<RpcException>(() => pickTask).DefaultTimeout();
179+
Assert.AreEqual(StatusCode.Unavailable, ex.StatusCode);
180+
Assert.AreEqual("Error connecting to subchannel.", ex.Status.Detail);
181+
Assert.AreEqual("Test error!", ex.Status.DebugException?.Message);
182+
}
183+
147184
[Test]
148185
public async Task PickAsync_RetryWithDrop_ThrowsError()
149186
{

test/Grpc.Net.Client.Tests/GrpcChannelTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ public async Task ConnectAsync_ShiftThroughStates_CompleteOnReady()
626626
var currentConnectivityState = ConnectivityState.TransientFailure;
627627

628628
var services = new ServiceCollection();
629+
services.AddNUnitLogger();
629630
services.AddSingleton<ResolverFactory, ChannelTestResolverFactory>();
630631
services.AddSingleton<ISubchannelTransportFactory>(new TestSubchannelTransportFactory(async (s, c) =>
631632
{

test/Grpc.Net.Client.Tests/Infrastructure/Balancer/TestSubChannelTransport.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ public async
8181
var newState = await (_onTryConnect?.Invoke(context.CancellationToken) ?? Task.FromResult(ConnectivityState.Ready));
8282

8383
CurrentAddress = Subchannel._addresses[0];
84-
Subchannel.UpdateConnectivityState(newState, Status.DefaultSuccess);
84+
var newStatus = newState == ConnectivityState.TransientFailure ? new Status(StatusCode.Internal, "") : Status.DefaultSuccess;
85+
Subchannel.UpdateConnectivityState(newState, newStatus);
8586

8687
_connectTcs.TrySetResult(null);
8788

0 commit comments

Comments
 (0)