-
Notifications
You must be signed in to change notification settings - Fork 165
Add KeepAliveMode and SupportedWebSocketSubProtocols options #1154
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
Merged
Merged
Changes from 2 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
3b7645b
Add KeepAliveMode option
Shane32 4c339d5
update
Shane32 37345e8
Merge branch 'master' into keepalivemode
Shane32 cd06492
Merge branch 'master' into keepalivemode
Shane32 2cad736
Update
Shane32 4bc4f43
Add tests
Shane32 3f2107c
Merge remote-tracking branch 'origin/master' into keepalivemode
Shane32 1ab10e9
Update readme
Shane32 d5ec0df
Update
Shane32 32cd6bc
update
Shane32 2141f56
Update src/Transports.AspNetCore/WebSockets/GraphQLWs/SubscriptionSer…
Shane32 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 hidden or 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 hidden or 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 hidden or 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
12 changes: 12 additions & 0 deletions
12
src/Transports.AspNetCore/WebSockets/GraphQLWs/PingPayload.cs
This file contains hidden or 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,12 @@ | ||
| namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs; | ||
|
|
||
| /// <summary> | ||
| /// The payload of the ping message. | ||
| /// </summary> | ||
| public class PingPayload | ||
| { | ||
| /// <summary> | ||
| /// The unique identifier of the ping message. | ||
| /// </summary> | ||
| public string? id { get; set; } | ||
gao-artur marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
This file contains hidden or 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 |
|---|---|---|
|
|
@@ -4,6 +4,11 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs; | |
| public class SubscriptionServer : BaseSubscriptionServer | ||
| { | ||
| private readonly IWebSocketAuthenticationService? _authenticationService; | ||
| private readonly IGraphQLSerializer _serializer; | ||
| private readonly GraphQLWebSocketOptions _options; | ||
| private DateTime _lastPongReceivedUtc; | ||
| private string? _lastPingId; | ||
| private readonly object _lastPingLock = new(); | ||
|
|
||
| /// <summary> | ||
| /// The WebSocket sub-protocol used for this protocol. | ||
|
|
@@ -67,6 +72,8 @@ public SubscriptionServer( | |
| UserContextBuilder = userContextBuilder ?? throw new ArgumentNullException(nameof(userContextBuilder)); | ||
| Serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); | ||
| _authenticationService = authenticationService; | ||
| _serializer = serializer; | ||
| _options = options; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
|
|
@@ -90,7 +97,9 @@ public override async Task OnMessageReceivedAsync(OperationMessage message) | |
| } | ||
| else | ||
| { | ||
| #pragma warning disable CS0618 // Type or member is obsolete | ||
| await OnConnectionInitAsync(message, true); | ||
| #pragma warning restore CS0618 // Type or member is obsolete | ||
| } | ||
| return; | ||
| } | ||
|
|
@@ -113,6 +122,69 @@ public override async Task OnMessageReceivedAsync(OperationMessage message) | |
| } | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| [Obsolete($"Please use the {nameof(OnConnectionInitAsync)} and {nameof(OnKeepAliveLoopAsync)} methods instead. This method will be removed in a future version of this library.")] | ||
| protected override Task OnConnectionInitAsync(OperationMessage message, bool smartKeepAlive) | ||
|
Comment on lines
+126
to
+127
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code has been carefully crafted to be fully backwards compatible, just in case of the rare instance that someone derived from this class to customize behavior. |
||
| { | ||
| if (smartKeepAlive) | ||
| return base.OnConnectionInitAsync(message); | ||
| else | ||
| return base.OnConnectionInitAsync(message, smartKeepAlive); | ||
|
||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| protected override Task OnKeepAliveLoopAsync(TimeSpan keepAliveTimeout, KeepAliveMode keepAliveMode) | ||
gao-artur marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| if (keepAliveMode == KeepAliveMode.TimeoutWithPayload) | ||
| { | ||
| if (keepAliveTimeout <= TimeSpan.Zero) | ||
| return Task.CompletedTask; | ||
| return SecureKeepAliveLoopAsync(keepAliveTimeout, keepAliveTimeout); | ||
| } | ||
| return base.OnKeepAliveLoopAsync(keepAliveTimeout, keepAliveMode); | ||
|
|
||
| // pingInterval is the time since the last pong was received before sending a new ping | ||
| // pongInterval is the time to wait for a pong after a ping was sent before forcibly closing the connection | ||
| async Task SecureKeepAliveLoopAsync(TimeSpan pingInterval, TimeSpan pongInterval) | ||
| { | ||
| lock (_lastPingLock) | ||
| _lastPongReceivedUtc = DateTime.UtcNow; | ||
Shane32 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| while (!CancellationToken.IsCancellationRequested) | ||
| { | ||
| // Wait for the next ping interval | ||
| TimeSpan interval; | ||
| var now = DateTime.UtcNow; | ||
| DateTime lastPongReceivedUtc; | ||
| lock (_lastPingLock) | ||
| { | ||
| lastPongReceivedUtc = _lastPongReceivedUtc; | ||
| } | ||
| var nextPing = _lastPongReceivedUtc.Add(pingInterval); | ||
Shane32 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| interval = nextPing.Subtract(now); | ||
| if (interval > TimeSpan.Zero) // could easily be zero or less, if pongInterval is equal or greater than pingInterval | ||
| await Task.Delay(interval, CancellationToken); | ||
|
|
||
| // Send a new ping message | ||
| await OnSendKeepAliveAsync(); | ||
|
|
||
| // Wait for the pong response | ||
| await Task.Delay(pongInterval, CancellationToken); | ||
| bool abort; | ||
| lock (_lastPingLock) | ||
| { | ||
| abort = _lastPongReceivedUtc == lastPongReceivedUtc; | ||
| } | ||
| if (abort) | ||
| { | ||
| // Forcibly close the connection if the client has not responded to the keep-alive message. | ||
| // Do not send a close message to the client or wait for a response. | ||
| Connection.HttpContext.Abort(); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Pong is a required response to a ping, and also a unidirectional keep-alive packet, | ||
| /// whereas ping is a bidirectional keep-alive packet. | ||
|
|
@@ -129,11 +201,46 @@ protected virtual Task OnPingAsync(OperationMessage message) | |
| /// Executes when a pong message is received. | ||
| /// </summary> | ||
| protected virtual Task OnPongAsync(OperationMessage message) | ||
| => Task.CompletedTask; | ||
| { | ||
| if (_options.KeepAliveMode == KeepAliveMode.TimeoutWithPayload) | ||
| { | ||
| try | ||
| { | ||
| var pingId = _serializer.ReadNode<PingPayload>(message.Payload)?.id; | ||
| lock (_lastPingLock) | ||
| { | ||
| if (_lastPingId == pingId) | ||
| _lastPongReceivedUtc = DateTime.UtcNow; | ||
| } | ||
| } | ||
| catch { } // ignore deserialization errors in case the pong message does not match the expected format | ||
|
||
| } | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| protected override Task OnSendKeepAliveAsync() | ||
| => Connection.SendMessageAsync(_pongMessage); | ||
| { | ||
| if (_options.KeepAliveMode == KeepAliveMode.TimeoutWithPayload) | ||
| { | ||
| var lastPingId = Guid.NewGuid().ToString("N"); | ||
| lock (_lastPingLock) | ||
| { | ||
| _lastPingId = lastPingId; | ||
| } | ||
| return Connection.SendMessageAsync( | ||
| new() | ||
| { | ||
| Type = MessageType.Ping, | ||
| Payload = new PingPayload { id = lastPingId } | ||
| } | ||
| ); | ||
| } | ||
| else | ||
| { | ||
| return Connection.SendMessageAsync(_pongMessage); | ||
| } | ||
| } | ||
|
|
||
| private static readonly OperationMessage _connectionAckMessage = new() { Type = MessageType.ConnectionAck }; | ||
| /// <inheritdoc/> | ||
|
|
||
This file contains hidden or 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,36 @@ | ||
| namespace GraphQL.Server.Transports.AspNetCore.WebSockets; | ||
|
|
||
| /// <summary> | ||
| /// Specifies the mode of keep-alive behavior. | ||
| /// </summary> | ||
| public enum KeepAliveMode | ||
| { | ||
| /// <summary> | ||
| /// Same as <see cref="Timeout"/>: Sends a unidirectional keep-alive message when no message has been received within the specified timeout period. | ||
| /// </summary> | ||
| Default = 0, | ||
|
|
||
| /// <summary> | ||
| /// Sends a unidirectional keep-alive message when no message has been received within the specified timeout period. | ||
| /// </summary> | ||
| Timeout = 1, | ||
|
|
||
| /// <summary> | ||
| /// Sends a unidirectional keep-alive message at a fixed interval, regardless of message activity. | ||
| /// </summary> | ||
| Interval = 2, | ||
|
|
||
| /// <summary> | ||
| /// Sends a Ping message with a payload after the specified timeout from the last received Pong, | ||
| /// and waits for a corresponding Pong response. Requires that the client reflects the payload | ||
| /// in the response. Forcibly disconnects the client if the client does not respond with a Pong | ||
| /// message within the specified timeout. This means that a dead connection will be closed after | ||
| /// a maximum of double the <see cref="GraphQLWebSocketOptions.KeepAliveTimeout"/> period. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This mode is particularly useful when backpressure causes subscription messages to be delayed | ||
| /// due to a slow or unresponsive client connection. The server can detect that the client is not | ||
| /// processing messages in a timely manner and disconnect the client to free up resources. | ||
| /// </remarks> | ||
| TimeoutWithPayload = 3, | ||
| } |
This file contains hidden or 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
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.
Uh oh!
There was an error while loading. Please reload this page.