Skip to content

Commit 7d80971

Browse files
Stateful Reconnect API changes (#50092)
1 parent 1e2767e commit 7d80971

23 files changed

+65
-59
lines changed

src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1904,7 +1904,7 @@ public ConnectionState(ConnectionContext connection, HubConnection hubConnection
19041904
{
19051905
_messageBuffer = new MessageBuffer(connection, hubConnection._protocol,
19061906
_hubConnection._serviceProvider.GetService<IOptions<HubConnectionOptions>>()?.Value.StatefulReconnectBufferSize
1907-
?? DefaultStatefulReconnectBufferSize);
1907+
?? DefaultStatefulReconnectBufferSize);
19081908

19091909
feature.NotifyOnReconnect = _messageBuffer.Resend;
19101910
}

src/SignalR/clients/csharp/Client/src/HubConnectionBuilderHttpExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ namespace Microsoft.AspNetCore.SignalR.Client;
1919
/// </summary>
2020
public static class HubConnectionBuilderHttpExtensions
2121
{
22+
/// <summary>
23+
/// Configures the <see cref="HttpConnectionOptions"/> to negotiate stateful reconnect with the server.
24+
/// </summary>
25+
/// <param name="hubConnectionBuilder">The <see cref="IHubConnectionBuilder" /> to configure.</param>
26+
/// <returns>The same instance of the <see cref="IHubConnectionBuilder"/> for chaining.</returns>
27+
public static IHubConnectionBuilder WithStatefulReconnect(this IHubConnectionBuilder hubConnectionBuilder)
28+
{
29+
hubConnectionBuilder.Services.Configure<HttpConnectionOptions>(options => options.UseStatefulReconnect = true);
30+
31+
return hubConnectionBuilder;
32+
}
33+
2234
/// <summary>
2335
/// Configures the <see cref="HubConnection" /> to use HTTP-based transports to connect to the specified URL.
2436
/// </summary>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
static Microsoft.AspNetCore.SignalR.Client.HubConnectionBuilderHttpExtensions.WithStatefulReconnect(this Microsoft.AspNetCore.SignalR.Client.IHubConnectionBuilder! hubConnectionBuilder) -> Microsoft.AspNetCore.SignalR.Client.IHubConnectionBuilder!

src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2560,7 +2560,7 @@ public async Task CanReconnectAndSendMessageWhileDisconnected()
25602560
tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
25612561
return websocket;
25622562
};
2563-
o.UseAcks = true;
2563+
o.UseStatefulReconnect = true;
25642564
});
25652565
connectionBuilder.Services.AddSingleton(protocol);
25662566
var connection = connectionBuilder.Build();
@@ -2617,7 +2617,7 @@ public async Task CanReconnectAndSendMessageOnceConnected()
26172617
tcs.SetResult();
26182618
return websocket;
26192619
};
2620-
o.UseAcks = true;
2620+
o.UseStatefulReconnect = true;
26212621
})
26222622
.WithAutomaticReconnect();
26232623
connectionBuilder.Services.AddSingleton(protocol);
@@ -2691,8 +2691,8 @@ public async Task ChangingUserNameDuringReconnectLogsWarning()
26912691
tcs.SetResult();
26922692
return websocket;
26932693
};
2694-
o.UseAcks = true;
26952694
})
2695+
.WithStatefulReconnect()
26962696
.WithAutomaticReconnect();
26972697
connectionBuilder.Services.AddSingleton(protocol);
26982698
var connection = connectionBuilder.Build();
@@ -2756,7 +2756,7 @@ public async Task ServerAbortsConnectionWithAckingEnabledNoReconnectAttempted()
27562756
await ws.ConnectAsync(context.Uri, token);
27572757
return ws;
27582758
};
2759-
o.UseAcks = true;
2759+
o.UseStatefulReconnect = true;
27602760
});
27612761
connectionBuilder.Services.AddSingleton(protocol);
27622762
var connection = connectionBuilder.Build();
@@ -2799,12 +2799,10 @@ public async Task CanSetMessageBufferSizeOnClient()
27992799
const string originalMessage = "SignalR";
28002800
var connectionBuilder = new HubConnectionBuilder()
28012801
.WithLoggerFactory(LoggerFactory)
2802-
.WithUrl(server.Url + "/default", HttpTransportType.WebSockets, o =>
2803-
{
2804-
o.UseAcks = true;
2805-
});
2806-
connectionBuilder.Services.Configure<HubConnectionOptions>(o => o.StatefulReconnectBufferSize = 500);
2802+
.WithStatefulReconnect()
2803+
.WithUrl(server.Url + "/default", HttpTransportType.WebSockets);
28072804
connectionBuilder.Services.AddSingleton(protocol);
2805+
connectionBuilder.Services.Configure<HubConnectionOptions>(o => o.StatefulReconnectBufferSize = 500);
28082806
var connection = connectionBuilder.Build();
28092807

28102808
try

src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public void Configure(IApplicationBuilder app)
9393

9494
app.UseEndpoints(endpoints =>
9595
{
96-
endpoints.MapHub<TestHub>("/default", o => o.AllowAcks = true);
96+
endpoints.MapHub<TestHub>("/default", o => o.AllowStatefulReconnects = true);
9797
endpoints.MapHub<DynamicTestHub>("/dynamic");
9898
endpoints.MapHub<TestHubT>("/hubT");
9999
endpoints.MapHub<HubWithAuthorization>("/authorizedhub");

src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionFactoryTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void ShallowCopyHttpConnectionOptionsCopiesAllPublicProperties()
100100
{ $"{nameof(HttpConnectionOptions.WebSocketFactory)}", webSocketFactory },
101101
{ $"{nameof(HttpConnectionOptions.ApplicationMaxBufferSize)}", 1L * 1024 * 1024 },
102102
{ $"{nameof(HttpConnectionOptions.TransportMaxBufferSize)}", 1L * 1024 * 1024 },
103-
{ $"{nameof(HttpConnectionOptions.UseAcks)}", true },
103+
{ $"{nameof(HttpConnectionOptions.UseStatefulReconnect)}", true },
104104
};
105105

106106
var options = new HttpConnectionOptions();

src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,13 +399,13 @@ private async Task SelectAndStartTransport(TransferFormat transferFormat, Cancel
399399
if (negotiationResponse == null)
400400
{
401401
// Temporary until other transports work
402-
_httpConnectionOptions.UseAcks = transportType == HttpTransportType.WebSockets ? _httpConnectionOptions.UseAcks : false;
402+
_httpConnectionOptions.UseStatefulReconnect = transportType == HttpTransportType.WebSockets ? _httpConnectionOptions.UseStatefulReconnect : false;
403403
negotiationResponse = await GetNegotiationResponseAsync(uri, cancellationToken).ConfigureAwait(false);
404404
connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionToken);
405405
}
406406

407407
Log.StartingTransport(_logger, transportType, uri);
408-
await StartTransport(connectUrl, transportType, transferFormat, cancellationToken, negotiationResponse.UseAcking).ConfigureAwait(false);
408+
await StartTransport(connectUrl, transportType, transferFormat, cancellationToken, negotiationResponse.UseStatefulReconnect).ConfigureAwait(false);
409409
break;
410410
}
411411
}
@@ -457,7 +457,7 @@ private async Task<NegotiationResponse> NegotiateAsync(Uri url, HttpClient httpC
457457
uri = Utils.AppendQueryString(urlBuilder.Uri, $"negotiateVersion={_protocolVersionNumber}");
458458
}
459459

460-
if (_httpConnectionOptions.UseAcks)
460+
if (_httpConnectionOptions.UseStatefulReconnect)
461461
{
462462
uri = Utils.AppendQueryString(uri, "useAck=true");
463463
}

src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ internal static HttpConnectionOptions ShallowCopyHttpConnectionOptions(HttpConne
8989
DefaultTransferFormat = options.DefaultTransferFormat,
9090
ApplicationMaxBufferSize = options.ApplicationMaxBufferSize,
9191
TransportMaxBufferSize = options.TransportMaxBufferSize,
92-
UseAcks = options.UseAcks,
92+
UseStatefulReconnect = options.UseStatefulReconnect,
9393
};
9494

9595
if (!OperatingSystem.IsBrowser())

src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionOptions.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,13 @@ public Action<ClientWebSocketOptions>? WebSocketConfiguration
276276
}
277277

278278
/// <summary>
279-
/// Setting to enable acking bytes sent between client and server, this allows reconnecting that preserves messages sent while disconnected.
279+
/// Setting to enable Stateful Reconnect between client and server, this allows reconnecting that preserves messages sent while disconnected.
280280
/// Also preserves the <see cref="HttpConnection.ConnectionId"/> when the reconnect is successful.
281281
/// </summary>
282282
/// <remarks>
283283
/// Only works with WebSockets transport currently.
284-
/// API likely to change in future previews.
285284
/// </remarks>
286-
public bool UseAcks { get; set; }
285+
public bool UseStatefulReconnect { get; set; }
287286

288287
private static void ThrowIfUnsupportedPlatform()
289288
{

src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ internal sealed partial class WebSocketsTransport : ITransport, IReconnectFeatur
3636
private readonly HttpConnectionOptions _httpConnectionOptions;
3737
private readonly HttpClient? _httpClient;
3838
private CancellationTokenSource _stopCts = default!;
39-
private readonly bool _useAck;
39+
private readonly bool _useStatefulReconnect;
4040

4141
private IDuplexPipe? _transport;
4242
// Used for reconnect (when enabled) to determine if the close was ungraceful or not, reconnect only happens on ungraceful disconnect
@@ -53,9 +53,9 @@ internal sealed partial class WebSocketsTransport : ITransport, IReconnectFeatur
5353
public Action NotifyOnReconnect { get => _notifyOnReconnect is not null ? _notifyOnReconnect : () => { }; set => _notifyOnReconnect = value; }
5454

5555
public WebSocketsTransport(HttpConnectionOptions httpConnectionOptions, ILoggerFactory loggerFactory, Func<Task<string?>> accessTokenProvider, HttpClient? httpClient,
56-
bool useAck = false)
56+
bool useStatefulReconnect = false)
5757
{
58-
_useAck = useAck;
58+
_useStatefulReconnect = useStatefulReconnect;
5959
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<WebSocketsTransport>();
6060
_httpConnectionOptions = httpConnectionOptions ?? new HttpConnectionOptions();
6161

@@ -368,7 +368,7 @@ private async Task ProcessSocketAsync(WebSocket socket, Uri url, bool ignoreFirs
368368
}
369369
}
370370

371-
if (_useAck && !_gracefulClose)
371+
if (_useStatefulReconnect && !_gracefulClose)
372372
{
373373
UpdateConnectionPair();
374374
await StartAsync(url, _webSocketMessageType == WebSocketMessageType.Binary ? TransferFormat.Binary : TransferFormat.Text, default).ConfigureAwait(false);
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#nullable enable
2-
Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions.UseAcks.get -> bool
3-
Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions.UseAcks.set -> void
2+
Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions.UseStatefulReconnect.get -> bool
3+
Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions.UseStatefulReconnect.set -> void

src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public static void WriteResponse(NegotiationResponse response, IBufferWriter<byt
6666
return;
6767
}
6868

69-
if (response.UseAcking)
69+
if (response.UseStatefulReconnect)
7070
{
7171
writer.WriteBoolean(AckPropertyNameBytes, true);
7272
}
@@ -262,7 +262,7 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan<byte> content)
262262
AvailableTransports = availableTransports,
263263
Error = error,
264264
Version = version,
265-
UseAcking = useAck,
265+
UseStatefulReconnect = useAck,
266266
};
267267
}
268268
catch (Exception ex)

src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,5 @@ public class NegotiationResponse
5252
/// It should also set <see cref="IReconnectFeature"/> on the <see cref="BaseConnectionContext.Features"/> collection so other layers of the
5353
/// application (like SignalR) can react.
5454
/// </summary>
55-
public bool UseAcking { get; set; }
55+
public bool UseStatefulReconnect { get; set; }
5656
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#nullable enable
2-
Microsoft.AspNetCore.Http.Connections.NegotiationResponse.UseAcking.get -> bool
3-
Microsoft.AspNetCore.Http.Connections.NegotiationResponse.UseAcking.set -> void
2+
Microsoft.AspNetCore.Http.Connections.NegotiationResponse.UseStatefulReconnect.get -> bool
3+
Microsoft.AspNetCore.Http.Connections.NegotiationResponse.UseStatefulReconnect.set -> void

src/SignalR/common/Http.Connections/src/HttpConnectionDispatcherOptions.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.IO.Pipelines;
55
using Microsoft.AspNetCore.Authentication;
66
using Microsoft.AspNetCore.Authorization;
7+
using Microsoft.AspNetCore.Connections;
78

89
namespace Microsoft.AspNetCore.Http.Connections;
910

@@ -125,12 +126,12 @@ public TimeSpan TransportSendTimeout
125126
public bool CloseOnAuthenticationExpiration { get; set; }
126127

127128
/// <summary>
128-
/// Set to allow connections to ack messages, helps enable reconnects that keep connection state.
129+
/// Set to allow connections to reconnect with the same <see cref="BaseConnectionContext.ConnectionId"/>.
129130
/// </summary>
130131
/// <remarks>
131-
/// Keeps messages in memory until acked (up to a limit), and keeps connections around for a short time to allow stateful reconnects.
132+
/// Client still has to negotiate this option.
132133
/// </remarks>
133-
public bool AllowAcks { get; set; }
134+
public bool AllowStatefulReconnects { get; set; }
134135

135136
internal long TransportSendTimeoutTicks { get; private set; }
136137
internal bool TransportSendTimeoutEnabled => _transportSendTimeout != Timeout.InfiniteTimeSpan;

src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ internal sealed partial class HttpConnectionContext : ConnectionContext,
4949
private CancellationTokenSource? _sendCts;
5050
private bool _activeSend;
5151
private long _startedSendTime;
52-
private readonly bool _useAcks;
52+
private readonly bool _useStatefulReconnect;
5353
private readonly object _sendingLock = new object();
5454
internal CancellationToken SendingToken { get; private set; }
5555

@@ -105,10 +105,10 @@ public HttpConnectionContext(string connectionId, string connectionToken, ILogge
105105
_connectionCloseRequested = new CancellationTokenSource();
106106
ConnectionClosedRequested = _connectionCloseRequested.Token;
107107
AuthenticationExpiration = DateTimeOffset.MaxValue;
108-
_useAcks = useAcks;
108+
_useStatefulReconnect = useAcks;
109109
}
110110

111-
public bool UseAcks => _useAcks;
111+
public bool UseStatefulReconnect => _useStatefulReconnect;
112112

113113
public CancellationTokenSource? Cancellation { get; set; }
114114

@@ -548,7 +548,7 @@ internal async Task<bool> CancelPreviousPoll(HttpContext context)
548548
cts?.Cancel();
549549

550550
// TODO: remove transport check once other transports support acks
551-
if (UseAcks && TransportType == HttpTransportType.WebSockets)
551+
if (UseStatefulReconnect && TransportType == HttpTransportType.WebSockets)
552552
{
553553
// Break transport send loop in case it's still waiting on reading from the application
554554
Application.Input.CancelPendingRead();

src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti
189189
return;
190190
}
191191

192-
if (connection.TransportType != HttpTransportType.WebSockets || connection.UseAcks)
192+
if (connection.TransportType != HttpTransportType.WebSockets || connection.UseStatefulReconnect)
193193
{
194194
if (!await connection.CancelPreviousPoll(context))
195195
{
@@ -336,7 +336,7 @@ private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatche
336336
}
337337

338338
var useAck = false;
339-
if (options.AllowAcks == true && context.Request.Query.TryGetValue("UseAck", out var useAckValue))
339+
if (options.AllowStatefulReconnects == true && context.Request.Query.TryGetValue("UseAck", out var useAckValue))
340340
{
341341
var useAckStringValue = useAckValue.ToString();
342342
bool.TryParse(useAckStringValue, out useAck);
@@ -389,7 +389,7 @@ private static void WriteNegotiatePayload(IBufferWriter<byte> writer, string? co
389389
response.ConnectionId = connectionId;
390390
response.ConnectionToken = connectionToken;
391391
response.AvailableTransports = new List<AvailableTransport>();
392-
response.UseAcking = useAck;
392+
response.UseStatefulReconnect = useAck;
393393

394394
if ((options.Transports & HttpTransportType.WebSockets) != 0 && ServerHasWebSockets(context.Features))
395395
{
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#nullable enable
2-
Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions.AllowAcks.get -> bool
3-
Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions.AllowAcks.set -> void
2+
Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions.AllowStatefulReconnects.get -> bool
3+
Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions.AllowStatefulReconnects.set -> void

src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2279,7 +2279,7 @@ public async Task NegotiateDoesNotReturnUseAckWhenNotEnabledOnServer()
22792279
context.Request.Method = "POST";
22802280
context.Response.Body = ms;
22812281
context.Request.QueryString = new QueryString("?negotiateVersion=1&UseAck=true");
2282-
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { AllowAcks = false });
2282+
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { AllowStatefulReconnects = false });
22832283

22842284
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
22852285
Assert.False(negotiateResponse.TryGetValue("useAck", out _));
@@ -2306,7 +2306,7 @@ public async Task NegotiateDoesNotReturnUseAckWhenEnabledOnServerButNotRequested
23062306
context.Request.Method = "POST";
23072307
context.Response.Body = ms;
23082308
context.Request.QueryString = new QueryString("?negotiateVersion=1");
2309-
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { AllowAcks = true });
2309+
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { AllowStatefulReconnects = true });
23102310

23112311
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
23122312
Assert.False(negotiateResponse.TryGetValue("useAck", out _));
@@ -2333,7 +2333,7 @@ public async Task NegotiateReturnsUseAckWhenEnabledOnServerAndRequestedByClient(
23332333
context.Request.Method = "POST";
23342334
context.Response.Body = ms;
23352335
context.Request.QueryString = new QueryString("?negotiateVersion=1&UseAck=true");
2336-
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { AllowAcks = true });
2336+
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { AllowStatefulReconnects = true });
23372337

23382338
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
23392339
Assert.True((bool)negotiateResponse["useAck"]);

src/SignalR/perf/Microbenchmarks/DefaultHubDispatcherBenchmark.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public void GlobalSetup()
3535
new HubContext<TestHub>(hubLifetimeManager),
3636
enableDetailedErrors: false,
3737
disableImplicitFromServiceParameters: true,
38-
useAcks: false,
3938
new Logger<DefaultHubDispatcher<TestHub>>(NullLoggerFactory.Instance),
4039
hubFilters: null,
4140
hubLifetimeManager);

0 commit comments

Comments
 (0)