Skip to content

Commit 85af042

Browse files
committed
Draft variants
1 parent 5b06d1d commit 85af042

File tree

5 files changed

+117
-39
lines changed

5 files changed

+117
-39
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal partial class CircuitHost : IAsyncDisposable
3333
private bool _onConnectionDownFired;
3434
private bool _disposed;
3535
private long _startTime;
36-
private PersistedCircuitState _persistedCircuitState;
36+
private PersistedCircuitState? _persistedCircuitState;
3737

3838
// This event is fired when there's an unrecoverable exception coming from the circuit, and
3939
// it need so be torn down. The registry listens to this even so that the circuit can
@@ -944,17 +944,18 @@ internal PersistedCircuitState TakePersistedCircuitState()
944944
return result;
945945
}
946946

947-
internal async Task<bool> SendPersistedStateToClient(string rootComponents, string applicationState, CancellationToken cancellation)
947+
internal async Task<bool> SendPersistedStateToClient(string rootComponents, string applicationState, DateTimeOffset expiration, CancellationToken cancellation)
948948
{
949949
try
950950
{
951-
var succeded = await Client.InvokeAsync<bool>(
951+
var succeeded = await Client.InvokeAsync<bool>(
952952
"JS.SavePersistedState",
953953
CircuitId.Secret,
954954
rootComponents,
955955
applicationState,
956+
expiration.ToUnixTimeMilliseconds(),
956957
cancellationToken: cancellation);
957-
return succeded;
958+
return succeeded;
958959
}
959960
catch (Exception ex)
960961
{

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

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@ await circuit.Renderer.Dispatcher.InvokeAsync(async () =>
2525
{
2626
var renderer = circuit.Renderer;
2727
var persistenceManager = circuit.Services.GetRequiredService<ComponentStatePersistenceManager>();
28-
var collector = new CircuitPersistenceManagerCollector(circuitOptions, serverComponentSerializer, circuit.Renderer);
28+
29+
// TODO (OR): Select solution variant
30+
// Variant B: Client-side check
31+
var distributedRetention = circuitOptions.Value.PersistedCircuitDistributedRetentionPeriod;
32+
var localRetention = circuitOptions.Value.PersistedCircuitInMemoryRetentionPeriod;
33+
var maxRetention = (distributedRetention > localRetention ? distributedRetention : localRetention) ?? ServerComponentSerializationSettings.DataExpiration;
34+
var expiration = DateTimeOffset.UtcNow.Add(maxRetention);
35+
36+
var collector = new CircuitPersistenceManagerCollector(serverComponentSerializer, circuit.Renderer, maxRetention);
37+
2938
using var subscription = persistenceManager.State.RegisterOnPersisting(
3039
collector.PersistRootComponents,
3140
RenderMode.InteractiveServer);
@@ -34,7 +43,7 @@ await circuit.Renderer.Dispatcher.InvokeAsync(async () =>
3443

3544
if (saveStateToClient)
3645
{
37-
await SaveStateToClient(circuit, collector.PersistedCircuitState, cancellation);
46+
await SaveStateToClient(circuit, collector.PersistedCircuitState, expiration, cancellation);
3847
}
3948
else
4049
{
@@ -46,10 +55,10 @@ await circuitPersistenceProvider.PersistCircuitAsync(
4655
});
4756
}
4857

49-
internal async Task SaveStateToClient(CircuitHost circuit, PersistedCircuitState state, CancellationToken cancellation = default)
58+
internal async Task SaveStateToClient(CircuitHost circuit, PersistedCircuitState state, DateTimeOffset expiration, CancellationToken cancellation = default)
5059
{
5160
var (rootComponents, applicationState) = await ToProtectedStateAsync(state);
52-
if (!await circuit.SendPersistedStateToClient(rootComponents, applicationState, cancellation))
61+
if (!await circuit.SendPersistedStateToClient(rootComponents, applicationState, expiration, cancellation))
5362
{
5463
try
5564
{
@@ -101,6 +110,27 @@ public async Task<PersistedCircuitState> ResumeCircuitAsync(CircuitId circuitId,
101110
return await circuitPersistenceProvider.RestoreCircuitAsync(circuitId, cancellation);
102111
}
103112

113+
internal static bool CheckRootComponentMarkers(IServerComponentDeserializer serverComponentDeserializer, byte[] rootComponents)
114+
{
115+
var persistedMarkers = TryDeserializeMarkers(rootComponents);
116+
117+
if (persistedMarkers == null)
118+
{
119+
return false;
120+
}
121+
122+
foreach (var marker in persistedMarkers)
123+
{
124+
if (!serverComponentDeserializer.TryDeserializeWebRootComponentDescriptor(marker.Value, out var _))
125+
{
126+
// OR: Expired state
127+
return false;
128+
}
129+
}
130+
131+
return true;
132+
}
133+
104134
// We are going to construct a RootComponentOperationBatch but we are going to replace the descriptors from the client with the
105135
// descriptors that we have persisted when pausing the circuit.
106136
// The way pausing and resuming works is that when the client starts the resume process, it 'simulates' that an SSR has happened and
@@ -152,55 +182,63 @@ internal static RootComponentOperationBatch ToRootComponentOperationBatch(
152182

153183
if (!serverComponentDeserializer.TryDeserializeWebRootComponentDescriptor(operation.Marker.Value, out var descriptor))
154184
{
185+
// OR: Expired state
155186
return null;
156187
}
157188

158189
operation.Descriptor = descriptor;
159190
}
160191

161192
return batch;
193+
}
162194

163-
static Dictionary<int, ComponentMarker> TryDeserializeMarkers(byte[] rootComponents)
195+
private static Dictionary<int, ComponentMarker> TryDeserializeMarkers(byte[] rootComponents)
196+
{
197+
if (rootComponents == null || rootComponents.Length == 0)
164198
{
165-
if (rootComponents == null || rootComponents.Length == 0)
166-
{
167-
return null;
168-
}
199+
return null;
200+
}
169201

170-
try
171-
{
172-
return JsonSerializer.Deserialize<Dictionary<int, ComponentMarker>>(
173-
rootComponents,
174-
JsonSerializerOptionsProvider.Options);
175-
}
176-
catch
177-
{
178-
return null;
179-
}
202+
try
203+
{
204+
return JsonSerializer.Deserialize<Dictionary<int, ComponentMarker>>(
205+
rootComponents,
206+
JsonSerializerOptionsProvider.Options);
207+
}
208+
catch
209+
{
210+
return null;
180211
}
181212
}
182213

183-
private class CircuitPersistenceManagerCollector(
184-
IOptions<CircuitOptions> circuitOptions,
185-
ServerComponentSerializer serverComponentSerializer,
186-
RemoteRenderer renderer)
187-
: IPersistentComponentStateStore
214+
private class CircuitPersistenceManagerCollector : IPersistentComponentStateStore
188215
{
216+
private readonly ServerComponentSerializer _serverComponentSerializer;
217+
private readonly RemoteRenderer _renderer;
218+
private readonly TimeSpan _maxRetention;
219+
220+
public CircuitPersistenceManagerCollector(
221+
ServerComponentSerializer serverComponentSerializer,
222+
RemoteRenderer renderer,
223+
TimeSpan maxRetention)
224+
{
225+
_serverComponentSerializer = serverComponentSerializer;
226+
_renderer = renderer;
227+
_maxRetention = maxRetention;
228+
}
229+
189230
internal PersistedCircuitState PersistedCircuitState { get; private set; }
190231

191232
public Task PersistRootComponents()
192233
{
193234
var persistedComponents = new Dictionary<int, ComponentMarker>();
194-
var components = renderer.GetOrCreateWebRootComponentManager().GetRootComponents();
235+
var components = _renderer.GetOrCreateWebRootComponentManager().GetRootComponents();
195236
var invocation = new ServerComponentInvocationSequence();
237+
196238
foreach (var (id, componentKey, (componentType, parameters)) in components)
197239
{
198-
var distributedRetention = circuitOptions.Value.PersistedCircuitInMemoryRetentionPeriod;
199-
var localRetention = circuitOptions.Value.PersistedCircuitInMemoryRetentionPeriod;
200-
var maxRetention = distributedRetention > localRetention ? distributedRetention : localRetention;
201-
202240
var marker = ComponentMarker.Create(ComponentMarker.ServerMarkerType, prerendered: false, componentKey);
203-
serverComponentSerializer.SerializeInvocation(ref marker, invocation, componentType, parameters, maxRetention);
241+
_serverComponentSerializer.SerializeInvocation(ref marker, invocation, componentType, parameters, _maxRetention);
204242
persistedComponents.Add(id, marker);
205243
}
206244

src/Components/Server/src/Circuits/ServerComponentDeserializer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ private bool TryDeserializeServerComponent(ComponentMarker record, out ServerCom
249249
}
250250
catch (Exception e)
251251
{
252+
// OR: Expired state
252253
Log.FailedToUnprotectDescriptor(_logger, e);
253254
result = default;
254255
return false;

src/Components/Server/src/ComponentHub.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,17 @@ public async Task UpdateRootComponents(string serializedComponentOperations, str
184184
persistedState.RootComponents,
185185
serializedComponentOperations);
186186

187+
if (operations == null)
188+
{
189+
// OR: Expired state
190+
// There was an error, so kill the circuit.
191+
await _circuitRegistry.TerminateAsync(circuitHost.CircuitId);
192+
await NotifyClientError(Clients.Caller, "The persisted circuit state is invalid or expired.");
193+
Context.Abort();
194+
195+
return;
196+
}
197+
187198
store = new ProtectedPrerenderComponentApplicationStore(persistedState.ApplicationState, _dataProtectionProvider);
188199
}
189200
else
@@ -334,6 +345,14 @@ public async ValueTask<string> ResumeCircuit(
334345
Context.Abort();
335346
return null;
336347
}
348+
349+
// TODO (OR): Select solution variant
350+
// Variant A: Server-side check in ResumeCircuit
351+
if (!CircuitPersistenceManager.CheckRootComponentMarkers(_serverComponentSerializer, persistedCircuitState.RootComponents))
352+
{
353+
Log.InvalidInputData(_logger);
354+
return null;
355+
}
337356
}
338357
else
339358
{

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ import { showErrorNotification } from '../../BootErrors';
1919
import { attachWebRendererInterop, detachWebRendererInterop } from '../../Rendering/WebRendererInteropMethods';
2020
import { sendJSDataStream } from './CircuitStreamingInterop';
2121

22+
interface PersistedCircuitState {
23+
components: string;
24+
applicationState: string;
25+
expiration: number;
26+
}
27+
2228
export class CircuitManager implements DotNet.DotNetCallDispatcher {
2329

2430
private readonly _componentManager: RootComponentManager<ServerComponentDescriptor>;
@@ -53,7 +59,7 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
5359

5460
private _disconnectingState = new CircuitState<void>('disconnecting');
5561

56-
private _persistedCircuitState?: { components: string, applicationState: string };
62+
private _persistedCircuitState?: PersistedCircuitState;
5763

5864
private _isFirstRender = true;
5965

@@ -72,6 +78,20 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
7278
this._dispatcher = DotNet.attachDispatcher(this);
7379
}
7480

81+
private tryTakePersistedState(): PersistedCircuitState | undefined {
82+
// TODO (OR): Select solution variant
83+
// Variant B: Client-side check
84+
if (this._persistedCircuitState && this._persistedCircuitState.expiration <= Date.now()) {
85+
this._logger.log(LogLevel.Debug, 'Persisted circuit state has expired and will not be used.');
86+
this._persistedCircuitState = undefined;
87+
return undefined;
88+
} else {
89+
const state = this._persistedCircuitState;
90+
this._persistedCircuitState = undefined;
91+
return state;
92+
}
93+
}
94+
7595
public start(): Promise<boolean> {
7696
if (this.isDisposedOrDisposing()) {
7797
throw new Error('Cannot start a disposed circuit.');
@@ -139,14 +159,14 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
139159
connection.on('JS.EndInvokeDotNet', this._dispatcher.endInvokeDotNetFromJS.bind(this._dispatcher));
140160
connection.on('JS.ReceiveByteArray', this._dispatcher.receiveByteArray.bind(this._dispatcher));
141161

142-
connection.on('JS.SavePersistedState', (circuitId: string, components: string, applicationState: string) => {
162+
connection.on('JS.SavePersistedState', (circuitId: string, components: string, applicationState: string, expiration: number) => {
143163
if (!this._circuitId) {
144164
throw new Error('Circuit host not initialized.');
145165
}
146166
if (circuitId !== this._circuitId) {
147167
throw new Error(`Received persisted state for circuit ID '${circuitId}', but the current circuit ID is '${this._circuitId}'.`);
148168
}
149-
this._persistedCircuitState = { components, applicationState };
169+
this._persistedCircuitState = { components, applicationState, expiration };
150170
return true;
151171
});
152172

@@ -378,8 +398,7 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
378398
}
379399
}
380400

381-
const persistedCircuitState = this._persistedCircuitState;
382-
this._persistedCircuitState = undefined;
401+
const persistedCircuitState = this.tryTakePersistedState();
383402

384403
const newCircuitId = await this._connection!.invoke<string>(
385404
'ResumeCircuit',

0 commit comments

Comments
 (0)