-
Notifications
You must be signed in to change notification settings - Fork 14
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
Refactors how Slic keeps connections alive #3670
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
e0d32d7
Separate Ice and Slic idle timeout decorators
bernardnormier 0043c48
Refactor Slic connection keep alive logic
bernardnormier 50a7d46
More refactoring + new test
bernardnormier e1afc92
Cleanup
bernardnormier 5dff417
Fix review comments
bernardnormier fdb6d6f
Fix TODO
bernardnormier 19e4f31
Fix more review comments
bernardnormier 33aea40
More review comment fixes
bernardnormier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
src/IceRpc/Transports/Slic/Internal/SlicDuplexConnectionDecorator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// Copyright (c) ZeroC, Inc. | ||
|
||
using System.Buffers; | ||
using System.Diagnostics; | ||
|
||
namespace IceRpc.Transports.Slic.Internal; | ||
|
||
/// <summary>Decorates <see cref="ReadAsync" /> to fail if no byte is received for over idle timeout. Also optionally | ||
/// decorates both <see cref="ReadAsync"/> and <see cref="WriteAsync" /> to schedule pings that prevent both the local | ||
/// and remote idle timers from expiring.</summary> | ||
internal class SlicDuplexConnectionDecorator : IDuplexConnection | ||
{ | ||
private readonly IDuplexConnection _decoratee; | ||
private TimeSpan _idleTimeout = Timeout.InfiniteTimeSpan; | ||
private readonly CancellationTokenSource _readCts = new(); | ||
|
||
private readonly Timer? _readTimer; | ||
private readonly Timer? _writeTimer; | ||
|
||
public Task<TransportConnectionInformation> ConnectAsync(CancellationToken cancellationToken) => | ||
_decoratee.ConnectAsync(cancellationToken); | ||
|
||
public void Dispose() | ||
{ | ||
_decoratee.Dispose(); | ||
_readCts.Dispose(); | ||
|
||
// Using Dispose is fine, there's no need to wait for the keep alive action to terminate if it's running. | ||
_readTimer?.Dispose(); | ||
_writeTimer?.Dispose(); | ||
} | ||
|
||
public ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken) | ||
{ | ||
return _idleTimeout == Timeout.InfiniteTimeSpan ? | ||
_decoratee.ReadAsync(buffer, cancellationToken) : | ||
PerformReadAsync(); | ||
|
||
async ValueTask<int> PerformReadAsync() | ||
{ | ||
try | ||
{ | ||
using CancellationTokenRegistration _ = cancellationToken.UnsafeRegister( | ||
cts => ((CancellationTokenSource)cts!).Cancel(), | ||
_readCts); | ||
_readCts.CancelAfter(_idleTimeout); // enable idle timeout before reading | ||
|
||
int bytesRead = await _decoratee.ReadAsync(buffer, _readCts.Token).ConfigureAwait(false); | ||
|
||
// After each successful read, we schedule one ping some time in the future. | ||
if (bytesRead > 0) | ||
{ | ||
ResetReadTimer(); | ||
} | ||
// When 0, the other side called ShutdownWriteAsync, so there is no point to send a ping since we can't | ||
// get back a pong. | ||
|
||
return bytesRead; | ||
} | ||
catch (OperationCanceledException) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
|
||
throw new IceRpcException( | ||
IceRpcError.ConnectionIdle, | ||
$"The connection did not receive any bytes for over {_idleTimeout.TotalSeconds} s."); | ||
} | ||
finally | ||
{ | ||
_readCts.CancelAfter(Timeout.InfiniteTimeSpan); // disable idle timeout if not canceled | ||
} | ||
} | ||
} | ||
|
||
public Task ShutdownWriteAsync(CancellationToken cancellationToken) => | ||
_decoratee.ShutdownWriteAsync(cancellationToken); | ||
|
||
public ValueTask WriteAsync(ReadOnlySequence<byte> buffer, CancellationToken cancellationToken) | ||
{ | ||
return _idleTimeout == Timeout.InfiniteTimeSpan ? | ||
_decoratee.WriteAsync(buffer, cancellationToken) : | ||
PerformWriteAsync(); | ||
|
||
async ValueTask PerformWriteAsync() | ||
{ | ||
await _decoratee.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); | ||
|
||
// After each successful write, we schedule one ping some time in the future. Since each ping is itself a | ||
// write, if there is no application activity at all, we'll send successive pings at regular intervals. | ||
ResetWriteTimer(); | ||
} | ||
} | ||
|
||
/// <summary>Constructs a decorator that does nothing until it is enabled by a call to <see cref="Enable"/>. | ||
/// </summary> | ||
internal SlicDuplexConnectionDecorator(IDuplexConnection decoratee) => _decoratee = decoratee; | ||
|
||
/// <summary>Constructs a decorator that does nothing until it is enabled by a call to <see cref="Enable"/>. | ||
/// </summary> | ||
internal SlicDuplexConnectionDecorator(IDuplexConnection decoratee, Action sendReadPing, Action sendWritePing) | ||
: this(decoratee) | ||
{ | ||
_readTimer = new Timer(_ => sendReadPing()); | ||
_writeTimer = new Timer(_ => sendWritePing()); | ||
} | ||
|
||
/// <summary>Sets the idle timeout and schedules pings once the connection is established.</summary>. | ||
internal void Enable(TimeSpan idleTimeout) | ||
{ | ||
Debug.Assert(idleTimeout != Timeout.InfiniteTimeSpan); | ||
_idleTimeout = idleTimeout; | ||
|
||
ResetReadTimer(); | ||
ResetWriteTimer(); | ||
} | ||
|
||
/// <summary>Resets the read timer. We send a "read" ping when this timer expires.</summary> | ||
/// <remarks>This method is no-op unless this decorator is constructed with send ping actions.</remarks> | ||
private void ResetReadTimer() => _readTimer?.Change(_idleTimeout * 0.5, Timeout.InfiniteTimeSpan); | ||
|
||
/// <summary>Resets the write timer. We send a "write" ping when this timer expires.</summary> | ||
/// <remarks>This method is no-op unless this decorator is constructed with send ping actions.</remarks> | ||
// The write timer factor (0.6) was chosen to be greater than the read timer factor (0.5). This way, when the | ||
// connection is completely idle, the read timer expires before the write timer and has time to send a ping that | ||
// resets the write timer. This reduces the likelihood of duplicate "keep alive" pings. | ||
private void ResetWriteTimer() => _writeTimer?.Change(_idleTimeout * 0.6, Timeout.InfiniteTimeSpan); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be better to keep
IDuplexConnection
and then doThen we can make
SlicDuplexConnectionDecorator
timers non-nullable, and keep a single constructor.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how this would work.
The difficulty here is the idle timeout is negotiated during connection establishment. At construction time, we don't know if the negotiated idle timeout is good to be infinite or some other value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can pass the
idleTimeout
I forget to add it there, and that is not the point. The point is to avoid creating this decorator for the Server connections.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need the decorator for server connections too. It implements the idle timer.