Skip to content

Commit 48db0cd

Browse files
Replace ShutdownComplete by Closed and ShutdownRequested (#2396)
1 parent 242155f commit 48db0cd

22 files changed

+1235
-781
lines changed

src/IceRpc/ClientConnection.cs

Lines changed: 275 additions & 125 deletions
Large diffs are not rendered by default.

src/IceRpc/ClientProtocolConnectionFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ public IProtocolConnection CreateConnection(ServerAddress serverAddress)
8989
_clientAuthenticationOptions),
9090
transportConnectionInformation: null,
9191
_connectionOptions);
92-
#pragma warning restore CA2000
9392

9493
connection = new MetricsProtocolConnectionDecorator(connection);
9594

9695
return _logger == NullLogger.Instance ? connection :
9796
new LogProtocolConnectionDecorator(connection, remoteNetworkAddress: null, _logger);
97+
#pragma warning restore CA2000
9898
}
9999
}

src/IceRpc/ConnectionCache.cs

Lines changed: 111 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public sealed class ConnectionCache : IInvoker, IAsyncDisposable
2323

2424
private readonly IClientProtocolConnectionFactory _connectionFactory;
2525

26+
private readonly TimeSpan _connectTimeout;
27+
28+
private Task? _disposeTask;
29+
2630
private readonly object _mutex = new();
2731

2832
// New connections in the process of connecting. They can be returned only after ConnectAsync succeeds.
@@ -33,6 +37,8 @@ public sealed class ConnectionCache : IInvoker, IAsyncDisposable
3337

3438
private readonly CancellationTokenSource _shutdownCts = new();
3539

40+
private readonly TimeSpan _shutdownTimeout;
41+
3642
/// <summary>Constructs a connection cache.</summary>
3743
/// <param name="options">The connection cache options.</param>
3844
/// <param name="duplexClientTransport">The duplex transport used to create ice protocol connections.</param>
@@ -52,6 +58,9 @@ public ConnectionCache(
5258
multiplexedClientTransport,
5359
logger);
5460

61+
_connectTimeout = options.ConnectionOptions.ConnectTimeout;
62+
_shutdownTimeout = options.ConnectionOptions.ShutdownTimeout;
63+
5564
_preferExistingConnection = options.PreferExistingConnection;
5665
}
5766

@@ -63,36 +72,35 @@ public ConnectionCache()
6372

6473
/// <summary>Releases all resources allocated by this connection cache.</summary>
6574
/// <returns>A value task that completes when all connections managed by this cache are disposed.</returns>
66-
public async ValueTask DisposeAsync()
75+
public ValueTask DisposeAsync()
6776
{
6877
lock (_mutex)
6978
{
70-
// We always cancel _shutdownCts with _mutex locked. This way, when _mutex is locked, _shutdownCts.Token
71-
// does not change.
72-
try
79+
if (_disposeTask is null)
7380
{
7481
_shutdownCts.Cancel();
82+
if (_backgroundConnectionDisposeCount == 0)
83+
{
84+
// There is no outstanding background dispose.
85+
_ = _backgroundConnectionDisposeTcs.TrySetResult();
86+
}
87+
_disposeTask = PerformDisposeAsync();
7588
}
76-
catch (ObjectDisposedException)
77-
{
78-
// already disposed by a previous or concurrent call.
79-
}
80-
81-
if (_backgroundConnectionDisposeCount == 0)
82-
{
83-
// There is no outstanding background dispose.
84-
_ = _backgroundConnectionDisposeTcs.TrySetResult();
85-
}
89+
return new(_disposeTask);
8690
}
8791

88-
// Dispose all connections managed by this cache.
89-
IEnumerable<IProtocolConnection> allConnections = _pendingConnections.Values.Select(value => value.Connection)
90-
.Concat(_activeConnections.Values);
92+
async Task PerformDisposeAsync()
93+
{
94+
await Task.Yield(); // exit mutex lock
95+
96+
IEnumerable<IProtocolConnection> allConnections =
97+
_pendingConnections.Values.Select(value => value.Connection).Concat(_activeConnections.Values);
9198

92-
await Task.WhenAll(allConnections.Select(connection => connection.DisposeAsync().AsTask())
93-
.Append(_backgroundConnectionDisposeTcs.Task)).ConfigureAwait(false);
99+
await Task.WhenAll(allConnections.Select(connection => connection.DisposeAsync().AsTask())
100+
.Append(_backgroundConnectionDisposeTcs.Task)).ConfigureAwait(false);
94101

95-
_shutdownCts.Dispose();
102+
_shutdownCts.Dispose();
103+
}
96104
}
97105

98106
/// <summary>Sends an outgoing request and returns the corresponding incoming response. If the request
@@ -186,7 +194,9 @@ async Task<IncomingResponse> PerformInvokeAsync()
186194
{
187195
try
188196
{
189-
connection = await ConnectAsync(mainServerAddress, cancellationToken).ConfigureAwait(false);
197+
// TODO: this code generates a UTE
198+
connection = await ConnectAsync(mainServerAddress).WaitAsync(cancellationToken)
199+
.ConfigureAwait(false);
190200
}
191201
catch (Exception) when (serverAddressFeature.AltServerAddresses.Count > 0)
192202
{
@@ -203,7 +213,7 @@ async Task<IncomingResponse> PerformInvokeAsync()
203213

204214
try
205215
{
206-
connection = await ConnectAsync(mainServerAddress, cancellationToken)
216+
connection = await ConnectAsync(mainServerAddress).WaitAsync(cancellationToken)
207217
.ConfigureAwait(false);
208218
break; // for
209219
}
@@ -238,52 +248,69 @@ public Task ShutdownAsync(CancellationToken cancellationToken = default)
238248
{
239249
lock (_mutex)
240250
{
241-
// We always cancel _shutdownCts with _mutex lock. This way, when _mutex is locked, _shutdownCts.Token
242-
// does not change.
243-
try
251+
if (_disposeTask is not null)
244252
{
245-
_shutdownCts.Cancel();
253+
throw new ObjectDisposedException($"{typeof(ConnectionCache)}");
246254
}
247-
catch (ObjectDisposedException)
255+
if (_shutdownCts.IsCancellationRequested)
248256
{
249-
throw new ObjectDisposedException($"{typeof(ConnectionCache)}");
257+
throw new InvalidOperationException($"The connection cache is already shut down or shutting down.");
250258
}
259+
260+
// We always cancel _shutdownCts with _mutex locked. This way, when _mutex is locked, _shutdownCts.Token
261+
// does not change.
262+
_shutdownCts.Cancel();
251263
}
252264

253-
// Shut down all connections managed by this cache.
254-
IEnumerable<IProtocolConnection> allConnections = _pendingConnections.Values.Select(value => value.Connection)
255-
.Concat(_activeConnections.Values);
265+
return PerformShutdownAsync();
266+
267+
async Task PerformShutdownAsync()
268+
{
269+
IEnumerable<IProtocolConnection> allConnections =
270+
_pendingConnections.Values.Select(value => value.Connection).Concat(_activeConnections.Values);
271+
272+
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
273+
cts.CancelAfter(_shutdownTimeout);
256274

257-
return Task.WhenAll(allConnections.Select(connection => connection.ShutdownAsync(cancellationToken)));
275+
try
276+
{
277+
// Note: this throws the first exception, not all of them.
278+
await Task.WhenAll(allConnections.Select(connection => connection.ShutdownAsync(cts.Token)))
279+
.ConfigureAwait(false);
280+
}
281+
catch (OperationCanceledException)
282+
{
283+
cancellationToken.ThrowIfCancellationRequested();
284+
throw new TimeoutException(
285+
$"The connection cache shutdown timed out after {_shutdownTimeout.TotalSeconds} s.");
286+
}
287+
}
258288
}
259289

260290
/// <summary>Creates a connection and attempts to connect this connection unless there is an active or pending
261291
/// connection for the desired server address.</summary>
262292
/// <param name="serverAddress">The server address.</param>
263-
/// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
264293
/// <returns>A connected connection.</returns>
265-
private async ValueTask<IProtocolConnection> ConnectAsync(
266-
ServerAddress serverAddress,
267-
CancellationToken cancellationToken)
294+
private async Task<IProtocolConnection> ConnectAsync(ServerAddress serverAddress)
268295
{
269296
(IProtocolConnection Connection, Task Task) pendingConnectionValue;
270297

271298
CancellationToken shutdownCancellationToken;
272299

273300
lock (_mutex)
274301
{
275-
try
276-
{
277-
shutdownCancellationToken = _shutdownCts.Token;
278-
}
279-
catch (ObjectDisposedException)
302+
if (_disposeTask is not null)
280303
{
281304
throw new ObjectDisposedException($"{typeof(ConnectionCache)}");
282305
}
283306

307+
// We make a copy of _shutdownCts.Token with the mutex locked. This copy remains usable after we release
308+
// the lock unlike _shutdownCts.Token that throws ObjectDisposedException once _shutdownCts is disposed.
309+
shutdownCancellationToken = _shutdownCts.Token;
310+
284311
if (shutdownCancellationToken.IsCancellationRequested)
285312
{
286-
throw new InvalidOperationException("connection cache is shut down or shutting down");
313+
throw new InvalidOperationException("Connection cache is shut down or shutting down.");
287314
}
288315

289316
if (_activeConnections.TryGetValue(serverAddress, out IProtocolConnection? connection))
@@ -309,12 +336,21 @@ async Task PerformConnectAsync(IProtocolConnection connection)
309336
{
310337
await Task.Yield(); // exit mutex lock
311338

312-
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
313-
cancellationToken,
314-
shutdownCancellationToken);
339+
using var cts = new CancellationTokenSource(_connectTimeout);
340+
using CancellationTokenRegistration tokenRegistration =
341+
shutdownCancellationToken.UnsafeRegister(cts => ((CancellationTokenSource)cts!).Cancel(), cts);
342+
315343
try
316344
{
317-
_ = await connection.ConnectAsync(cts.Token).ConfigureAwait(false);
345+
try
346+
{
347+
_ = await connection.ConnectAsync(cts.Token).ConfigureAwait(false);
348+
}
349+
catch (OperationCanceledException) when (!shutdownCancellationToken.IsCancellationRequested)
350+
{
351+
throw new TimeoutException(
352+
$"The connection establishment timed out after {_connectTimeout.TotalSeconds} s.");
353+
}
318354
}
319355
catch
320356
{
@@ -337,8 +373,6 @@ async Task PerformConnectAsync(IProtocolConnection connection)
337373
}
338374

339375
await connection.DisposeAsync().ConfigureAwait(false);
340-
341-
cancellationToken.ThrowIfCancellationRequested(); // throws OCE
342376
throw;
343377
}
344378

@@ -365,24 +399,22 @@ async Task PerformConnectAsync(IProtocolConnection connection)
365399

366400
async Task RemoveFromActiveAsync(IProtocolConnection connection, CancellationToken shutdownCancellationToken)
367401
{
402+
bool shutdownRequested;
403+
368404
try
369405
{
370-
await connection.ShutdownComplete.WaitAsync(shutdownCancellationToken).ConfigureAwait(false);
371-
}
372-
catch (OperationCanceledException exception) when (exception.CancellationToken == shutdownCancellationToken)
373-
{
374-
// The connection cache is being shut down or disposed and cache's DisposeAsync is responsible to
375-
// DisposeAsync this connection.
376-
return;
406+
shutdownRequested = await Task.WhenAny(connection.ShutdownRequested, connection.Closed)
407+
.WaitAsync(shutdownCancellationToken).ConfigureAwait(false) == connection.ShutdownRequested;
377408
}
378-
catch
409+
catch (OperationCanceledException)
379410
{
380-
// ignore and continue: the connection was aborted
411+
// The connection cache is being shut down or disposed. We handle it below after locking the mutex.
412+
shutdownRequested = false;
381413
}
414+
// no other exception can be thrown
382415

383416
lock (_mutex)
384417
{
385-
// shutdownCancellationToken.IsCancellationRequested remains the same when _mutex is locked.
386418
if (shutdownCancellationToken.IsCancellationRequested)
387419
{
388420
// ConnectionCache.DisposeAsync is responsible to dispose this connection.
@@ -396,6 +428,27 @@ async Task RemoveFromActiveAsync(IProtocolConnection connection, CancellationTok
396428
}
397429
}
398430

431+
if (shutdownRequested)
432+
{
433+
using var cts = new CancellationTokenSource(_shutdownTimeout);
434+
435+
try
436+
{
437+
await connection.ShutdownAsync(cts.Token).ConfigureAwait(false);
438+
}
439+
catch (OperationCanceledException)
440+
{
441+
}
442+
catch (IceRpcException)
443+
{
444+
}
445+
catch (Exception exception)
446+
{
447+
Debug.Fail($"Unexpected connection shutdown exception: {exception}");
448+
throw;
449+
}
450+
}
451+
399452
await connection.DisposeAsync().ConfigureAwait(false);
400453

401454
lock (_mutex)

src/IceRpc/IConnectionContext.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ public interface IConnectionContext
1515
/// non-null.</value>
1616
ServerAddress ServerAddress { get; }
1717

18-
/// <summary>Gets a task that completes when the connection is shut down or aborted.</summary>
19-
/// <value>A task that completes when the connection is successfully shut down. It completes with an exception when
20-
/// the connection is aborted.</value>
21-
Task ShutdownComplete { get; }
18+
/// <summary>Gets a task that completes when the connection is closed.</summary>
19+
/// <value>A task that completes when the connection is closed. If the connection was shut down gracefully, this
20+
/// task completes with a null exception; otherwise, it completes with the exception that aborted the connection.
21+
/// </value>
22+
/// <remarks>This task is never faulted or canceled.</remarks>
23+
Task<Exception?> Closed { get; }
2224

2325
/// <summary>Gets the transport connection information.</summary>
2426
TransportConnectionInformation TransportConnectionInformation { get; }

src/IceRpc/IProtocolConnection.cs

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@ namespace IceRpc;
1010
/// </summary>
1111
public interface IProtocolConnection : IInvoker, IAsyncDisposable
1212
{
13+
/// <summary>Gets a task that completes when the connection is closed.</summary>
14+
/// <value>A task that completes when the connection is closed. If the connection was shut down gracefully, this
15+
/// task completes with a null exception; otherwise, it completes with the exception that aborted the connection.
16+
/// </value>
17+
/// <remarks>This task is never faulted or canceled.</remarks>
18+
Task<Exception?> Closed { get; }
19+
1320
/// <summary>Gets the server address of this connection.</summary>
1421
/// <value>The server address of this connection. Its <see cref="ServerAddress.Transport" /> property is always
1522
/// non-null.</value>
1623
ServerAddress ServerAddress { get; }
1724

18-
/// <summary>Gets a task that completes when the connection is shut down or fails. The connection shutdown is
19-
/// initiated by any of the following events:
20-
/// <list type="bullet">
21-
/// <item><description>The application calls <see cref="ShutdownAsync" /> on the connection.</description></item>
22-
/// <item><description>The connection shuts down itself because it remained idle for longer than its configured idle
23-
/// timeout.</description></item>
24-
/// <item><description>The peer shuts down the connection.</description></item>
25-
/// </list>
25+
/// <summary>Gets a task that completes when the peer or the idle monitor requests the shutdown of this connection.
2626
/// </summary>
27-
/// <value>A task that completes when the connection is successfully shut down. It completes with an exception when
28-
/// the connection fails.</value>
29-
Task ShutdownComplete { get; }
27+
/// <remarks>This task is never faulted or canceled.</remarks>
28+
/// <seealso cref="ConnectionOptions.IdleTimeout" />
29+
Task ShutdownRequested { get; }
3030

3131
/// <summary>Establishes the connection to the peer.</summary>
3232
/// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
@@ -37,33 +37,27 @@ public interface IProtocolConnection : IInvoker, IAsyncDisposable
3737
/// </item>
3838
/// <item><description><see cref="OperationCanceledException" />if cancellation was requested through the
3939
/// cancellation token.</description></item>
40-
/// <item><description><see cref="TimeoutException" />if the connection establishment attempt exceeded <see
41-
/// cref="ConnectionOptions.ConnectTimeout" />.</description></item>
4240
/// </list>
4341
/// </returns>
4442
/// <exception cref="IceRpcException">Thrown if the connection is closed but not disposed yet.</exception>
43+
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once.</exception>
4544
/// <exception cref="ObjectDisposedException">Thrown if this connection is disposed.</exception>
4645
Task<TransportConnectionInformation> ConnectAsync(CancellationToken cancellationToken = default);
4746

48-
/// <summary>Gracefully shuts down the connection. The shutdown waits for pending invocations and dispatches to
49-
/// complete. For a speedier graceful shutdown, call <see cref="IAsyncDisposable.DisposeAsync" /> instead. It will
50-
/// cancel pending invocations and dispatches.</summary>
47+
/// <summary>Gracefully shuts down the connection.</summary>
5148
/// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
5249
/// <returns>A task that completes once the shutdown is complete. This task can also complete with one of the
5350
/// following exceptions:
5451
/// <list type="bullet">
5552
/// <item><description><see cref="IceRpcException" />if the connection shutdown failed.</description></item>
5653
/// <item><description><see cref="OperationCanceledException" />if cancellation was requested through the
5754
/// cancellation token.</description></item>
58-
/// <item><description><see cref="TimeoutException" />if this shutdown attempt or a previous attempt exceeded <see
59-
/// cref="ConnectionOptions.ShutdownTimeout" />.</description></item>
6055
/// </list>
6156
/// </returns>
6257
/// <exception cref="IceRpcException">Thrown if the connection is closed but not disposed yet.</exception>
6358
/// <exception cref="InvalidOperationException">Thrown if the connection was not connected successfully prior to
64-
/// call.</exception>
59+
/// this call, or if this method is called more than once.</exception>
6560
/// <exception cref="ObjectDisposedException">Thrown if this connection is disposed.</exception>
66-
/// <remarks>If shutdown is canceled, the protocol connection transitions to a faulted state and the disposal of the
67-
/// connection will abort the connection instead of performing a graceful speedy-shutdown.</remarks>
61+
/// <remarks>If cancellation token is canceled, the protocol connection is aborted.</remarks>
6862
Task ShutdownAsync(CancellationToken cancellationToken = default);
6963
}

0 commit comments

Comments
 (0)