Skip to content

Commit cdf354e

Browse files
committed
stuff
1 parent b62d0f8 commit cdf354e

File tree

12 files changed

+207
-17
lines changed

12 files changed

+207
-17
lines changed

src/Components/Server/src/Circuits/CircuitClientProxy.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55

66
namespace Microsoft.AspNetCore.Components.Server.Circuits;
77

8-
internal sealed class CircuitClientProxy : IClientProxy
8+
internal sealed class CircuitClientProxy : ISingleClientProxy
99
{
1010
public CircuitClientProxy()
1111
{
1212
Connected = false;
1313
}
1414

15-
public CircuitClientProxy(IClientProxy clientProxy, string connectionId)
15+
public CircuitClientProxy(ISingleClientProxy clientProxy, string connectionId)
1616
{
1717
Transfer(clientProxy, connectionId);
1818
}
@@ -21,9 +21,9 @@ public CircuitClientProxy(IClientProxy clientProxy, string connectionId)
2121

2222
public string ConnectionId { get; private set; }
2323

24-
public IClientProxy Client { get; private set; }
24+
public ISingleClientProxy Client { get; private set; }
2525

26-
public void Transfer(IClientProxy clientProxy, string connectionId)
26+
public void Transfer(ISingleClientProxy clientProxy, string connectionId)
2727
{
2828
Client = clientProxy ?? throw new ArgumentNullException(nameof(clientProxy));
2929
ConnectionId = connectionId ?? throw new ArgumentNullException(nameof(connectionId));

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,11 @@ internal PersistedCircuitState TakePersistedCircuitState()
891891
return result;
892892
}
893893

894+
internal async Task<bool> SendPersistedStateToClient(string rootComponents, string applicationState)
895+
{
896+
return Client.SendAsync("JS.SavePersistedState", rootComponents, applicationState);
897+
}
898+
894899
private static partial class Log
895900
{
896901
// 100s used for lifecycle stuff

src/Components/Server/src/Circuits/CircuitPersistenceManager.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal partial class CircuitPersistenceManager(
1919
IDataProtectionProvider dataProtectionProvider)
2020
{
2121

22-
public async Task PauseCircuitAsync(CircuitHost circuit, CancellationToken cancellation = default)
22+
public async Task PauseCircuitAsync(CircuitHost circuit, bool saveStateToClient, CancellationToken cancellation = default)
2323
{
2424
var renderer = circuit.Renderer;
2525
var persistenceManager = circuit.Services.GetRequiredService<ComponentStatePersistenceManager>();
@@ -29,12 +29,51 @@ public async Task PauseCircuitAsync(CircuitHost circuit, CancellationToken cance
2929
var store = new CircuitPersistenceManagerStore();
3030
await persistenceManager.PersistStateAsync(store, renderer);
3131

32+
if (saveStateToClient)
33+
{
34+
_ = SaveStateToClient(circuit, store.PersistedCircuitState, cancellation);
35+
}
36+
3237
await circuitPersistenceProvider.PersistCircuitAsync(
3338
circuit.CircuitId,
3439
store.PersistedCircuitState,
3540
cancellation);
3641
}
3742

43+
private async Task SaveStateToClient(CircuitHost circuit, PersistedCircuitState state, CancellationToken cancellation = default)
44+
{
45+
var (rootComponents, applicationState) = ToProtectedState(state);
46+
try
47+
{
48+
// Try to push the state to the client first, if that fails, we will persist it in the server-side store.
49+
bool succeded = await circuit.SendPersistedStateToClient(rootComponents, applicationState);
50+
if (!succeded)
51+
{
52+
try
53+
{
54+
await circuitPersistenceProvider.PersistCircuitAsync(
55+
circuit.CircuitId,
56+
state,
57+
cancellation);
58+
}
59+
catch (Exception)
60+
{
61+
// At this point, we give up as we haven't been able to save the state to the client nor the server.
62+
return;
63+
}
64+
}
65+
}
66+
catch (Exception)
67+
{
68+
// The call to save the state to the client failed, so fallback to saving it on the server.
69+
// The client can still try to resume later without the state.
70+
await circuitPersistenceProvider.PersistCircuitAsync(
71+
circuit.CircuitId,
72+
state,
73+
cancellation);
74+
}
75+
}
76+
3877
private Task PersistRootComponents(RemoteRenderer renderer, PersistentComponentState state)
3978
{
4079
var persistedComponents = new Dictionary<int, ComponentMarker>();
@@ -84,7 +123,7 @@ internal static RootComponentOperationBatch ToRootComponentOperationBatch(
84123
return null;
85124
}
86125

87-
var data = JsonSerializer.Deserialize<Dictionary<int,ComponentMarker>>(
126+
var data = JsonSerializer.Deserialize<Dictionary<int, ComponentMarker>>(
88127
rootComponents,
89128
JsonSerializerOptionsProvider.Options);
90129

src/Components/Server/src/Circuits/CircuitRegistry.cs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,16 +287,22 @@ private async Task PauseAndDisposeCircuitEntry(DisconnectedCircuitEntry entry)
287287

288288
try
289289
{
290-
await _circuitPersistenceManager.PauseCircuitAsync(entry.CircuitHost);
291-
entry.CircuitHost.UnhandledException -= CircuitHost_UnhandledException;
292-
await entry.CircuitHost.DisposeAsync();
290+
var circuitHost = entry.CircuitHost;
291+
await PauseAndDisposeCircuitHost(circuitHost, saveStateToClient: false);
293292
}
294293
catch (Exception ex)
295294
{
296295
Log.UnhandledExceptionDisposingCircuitHost(_logger, ex);
297296
}
298297
}
299298

299+
private async Task PauseAndDisposeCircuitHost(CircuitHost circuitHost, bool saveStateToClient)
300+
{
301+
await _circuitPersistenceManager.PauseCircuitAsync(circuitHost, saveStateToClient);
302+
circuitHost.UnhandledException -= CircuitHost_UnhandledException;
303+
await circuitHost.DisposeAsync();
304+
}
305+
300306
private void DisposeTokenSource(DisconnectedCircuitEntry entry)
301307
{
302308
try
@@ -353,6 +359,62 @@ private async void CircuitHost_UnhandledException(object sender, UnhandledExcept
353359
}
354360
}
355361

362+
internal Task PauseCircuitAsync(
363+
CircuitHost circuitHost,
364+
string connectionId)
365+
{
366+
Log.CircuitPauseStarted(_logger, circuitHost.CircuitId, connectionId);
367+
368+
Task circuitHandlerTask;
369+
lock (CircuitRegistryLock)
370+
{
371+
if (PauseCore(circuitHost, connectionId))
372+
{
373+
circuitHandlerTask = circuitHost.Renderer.Dispatcher.InvokeAsync(() => circuitHost.OnConnectionDownAsync(default));
374+
}
375+
else
376+
{
377+
// DisconnectCore may fail to disconnect the circuit if it was previously marked inactive or
378+
// has been transferred to a new connection. Do not invoke the circuit handlers in this instance.
379+
380+
// We have to do in this instance.
381+
return Task.CompletedTask;
382+
}
383+
}
384+
385+
return circuitHandlerTask;
386+
}
387+
388+
internal bool PauseCore(CircuitHost circuitHost, string connectionId)
389+
{
390+
var circuitId = circuitHost.CircuitId;
391+
if (!ConnectedCircuits.TryGetValue(circuitId, out circuitHost))
392+
{
393+
Log.CircuitNotActive(_logger, circuitId);
394+
395+
// Circuit should be in the connected state for pausing.
396+
return false;
397+
}
398+
399+
if (!string.Equals(circuitHost.Client.ConnectionId, connectionId, StringComparison.Ordinal))
400+
{
401+
// Circuit should be connected to the same connection for pausing.
402+
Log.CircuitConnectedToDifferentConnection(_logger, circuitId, circuitHost.Client.ConnectionId);
403+
404+
// The circuit is associated with a different connection. One way this could happen is when
405+
// the client reconnects with a new connection before the OnDisconnect for the older
406+
// connection is executed. Do nothing
407+
return false;
408+
}
409+
410+
var removeResult = ConnectedCircuits.TryRemove(circuitId, out _);
411+
Debug.Assert(removeResult, "This operation operates inside of a lock. We expect the previously inspected value to be still here.");
412+
413+
_ = PauseAndDisposeCircuitHost(circuitHost, saveStateToClient: true);
414+
415+
return removeResult;
416+
}
417+
356418
private readonly struct DisconnectedCircuitEntry
357419
{
358420
public DisconnectedCircuitEntry(CircuitHost circuitHost, CancellationTokenSource tokenSource)
@@ -417,5 +479,8 @@ public static void ExceptionDisposingTokenSource(ILogger logger, Exception excep
417479

418480
[LoggerMessage(115, LogLevel.Debug, "Reconnect to circuit with id {CircuitId} succeeded.", EventName = "ReconnectionSucceeded")]
419481
public static partial void ReconnectionSucceeded(ILogger logger, CircuitId circuitId);
482+
483+
[LoggerMessage(116, LogLevel.Debug, "Pausing circuit with id {CircuitId} from connection {ConnectionId}.", EventName = "CircuitPauseStarted")]
484+
public static partial void CircuitPauseStarted(ILogger logger, CircuitId circuitId, string connectionId);
420485
}
421486
}

src/Components/Server/src/ComponentHub.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,27 @@ public async ValueTask<bool> ConnectCircuit(string circuitIdSecret)
238238
return false;
239239
}
240240

241+
public async ValueTask<string> PauseCircuit()
242+
{
243+
var circuitHost = await GetActiveCircuitAsync();
244+
if (circuitHost == null)
245+
{
246+
return null;
247+
}
248+
249+
// What needs to happen
250+
// Disconnect the circuit
251+
// Evict the circuit from the registry
252+
// Capture the state during eviction
253+
// Return the state to the client
254+
// Close the connection (or have the client do it)
255+
256+
// Dissociate the circuit from the connection. From this point any new calls to the hub will fail to find the circuit.
257+
_circuitHandleRegistry.SetCircuit(Context.Items, CircuitKey, circuitHost: null);
258+
_ = _circuitRegistry.PauseCircuitAsync(circuitHost, Context.ConnectionId);
259+
return null;
260+
}
261+
241262
// This method drives the resumption of a circuit that has been previously paused and ejected out of memory.
242263
// Resuming a circuit is very similar to starting a new circuit.
243264
// We receive an existing circuit ID to look up the existing circuit state.
@@ -323,12 +344,13 @@ public async ValueTask<string> ResumeCircuit(
323344
return null;
324345
}
325346
}
326-
else if (!string.IsNullOrEmpty(rootComponents) || !string.IsNullOrEmpty(applicationState))
347+
else if ((rootComponents != "[]" && string.IsNullOrEmpty(applicationState)) ||
348+
(rootComponents == "[]" && !string.IsNullOrEmpty(applicationState)))
327349
{
328350
Log.InvalidInputData(_logger);
329351
await NotifyClientError(
330352
Clients.Caller,
331-
string.IsNullOrEmpty(rootComponents) ?
353+
rootComponents != "[]" ?
332354
"The root components provided are invalid." :
333355
"The application state provided is invalid."
334356
);

src/Components/Web.JS/src/Boot.Server.Common.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ async function startServerCore(components: RootComponentManager<ServerComponentD
6767
return true;
6868
};
6969

70+
Blazor.pause = async () => {
71+
if (circuit.didRenderingFail()) {
72+
// We can't pause after a failure, so exit early.
73+
return false;
74+
}
75+
76+
if (!(await circuit.pause())) {
77+
logger.log(LogLevel.Information, 'Pause attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server.');
78+
return false;
79+
}
80+
81+
return true;
82+
};
83+
7084
Blazor.resume = async () => {
7185
if (circuit.didRenderingFail()) {
7286
// We can't resume after a failure, so exit early.
@@ -81,6 +95,7 @@ async function startServerCore(components: RootComponentManager<ServerComponentD
8195
return true;
8296
};
8397

98+
8499
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
85100
options.reconnectionHandler = options.reconnectionHandler || Blazor.defaultReconnectionHandler;
86101

src/Components/Web.JS/src/GlobalExports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface IBlazor {
3838
removeEventListener?: typeof JSEventRegistry.prototype.removeEventListener;
3939
disconnect?: () => void;
4040
reconnect?: (existingConnection?: HubConnection) => Promise<boolean>;
41+
pause?: (existingConnection?: HubConnection) => Promise<boolean>;
4142
resume?: (existingConnection?: HubConnection) => Promise<boolean>;
4243
defaultReconnectionHandler?: DefaultReconnectionHandler;
4344
start?: ((userOptions?: Partial<CircuitStartOptions>) => Promise<void>) | ((options?: Partial<WebAssemblyStartOptions>) => Promise<void>) | ((options?: Partial<WebStartOptions>) => Promise<void>);

src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
4949

5050
private _disposed = false;
5151

52+
private _persistedCircuitState?: { components: string, applicationState: string };
53+
5254
public constructor(
5355
componentManager: RootComponentManager<ServerComponentDescriptor>,
5456
appState: string,
@@ -235,17 +237,25 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
235237

236238
public async resume(): Promise<boolean> {
237239
if (!this._circuitId) {
238-
throw new Error('Method not implemented.');
240+
throw new Error('Circuit host not initialized.');
239241
}
240242

243+
if (this._connection!.state !== HubConnectionState.Connected) {
244+
return false;
245+
}
246+
247+
const persistedCircuitState = this._persistedCircuitState;
248+
this._persistedCircuitState = undefined;
249+
241250
const resume = await this._connection!.invoke<string>(
242251
'ResumeCircuit',
243252
this._circuitId,
244253
navigationManagerFunctions.getBaseURI(),
245254
navigationManagerFunctions.getLocationHref(),
246-
'[]',
247-
''
255+
persistedCircuitState?.components ?? '[]',
256+
persistedCircuitState?.applicationState ?? '',
248257
);
258+
249259
if (!resume) {
250260
return false;
251261
}
@@ -254,10 +264,34 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
254264
this._renderQueue = new RenderQueue(this._logger);
255265
this._options.reconnectionHandler!.onCircuitResumed();
256266
this._options.reconnectionHandler!.onConnectionUp();
257-
this._componentManager.onComponentReload?.();
267+
this._componentManager.onComponentReload?.(WebRendererId.Server);
258268
return true;
259269
}
260270

271+
public async pause() {
272+
if (!this._circuitId) {
273+
throw new Error('Method not implemented.');
274+
}
275+
276+
if (this._connection!.state !== HubConnectionState.Connected) {
277+
return false;
278+
}
279+
280+
const pauseResult = await this._connection!.invoke<string>(
281+
'PauseCircuit',
282+
this._circuitId,
283+
);
284+
285+
if (!pauseResult) {
286+
return false;
287+
}
288+
289+
this._persistedCircuitState = JSON.parse(pauseResult);
290+
291+
this._options.reconnectionHandler!.onCircuitPaused();
292+
}
293+
294+
261295
// Implements DotNet.DotNetCallDispatcher
262296
public beginInvokeDotNetFromJS(callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): void {
263297
this.throwIfDispatchingWhenDisposed();

src/Components/Web.JS/src/Platform/Circuits/CircuitStartOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface ReconnectionHandler {
4848
onConnectionDown(options: ReconnectionOptions, error?: Error): void;
4949
onConnectionUp(): void;
5050
onCircuitResumed(): void;
51+
onCircuitPaused(): void;
5152
}
5253

5354
function computeDefaultRetryInterval(previousAttempts: number, maxRetries?: number): number | null {

src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectionHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export class DefaultReconnectionHandler implements ReconnectionHandler {
5555
onCircuitResumed(): void {
5656
return;
5757
}
58+
59+
onCircuitPaused(): void {
60+
return;
61+
}
5862
}
5963

6064
class ReconnectionProcess {

0 commit comments

Comments
 (0)