-
Notifications
You must be signed in to change notification settings - Fork 66
Use HTTP2 full duplex for committing session #561
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 all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
03046e9
Use HTTP2 full duplex for committing session
twsouthwick 191ad8b
respond to feedback
twsouthwick 4ad3387
update doc
twsouthwick 94818d6
check if server supports mode
twsouthwick 3147bcc
add tests
twsouthwick 2a84149
add logging
twsouthwick ae4fce5
log disabled
twsouthwick 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# Remote session Protocol | ||
|
||
## Readonly | ||
|
||
Readonly session will retrieve the session state from the framework app without any sort of locking. This consists of a single `GET` request that will return a session state and can be closed immediately. | ||
|
||
```mermaid | ||
sequenceDiagram | ||
participant core as ASP.NET Core | ||
participant framework as ASP.NET | ||
participant session as Session Store | ||
core ->> +framework: GET /session | ||
framework ->> session: Request session | ||
session -->> framework: Session | ||
framework -->> -core: Session | ||
core ->> core: Run request | ||
``` | ||
|
||
## Writeable (single connection with HTTP2 and SSL) | ||
|
||
Writeable session state protocol consists of a `POST` request that requires streaming over HTTP2 full-duplex. | ||
|
||
```mermaid | ||
sequenceDiagram | ||
participant core as ASP.NET Core | ||
participant framework as ASP.NET | ||
participant session as Session Store | ||
core ->> +framework: POST /session | ||
framework ->> session: Request session | ||
session -->> framework: Session | ||
framework -->> core: Session | ||
core ->> core: Run request | ||
core ->> framework: Updated session state | ||
framework ->> session: Persist | ||
framework -->> -core: Persist result (JSON) | ||
``` | ||
|
||
## Writeable (two connections when HTTP2 or SSL are unavailable) | ||
|
||
Writeable session state protocol starts with the the same as the readonly, but differs in the following: | ||
|
||
- Requires an additional `PUT` request to update the state | ||
- The initial `GET` request must be kept open until the session is done; if closed, the session will not be able to be updated | ||
- A lock store (implemented internally as `ILockedSessionCache`) is used to track active open requests | ||
|
||
```mermaid | ||
sequenceDiagram | ||
participant core as ASP.NET Core | ||
participant framework as ASP.NET | ||
participant store as Lock Store | ||
participant session as Session Store | ||
core ->> +framework: GET /session | ||
framework ->> session: Request session | ||
session -->> framework: Session | ||
framework -->> +store: Register active request | ||
framework -->> core: Session | ||
core ->> core: Run request | ||
core ->> +framework: PUT /session | ||
framework ->> store: Finalize session | ||
store ->> session: Persist | ||
store ->> -framework: Notify complete | ||
framework ->> -core: Persist complete | ||
framework -->> -core: Session complete | ||
``` |
129 changes: 129 additions & 0 deletions
129
...apters.CoreServices/SessionState/RemoteSession/DoubleConnectionRemoteAppSessionManager.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,129 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.IO; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.Net.Http.Headers; | ||
|
||
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession; | ||
|
||
/// <summary> | ||
/// This is an implementation of <see cref="ISessionManager"/> that connects to an upstream session store. | ||
/// <list type="bullet"> | ||
/// <item>If the request is readonly, it will close the remote connection and return the session state.</item> | ||
/// <item>If the request is not readonly, it will hold onto the remote connection and initiate a new connection to PUT the results.</item> | ||
/// </list> | ||
/// </summary> | ||
/// <remarks> | ||
/// For the non-readonly mode, it is preferrable to use <see cref="SingleConnectionWriteableRemoteAppSessionStateManager"/> instead | ||
/// which will only use a single connection via HTTP2 streaming. | ||
/// </remarks> | ||
internal sealed class DoubleConnectionRemoteAppSessionManager( | ||
ISessionSerializer serializer, | ||
IOptions<RemoteAppSessionStateClientOptions> options, | ||
IOptions<RemoteAppClientOptions> remoteAppClientOptions, | ||
ILogger<DoubleConnectionRemoteAppSessionManager> logger | ||
) : RemoteAppSessionStateManager(serializer, options, remoteAppClientOptions, logger) | ||
{ | ||
public Task<ISessionState> GetReadOnlySessionStateAsync(HttpContextCore context) => CreateAsync(context, isReadOnly: true); | ||
|
||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "They are either passed into another object or are manually disposed")] | ||
protected override async Task<ISessionState> GetSessionDataAsync(string? sessionId, bool readOnly, HttpContextCore callingContext, CancellationToken token) | ||
{ | ||
// The request message is manually disposed at a later time | ||
var request = new HttpRequestMessage(HttpMethod.Get, Options.Path.Relative); | ||
|
||
AddSessionCookieToHeader(request, sessionId); | ||
AddReadOnlyHeader(request, readOnly); | ||
|
||
var response = await BackchannelClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); | ||
|
||
LogRetrieveResponse(response.StatusCode); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
|
||
var remoteSessionState = await Serializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(token), token); | ||
|
||
if (remoteSessionState is null) | ||
{ | ||
throw new InvalidOperationException("Could not retrieve remote session state"); | ||
} | ||
|
||
// Propagate headers back to the caller since a new session ID may have been set | ||
// by the remote app if there was no session active previously or if the previous | ||
// session expired. | ||
PropagateHeaders(response, callingContext, HeaderNames.SetCookie); | ||
|
||
if (remoteSessionState.IsReadOnly) | ||
{ | ||
request.Dispose(); | ||
response.Dispose(); | ||
return remoteSessionState; | ||
} | ||
|
||
return new RemoteSessionState(remoteSessionState, request, response, this); | ||
} | ||
|
||
private sealed class SerializedSessionHttpContent : HttpContent | ||
{ | ||
private readonly ISessionSerializer _serializer; | ||
private readonly ISessionState _state; | ||
|
||
public SerializedSessionHttpContent(ISessionSerializer serializer, ISessionState state) | ||
{ | ||
_serializer = serializer; | ||
_state = state; | ||
} | ||
|
||
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) | ||
=> SerializeToStreamAsync(stream, context, default); | ||
|
||
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) | ||
=> _serializer.SerializeAsync(_state, stream, cancellationToken); | ||
|
||
protected override bool TryComputeLength(out long length) | ||
{ | ||
length = 0; | ||
return false; | ||
} | ||
} | ||
|
||
private sealed class RemoteSessionState(ISessionState other, HttpRequestMessage request, HttpResponseMessage response, DoubleConnectionRemoteAppSessionManager manager) : DelegatingSessionState | ||
{ | ||
protected override ISessionState State => other; | ||
|
||
protected override void Dispose(bool disposing) | ||
{ | ||
base.Dispose(disposing); | ||
|
||
if (disposing) | ||
{ | ||
request.Dispose(); | ||
response.Dispose(); | ||
} | ||
} | ||
|
||
public override async Task CommitAsync(CancellationToken token) | ||
{ | ||
using var request = new HttpRequestMessage(HttpMethod.Put, manager.Options.Path.Relative) | ||
{ | ||
Content = new SerializedSessionHttpContent(manager.Serializer, State) | ||
}; | ||
|
||
manager.AddSessionCookieToHeader(request, State.SessionID); | ||
|
||
using var response = await manager.BackchannelClient.SendAsync(request, token); | ||
|
||
manager.LogCommitResponse(response.StatusCode); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
} | ||
} | ||
} |
111 changes: 111 additions & 0 deletions
111
...e.SystemWebAdapters.CoreServices/SessionState/RemoteSession/RemoteAppSessionDispatcher.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,111 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession; | ||
|
||
/// <summary> | ||
/// This is used to dispatch to either <see cref="DoubleConnectionRemoteAppSessionManager"/> if in read-only mode or <see cref="SingleConnectionWriteableRemoteAppSessionStateManager"/> if not. | ||
/// </summary> | ||
internal sealed partial class RemoteAppSessionDispatcher : ISessionManager | ||
{ | ||
private readonly IOptions<RemoteAppSessionStateClientOptions> _options; | ||
private readonly ISessionManager _singleConnection; | ||
private readonly ISessionManager _doubleConnection; | ||
private readonly ILogger _logger; | ||
|
||
/// <summary> | ||
/// This method is used to test the behavior of this class since it can take any ISessionManage instances. Once we drop support for .NET 6, we should be able to use keyed services | ||
/// in the DI container to achieve the same effect with just a constructor. | ||
/// </summary> | ||
public static ISessionManager Create( | ||
IOptions<RemoteAppSessionStateClientOptions> options, | ||
ISessionManager singleConnection, | ||
ISessionManager doubleConnection, | ||
ILogger logger | ||
) | ||
{ | ||
return new RemoteAppSessionDispatcher(options, singleConnection, doubleConnection, logger); | ||
} | ||
|
||
public RemoteAppSessionDispatcher( | ||
IOptions<RemoteAppSessionStateClientOptions> options, | ||
SingleConnectionWriteableRemoteAppSessionStateManager singleConnection, | ||
DoubleConnectionRemoteAppSessionManager doubleConnection, | ||
ILogger<RemoteAppSessionDispatcher> logger | ||
) | ||
: this(options, (ISessionManager)singleConnection, doubleConnection, logger) | ||
{ | ||
} | ||
|
||
private RemoteAppSessionDispatcher( | ||
IOptions<RemoteAppSessionStateClientOptions> options, | ||
ISessionManager singleConnection, | ||
ISessionManager doubleConnection, | ||
ILogger logger) | ||
{ | ||
_options = options; | ||
_singleConnection = singleConnection; | ||
_doubleConnection = doubleConnection; | ||
_logger = logger; | ||
} | ||
|
||
public async Task<ISessionState> CreateAsync(HttpContextCore context, SessionAttribute metadata) | ||
{ | ||
if (metadata.IsReadOnly) | ||
{ | ||
// In readonly mode it's a simple GET request | ||
return await _doubleConnection.CreateAsync(context, metadata); | ||
} | ||
|
||
if (_options.Value.UseSingleConnection) | ||
{ | ||
try | ||
{ | ||
return await _singleConnection.CreateAsync(context, metadata); | ||
} | ||
|
||
// We can attempt to discover if the server supports the single connection. If it doesn't, | ||
// future attempts will fallback to the double until the option value is reset. | ||
catch (HttpRequestException ex) when (ServerDoesNotSupportSingleConnection(ex)) | ||
{ | ||
LogServerDoesNotSupportSingleConnection(ex); | ||
_options.Value.UseSingleConnection = false; | ||
} | ||
catch (Exception ex) | ||
{ | ||
LogServerFailedSingelConnection(ex); | ||
throw; | ||
} | ||
} | ||
|
||
return await _doubleConnection.CreateAsync(context, metadata); | ||
} | ||
|
||
private static bool ServerDoesNotSupportSingleConnection(HttpRequestException ex) | ||
{ | ||
#if NET8_0_OR_GREATER | ||
// This is thrown when HTTP2 cannot be initiated | ||
if (ex.HttpRequestError == HttpRequestError.HttpProtocolError) | ||
{ | ||
return true; | ||
} | ||
#endif | ||
|
||
// This is thrown if the server does not know about the POST verb | ||
return ex.StatusCode == HttpStatusCode.MethodNotAllowed; | ||
} | ||
|
||
[LoggerMessage(0, LogLevel.Warning, "The server does not support the single connection mode for remote session. Falling back to double connection mode. This must be manually reset to try again.")] | ||
private partial void LogServerDoesNotSupportSingleConnection(HttpRequestException ex); | ||
|
||
[LoggerMessage(1, LogLevel.Error, "Failed to connect to server with an unknown reason")] | ||
private partial void LogServerFailedSingelConnection(Exception ex); | ||
} |
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
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.