@fluidframework/container-loader 1.2.2 → 2.0.0-internal.1.0.0.81589
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audience.d.ts +2 -2
- package/dist/audience.d.ts.map +1 -1
- package/dist/audience.js.map +1 -1
- package/dist/catchUpMonitor.d.ts +40 -0
- package/dist/catchUpMonitor.d.ts.map +1 -0
- package/dist/catchUpMonitor.js +74 -0
- package/dist/catchUpMonitor.js.map +1 -0
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +0 -1
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionState.d.ts +0 -5
- package/dist/connectionState.d.ts.map +1 -1
- package/dist/connectionState.js +0 -5
- package/dist/connectionState.js.map +1 -1
- package/dist/connectionStateHandler.d.ts +12 -4
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +47 -15
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +8 -6
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +82 -46
- package/dist/container.js.map +1 -1
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +6 -6
- package/dist/deltaManager.js.map +1 -1
- package/dist/deltaManagerProxy.d.ts +4 -1
- package/dist/deltaManagerProxy.d.ts.map +1 -1
- package/dist/deltaQueue.d.ts +9 -2
- package/dist/deltaQueue.d.ts.map +1 -1
- package/dist/deltaQueue.js +31 -26
- package/dist/deltaQueue.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +7 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +4 -3
- package/dist/loader.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/protocol.d.ts +22 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +52 -0
- package/dist/protocol.js.map +1 -0
- package/lib/audience.d.ts +2 -2
- package/lib/audience.d.ts.map +1 -1
- package/lib/audience.js.map +1 -1
- package/lib/catchUpMonitor.d.ts +40 -0
- package/lib/catchUpMonitor.d.ts.map +1 -0
- package/lib/catchUpMonitor.js +69 -0
- package/lib/catchUpMonitor.js.map +1 -0
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +0 -1
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionState.d.ts +0 -5
- package/lib/connectionState.d.ts.map +1 -1
- package/lib/connectionState.js +0 -5
- package/lib/connectionState.js.map +1 -1
- package/lib/connectionStateHandler.d.ts +12 -4
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +47 -15
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +8 -6
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +82 -46
- package/lib/container.js.map +1 -1
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +6 -6
- package/lib/deltaManager.js.map +1 -1
- package/lib/deltaManagerProxy.d.ts +4 -1
- package/lib/deltaManagerProxy.d.ts.map +1 -1
- package/lib/deltaQueue.d.ts +9 -2
- package/lib/deltaQueue.d.ts.map +1 -1
- package/lib/deltaQueue.js +32 -27
- package/lib/deltaQueue.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/loader.d.ts +7 -0
- package/lib/loader.d.ts.map +1 -1
- package/lib/loader.js +4 -3
- package/lib/loader.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/protocol.d.ts +22 -0
- package/lib/protocol.d.ts.map +1 -0
- package/lib/protocol.js +48 -0
- package/lib/protocol.js.map +1 -0
- package/package.json +23 -15
- package/src/audience.ts +2 -2
- package/src/catchUpMonitor.ts +99 -0
- package/src/connectionManager.ts +0 -1
- package/src/connectionState.ts +0 -6
- package/src/connectionStateHandler.ts +55 -15
- package/src/container.ts +115 -63
- package/src/deltaManager.ts +6 -4
- package/src/deltaQueue.ts +34 -28
- package/src/index.ts +4 -0
- package/src/loader.ts +13 -2
- package/src/packageVersion.ts +1 -1
- package/src/protocol.ts +96 -0
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { ITelemetryLogger, ITelemetryProperties } from "@fluidframework/common-definitions";
|
|
7
7
|
import { assert, Timer } from "@fluidframework/common-utils";
|
|
8
|
-
import { IConnectionDetails } from "@fluidframework/container-definitions";
|
|
8
|
+
import { IConnectionDetails, IDeltaManager } from "@fluidframework/container-definitions";
|
|
9
9
|
import { ILocalSequencedClient, IProtocolHandler } from "@fluidframework/protocol-base";
|
|
10
10
|
import { ConnectionMode, IQuorumClients } from "@fluidframework/protocol-definitions";
|
|
11
11
|
import { PerformanceEvent } from "@fluidframework/telemetry-utils";
|
|
12
12
|
import { ConnectionState } from "./connectionState";
|
|
13
|
+
import { CatchUpMonitor, ICatchUpMonitor, ImmediateCatchUpMonitor } from "./catchUpMonitor";
|
|
13
14
|
|
|
14
15
|
/** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */
|
|
15
16
|
export interface IConnectionStateHandlerInputs {
|
|
@@ -36,7 +37,7 @@ const JoinOpTimeoutMs = 45000;
|
|
|
36
37
|
* sequenced or blocked by the server before emitting the new "connected" event and allowing runtime to resubmit ops.
|
|
37
38
|
*
|
|
38
39
|
* Each connection is assigned a clientId by the service, and the connection is book-ended by a Join and a Leave op
|
|
39
|
-
* generated by the service. Due to the distributed nature of the
|
|
40
|
+
* generated by the service. Due to the distributed nature of the Relay Service, in the case of reconnect we cannot
|
|
40
41
|
* make any assumptions about ordering of operations between the old and new connections - i.e. new Join op could
|
|
41
42
|
* be sequenced before old Leave op (and some acks from pending ops that were in flight when we disconnected).
|
|
42
43
|
*
|
|
@@ -47,14 +48,17 @@ const JoinOpTimeoutMs = 45000;
|
|
|
47
48
|
* pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.
|
|
48
49
|
* (B) We process the Join op for the new clientId (identified when the underlying connection was first established),
|
|
49
50
|
* indicating the service is ready to sequence ops sent with the new clientId.
|
|
51
|
+
* (C) We process all ops known at the time the underlying connection was established (so we are "caught up")
|
|
50
52
|
*
|
|
51
53
|
* For (A) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.
|
|
52
54
|
* For (B) we log telemetry if it takes too long, but still only transition to Connected when the Join op is processed
|
|
53
55
|
* and we are added to the Quorum.
|
|
56
|
+
* For (C) this is optional behavior, controlled by the parameters of receivedConnectEvent
|
|
54
57
|
*/
|
|
55
58
|
export class ConnectionStateHandler {
|
|
56
59
|
private _connectionState = ConnectionState.Disconnected;
|
|
57
60
|
private _pendingClientId: string | undefined;
|
|
61
|
+
private catchUpMonitor: ICatchUpMonitor | undefined;
|
|
58
62
|
private readonly prevClientLeftTimer: Timer;
|
|
59
63
|
private readonly joinOpTimer: Timer;
|
|
60
64
|
|
|
@@ -106,7 +110,7 @@ export class ConnectionStateHandler {
|
|
|
106
110
|
const quorumClients = this.handler.quorumClients();
|
|
107
111
|
const details = {
|
|
108
112
|
quorumInitialized: quorumClients !== undefined,
|
|
109
|
-
|
|
113
|
+
pendingClientId: this.pendingClientId,
|
|
110
114
|
inQuorum: quorumClients?.getMember(this.pendingClientId ?? "") !== undefined,
|
|
111
115
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
112
116
|
};
|
|
@@ -184,7 +188,10 @@ export class ConnectionStateHandler {
|
|
|
184
188
|
&& !this.waitingForLeaveOp
|
|
185
189
|
) {
|
|
186
190
|
this.waitEvent?.end({ source });
|
|
187
|
-
|
|
191
|
+
|
|
192
|
+
assert(this.catchUpMonitor !== undefined,
|
|
193
|
+
"catchUpMonitor should always be set if pendingClientId is set");
|
|
194
|
+
this.catchUpMonitor.on("caughtUp", this.transitionToConnectedState);
|
|
188
195
|
} else {
|
|
189
196
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
190
197
|
this.logger.sendTelemetryEvent({
|
|
@@ -210,32 +217,47 @@ export class ConnectionStateHandler {
|
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
public receivedDisconnectEvent(reason: string) {
|
|
213
|
-
if (this.joinOpTimer.hasTimer) {
|
|
214
|
-
this.stopJoinOpTimer();
|
|
215
|
-
}
|
|
216
220
|
this.setConnectionState(ConnectionState.Disconnected, reason);
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
private readonly transitionToConnectedState = () => {
|
|
224
|
+
// Defensive measure, we should always be in CatchingUp state when this is called.
|
|
225
|
+
if (this._connectionState === ConnectionState.CatchingUp) {
|
|
226
|
+
this.setConnectionState(ConnectionState.Connected);
|
|
227
|
+
} else {
|
|
228
|
+
this.logger.sendTelemetryEvent({
|
|
229
|
+
eventName: "cannotTransitionToConnectedState",
|
|
230
|
+
connectionState: ConnectionState[this._connectionState],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
219
235
|
/**
|
|
220
236
|
* The "connect" event indicates the connection to the Relay Service is live.
|
|
221
237
|
* However, some additional conditions must be met before we can fully transition to
|
|
222
238
|
* "Connected" state. This function handles that interim period, known as "Connecting" state.
|
|
223
239
|
* @param connectionMode - Read or Write connection
|
|
224
|
-
* @param details - Connection details returned from the
|
|
240
|
+
* @param details - Connection details returned from the Relay Service
|
|
241
|
+
* @param deltaManager - DeltaManager to be used for delaying Connected transition until caught up.
|
|
242
|
+
* If it's undefined, then don't delay and transition to Connected as soon as Leave/Join op are accounted for
|
|
225
243
|
*/
|
|
226
244
|
public receivedConnectEvent(
|
|
227
245
|
connectionMode: ConnectionMode,
|
|
228
246
|
details: IConnectionDetails,
|
|
247
|
+
deltaManager?: IDeltaManager<any, any>,
|
|
229
248
|
) {
|
|
230
249
|
const oldState = this._connectionState;
|
|
231
250
|
this._connectionState = ConnectionState.CatchingUp;
|
|
232
251
|
|
|
233
252
|
const writeConnection = connectionMode === "write";
|
|
234
|
-
assert(
|
|
253
|
+
assert(!this.handler.shouldClientJoinWrite() || writeConnection,
|
|
235
254
|
0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
|
|
236
|
-
assert(
|
|
255
|
+
assert(!this.waitingForLeaveOp || writeConnection,
|
|
237
256
|
0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
|
|
238
257
|
|
|
258
|
+
// Defensive measure in case catchUpMonitor from previous connection attempt wasn't already cleared
|
|
259
|
+
this.catchUpMonitor?.dispose();
|
|
260
|
+
|
|
239
261
|
// Note that this may be undefined since the connection is established proactively on load
|
|
240
262
|
// and the quorum may still be under initialization.
|
|
241
263
|
const quorumClients: IQuorumClients | undefined = this.handler.quorumClients();
|
|
@@ -248,6 +270,11 @@ export class ConnectionStateHandler {
|
|
|
248
270
|
// we know there can no longer be outstanding ops that we sent with the previous client id.
|
|
249
271
|
this._pendingClientId = details.clientId;
|
|
250
272
|
|
|
273
|
+
// We may want to catch up to known ops as of now before transitioning to Connected state
|
|
274
|
+
this.catchUpMonitor = deltaManager !== undefined
|
|
275
|
+
? new CatchUpMonitor(deltaManager)
|
|
276
|
+
: new ImmediateCatchUpMonitor();
|
|
277
|
+
|
|
251
278
|
// IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
|
|
252
279
|
this.handler.logConnectionStateChangeTelemetry(ConnectionState.CatchingUp, oldState);
|
|
253
280
|
|
|
@@ -264,9 +291,21 @@ export class ConnectionStateHandler {
|
|
|
264
291
|
this.startJoinOpTimer();
|
|
265
292
|
} else if (!this.waitingForLeaveOp) {
|
|
266
293
|
// We're not waiting for Join or Leave op (if read-only connection those don't even apply),
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
294
|
+
// but we do need to wait until we are caught up (to now-known ops) before transitioning to Connected state.
|
|
295
|
+
this.catchUpMonitor.on("caughtUp", this.transitionToConnectedState);
|
|
296
|
+
}
|
|
297
|
+
// else - We are waiting for Leave op still, do nothing for now, we will transition to Connected later
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Clear all the state used during the Connecting phase (set in receivedConnectEvent) */
|
|
301
|
+
private clearPendingConnectionState() {
|
|
302
|
+
this._pendingClientId = undefined;
|
|
303
|
+
|
|
304
|
+
this.catchUpMonitor?.dispose();
|
|
305
|
+
this.catchUpMonitor = undefined;
|
|
306
|
+
|
|
307
|
+
if (this.joinOpTimer.hasTimer) {
|
|
308
|
+
this.stopJoinOpTimer();
|
|
270
309
|
}
|
|
271
310
|
}
|
|
272
311
|
|
|
@@ -295,8 +334,9 @@ export class ConnectionStateHandler {
|
|
|
295
334
|
}
|
|
296
335
|
this._clientId = this.pendingClientId;
|
|
297
336
|
} else if (value === ConnectionState.Disconnected) {
|
|
298
|
-
//
|
|
299
|
-
this.
|
|
337
|
+
// Clear pending state immediately to prepare for reconnect
|
|
338
|
+
this.clearPendingConnectionState();
|
|
339
|
+
|
|
300
340
|
// Only wait for "leave" message if the connected client exists in the quorum because only the write
|
|
301
341
|
// client will exist in the quorum and only for those clients we will receive "removeMember" event and
|
|
302
342
|
// the client has some unacked ops.
|
package/src/container.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import merge from "lodash/merge";
|
|
8
8
|
import { v4 as uuid } from "uuid";
|
|
9
9
|
import {
|
|
10
|
-
IDisposable, ITelemetryProperties,
|
|
10
|
+
IDisposable, ITelemetryLogger, ITelemetryProperties,
|
|
11
11
|
} from "@fluidframework/common-definitions";
|
|
12
12
|
import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
|
|
13
13
|
import {
|
|
@@ -53,10 +53,7 @@ import {
|
|
|
53
53
|
isRuntimeMessage,
|
|
54
54
|
isUnpackedRuntimeMessage,
|
|
55
55
|
} from "@fluidframework/driver-utils";
|
|
56
|
-
import {
|
|
57
|
-
IProtocolHandler,
|
|
58
|
-
ProtocolOpHandlerWithClientValidation,
|
|
59
|
-
} from "@fluidframework/protocol-base";
|
|
56
|
+
import { IQuorumSnapshot } from "@fluidframework/protocol-base";
|
|
60
57
|
import {
|
|
61
58
|
IClient,
|
|
62
59
|
IClientConfiguration,
|
|
@@ -109,6 +106,11 @@ import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, Quorum
|
|
|
109
106
|
import { CollabWindowTracker } from "./collabWindowTracker";
|
|
110
107
|
import { ConnectionManager } from "./connectionManager";
|
|
111
108
|
import { ConnectionState } from "./connectionState";
|
|
109
|
+
import {
|
|
110
|
+
IProtocolHandler,
|
|
111
|
+
ProtocolHandler,
|
|
112
|
+
ProtocolHandlerBuilder,
|
|
113
|
+
} from "./protocol";
|
|
112
114
|
|
|
113
115
|
const detachedContainerRefSeqNumber = 0;
|
|
114
116
|
|
|
@@ -179,6 +181,10 @@ export async function waitContainerToCatchUp(container: IContainer) {
|
|
|
179
181
|
};
|
|
180
182
|
container.on("closed", closedCallback);
|
|
181
183
|
|
|
184
|
+
// Depending on config, transition to "connected" state may include the guarantee
|
|
185
|
+
// that all known ops have been processed. If so, we may introduce additional wait here.
|
|
186
|
+
// Waiting for "connected" state in either case gets us at least to our own Join op
|
|
187
|
+
// which is a reasonable approximation of "caught up"
|
|
182
188
|
const waitForOps = () => {
|
|
183
189
|
assert(container.connectionState === ConnectionState.CatchingUp
|
|
184
190
|
|| container.connectionState === ConnectionState.Connected,
|
|
@@ -228,6 +234,24 @@ const getCodeProposal =
|
|
|
228
234
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
229
235
|
(quorum: IQuorumProposals) => quorum.get("code") ?? quorum.get("code2");
|
|
230
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Helper function to report to telemetry cases where operation takes longer than expected (1s)
|
|
239
|
+
* @param logger - logger to use
|
|
240
|
+
* @param eventName - event name
|
|
241
|
+
* @param action - functor to call and measure
|
|
242
|
+
*/
|
|
243
|
+
async function ReportIfTooLong(
|
|
244
|
+
logger: ITelemetryLogger,
|
|
245
|
+
eventName: string,
|
|
246
|
+
action: () => Promise<ITelemetryProperties>,
|
|
247
|
+
) {
|
|
248
|
+
const event = PerformanceEvent.start(logger, { eventName });
|
|
249
|
+
const props = await action();
|
|
250
|
+
if (event.duration > 1000) {
|
|
251
|
+
event.end(props);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
231
255
|
/**
|
|
232
256
|
* State saved by a container at close time, to be used to load a new instance
|
|
233
257
|
* of the container to the same state
|
|
@@ -252,6 +276,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
252
276
|
loader: Loader,
|
|
253
277
|
loadOptions: IContainerLoadOptions,
|
|
254
278
|
pendingLocalState?: IPendingContainerState,
|
|
279
|
+
protocolHandlerBuilder?: ProtocolHandlerBuilder,
|
|
255
280
|
): Promise<Container> {
|
|
256
281
|
const container = new Container(
|
|
257
282
|
loader,
|
|
@@ -260,7 +285,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
260
285
|
resolvedUrl: loadOptions.resolvedUrl,
|
|
261
286
|
canReconnect: loadOptions.canReconnect,
|
|
262
287
|
serializedContainerState: pendingLocalState,
|
|
263
|
-
}
|
|
288
|
+
},
|
|
289
|
+
protocolHandlerBuilder);
|
|
264
290
|
|
|
265
291
|
return PerformanceEvent.timedExecAsync(
|
|
266
292
|
container.mc.logger,
|
|
@@ -308,10 +334,12 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
308
334
|
public static async createDetached(
|
|
309
335
|
loader: Loader,
|
|
310
336
|
codeDetails: IFluidCodeDetails,
|
|
337
|
+
protocolHandlerBuilder?: ProtocolHandlerBuilder,
|
|
311
338
|
): Promise<Container> {
|
|
312
339
|
const container = new Container(
|
|
313
340
|
loader,
|
|
314
|
-
{}
|
|
341
|
+
{},
|
|
342
|
+
protocolHandlerBuilder);
|
|
315
343
|
|
|
316
344
|
return PerformanceEvent.timedExecAsync(
|
|
317
345
|
container.mc.logger,
|
|
@@ -330,10 +358,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
330
358
|
public static async rehydrateDetachedFromSnapshot(
|
|
331
359
|
loader: Loader,
|
|
332
360
|
snapshot: string,
|
|
361
|
+
protocolHandlerBuilder?: ProtocolHandlerBuilder,
|
|
333
362
|
): Promise<Container> {
|
|
334
363
|
const container = new Container(
|
|
335
364
|
loader,
|
|
336
|
-
{}
|
|
365
|
+
{},
|
|
366
|
+
protocolHandlerBuilder);
|
|
367
|
+
|
|
337
368
|
return PerformanceEvent.timedExecAsync(
|
|
338
369
|
container.mc.logger,
|
|
339
370
|
{ eventName: "RehydrateDetachedFromSnapshot" },
|
|
@@ -387,7 +418,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
387
418
|
private readonly clientDetailsOverride: IClientDetails | undefined;
|
|
388
419
|
private readonly _deltaManager: DeltaManager<ConnectionManager>;
|
|
389
420
|
private service: IDocumentService | undefined;
|
|
390
|
-
private
|
|
421
|
+
private _initialClients: ISignalClient[] | undefined;
|
|
391
422
|
|
|
392
423
|
private _context: ContainerContext | undefined;
|
|
393
424
|
private get context() {
|
|
@@ -510,13 +541,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
510
541
|
* Retrieves the audience associated with the document
|
|
511
542
|
*/
|
|
512
543
|
public get audience(): IAudience {
|
|
513
|
-
return this.
|
|
544
|
+
return this.protocolHandler.audience;
|
|
514
545
|
}
|
|
515
546
|
|
|
516
547
|
/**
|
|
517
548
|
* Returns true if container is dirty.
|
|
518
549
|
* Which means data loss if container is closed at that same moment
|
|
519
|
-
* Most likely that happens when there is no network connection to
|
|
550
|
+
* Most likely that happens when there is no network connection to Relay Service
|
|
520
551
|
*/
|
|
521
552
|
public get isDirty() {
|
|
522
553
|
return this._dirtyContainer;
|
|
@@ -531,6 +562,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
531
562
|
constructor(
|
|
532
563
|
private readonly loader: Loader,
|
|
533
564
|
config: IContainerConfig,
|
|
565
|
+
private readonly protocolHandlerBuilder?: ProtocolHandlerBuilder,
|
|
534
566
|
) {
|
|
535
567
|
super((name, error) => {
|
|
536
568
|
this.mc.logger.sendErrorEvent(
|
|
@@ -540,7 +572,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
540
572
|
},
|
|
541
573
|
error);
|
|
542
574
|
});
|
|
543
|
-
this._audience = new Audience();
|
|
544
575
|
|
|
545
576
|
this.clientDetailsOverride = config.clientDetailsOverride;
|
|
546
577
|
this._resolvedUrl = config.resolvedUrl;
|
|
@@ -566,6 +597,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
566
597
|
containerAttachState: () => this._attachState,
|
|
567
598
|
containerLifecycleState: () => this._lifecycleState,
|
|
568
599
|
containerConnectionState: () => ConnectionState[this.connectionState],
|
|
600
|
+
serializedContainer: config.serializedContainerState !== undefined,
|
|
569
601
|
},
|
|
570
602
|
// we need to be judicious with our logging here to avoid generating too much data
|
|
571
603
|
// all data logged here should be broadly applicable, and not specific to a
|
|
@@ -578,6 +610,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
578
610
|
containerLoadedFromVersionId: () => this.loadedFromVersion?.id,
|
|
579
611
|
containerLoadedFromVersionDate: () => this.loadedFromVersion?.date,
|
|
580
612
|
// message information to associate errors with the specific execution state
|
|
613
|
+
// dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
|
|
581
614
|
dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
|
|
582
615
|
dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
|
|
583
616
|
dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
|
|
@@ -772,8 +805,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
772
805
|
assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
|
|
773
806
|
0x0d2 /* "resolved url should be valid Fluid url" */);
|
|
774
807
|
assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
|
|
775
|
-
assert(this._protocolHandler.attributes.term !== undefined,
|
|
776
|
-
0x30b /* Must have a valid protocol handler instance */);
|
|
808
|
+
assert(this._protocolHandler.attributes.term !== undefined, "Must have a valid protocol handler instance");
|
|
777
809
|
const pendingState: IPendingContainerState = {
|
|
778
810
|
pendingRuntimeState: this.context.getPendingLocalState(),
|
|
779
811
|
url: this.resolvedUrl.url,
|
|
@@ -1159,12 +1191,17 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1159
1191
|
// ...load in the existing quorum
|
|
1160
1192
|
// Initialize the protocol handler
|
|
1161
1193
|
this._protocolHandler = pendingLocalState === undefined
|
|
1162
|
-
? await this.initializeProtocolStateFromSnapshot(
|
|
1163
|
-
|
|
1194
|
+
? await this.initializeProtocolStateFromSnapshot(
|
|
1195
|
+
attributes,
|
|
1196
|
+
this.storageService,
|
|
1197
|
+
snapshot,
|
|
1198
|
+
) : await this.initializeProtocolState(
|
|
1164
1199
|
attributes,
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1200
|
+
{
|
|
1201
|
+
members: pendingLocalState.protocol.members,
|
|
1202
|
+
proposals: pendingLocalState.protocol.proposals,
|
|
1203
|
+
values: pendingLocalState.protocol.values,
|
|
1204
|
+
}, // pending IQuorumSnapshot
|
|
1168
1205
|
);
|
|
1169
1206
|
|
|
1170
1207
|
const codeDetails = this.getCodeDetailsFromQuorum();
|
|
@@ -1175,17 +1212,20 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1175
1212
|
pendingLocalState?.pendingRuntimeState,
|
|
1176
1213
|
);
|
|
1177
1214
|
|
|
1178
|
-
// Internal context is fully loaded at this point
|
|
1179
|
-
this.setLoaded();
|
|
1180
|
-
|
|
1181
1215
|
// We might have hit some failure that did not manifest itself in exception in this flow,
|
|
1182
1216
|
// do not start op processing in such case - static version of Container.load() will handle it correctly.
|
|
1183
1217
|
if (!this.closed) {
|
|
1184
1218
|
if (opsBeforeReturnP !== undefined) {
|
|
1185
1219
|
this._deltaManager.inbound.resume();
|
|
1186
1220
|
|
|
1187
|
-
await
|
|
1188
|
-
|
|
1221
|
+
await ReportIfTooLong(
|
|
1222
|
+
this.mc.logger,
|
|
1223
|
+
"WaitOps",
|
|
1224
|
+
async () => { await opsBeforeReturnP; return {}; });
|
|
1225
|
+
await ReportIfTooLong(
|
|
1226
|
+
this.mc.logger,
|
|
1227
|
+
"WaitOpProcessing",
|
|
1228
|
+
async () => this._deltaManager.inbound.waitTillProcessingDone());
|
|
1189
1229
|
|
|
1190
1230
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1191
1231
|
this._deltaManager.inbound.pause();
|
|
@@ -1215,9 +1255,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1215
1255
|
throw new Error("Container was closed while load()");
|
|
1216
1256
|
}
|
|
1217
1257
|
|
|
1258
|
+
// Internal context is fully loaded at this point
|
|
1259
|
+
this.setLoaded();
|
|
1260
|
+
|
|
1218
1261
|
return {
|
|
1219
1262
|
sequenceNumber: attributes.sequenceNumber,
|
|
1220
1263
|
version: versionId,
|
|
1264
|
+
dmLastProcessedSeqNumber: this._deltaManager.lastSequenceNumber,
|
|
1265
|
+
dmLastKnownSeqNumber: this._deltaManager.lastKnownSeqNumber,
|
|
1221
1266
|
};
|
|
1222
1267
|
}
|
|
1223
1268
|
|
|
@@ -1234,9 +1279,11 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1234
1279
|
const qValues = initQuorumValuesFromCodeDetails(source);
|
|
1235
1280
|
this._protocolHandler = await this.initializeProtocolState(
|
|
1236
1281
|
attributes,
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1282
|
+
{
|
|
1283
|
+
members: [],
|
|
1284
|
+
proposals: [],
|
|
1285
|
+
values: qValues,
|
|
1286
|
+
}, // IQuorumSnapShot
|
|
1240
1287
|
);
|
|
1241
1288
|
|
|
1242
1289
|
// The load context - given we seeded the quorum - will be great
|
|
@@ -1270,9 +1317,12 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1270
1317
|
this._protocolHandler =
|
|
1271
1318
|
await this.initializeProtocolState(
|
|
1272
1319
|
attributes,
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1320
|
+
{
|
|
1321
|
+
members: [],
|
|
1322
|
+
proposals: [],
|
|
1323
|
+
values: codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
|
|
1324
|
+
}, // IQuorumSnapShot
|
|
1325
|
+
);
|
|
1276
1326
|
|
|
1277
1327
|
await this.instantiateContextDetached(
|
|
1278
1328
|
true, // existing
|
|
@@ -1336,44 +1386,40 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1336
1386
|
storage: IDocumentStorageService,
|
|
1337
1387
|
snapshot: ISnapshotTree | undefined,
|
|
1338
1388
|
): Promise<IProtocolHandler> {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1389
|
+
const quorumSnapshot: IQuorumSnapshot = {
|
|
1390
|
+
members: [],
|
|
1391
|
+
proposals: [],
|
|
1392
|
+
values: [],
|
|
1393
|
+
};
|
|
1342
1394
|
|
|
1343
1395
|
if (snapshot !== undefined) {
|
|
1344
1396
|
const baseTree = getProtocolSnapshotTree(snapshot);
|
|
1345
|
-
[members, proposals, values] = await Promise.all([
|
|
1397
|
+
[quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] = await Promise.all([
|
|
1346
1398
|
readAndParse<[string, ISequencedClient][]>(storage, baseTree.blobs.quorumMembers),
|
|
1347
1399
|
readAndParse<[number, ISequencedProposal, string[]][]>(storage, baseTree.blobs.quorumProposals),
|
|
1348
1400
|
readAndParse<[string, ICommittedProposal][]>(storage, baseTree.blobs.quorumValues),
|
|
1349
1401
|
]);
|
|
1350
1402
|
}
|
|
1351
1403
|
|
|
1352
|
-
const protocolHandler = await this.initializeProtocolState(
|
|
1353
|
-
attributes,
|
|
1354
|
-
members,
|
|
1355
|
-
proposals,
|
|
1356
|
-
values);
|
|
1357
|
-
|
|
1404
|
+
const protocolHandler = await this.initializeProtocolState(attributes, quorumSnapshot);
|
|
1358
1405
|
return protocolHandler;
|
|
1359
1406
|
}
|
|
1360
1407
|
|
|
1361
1408
|
private async initializeProtocolState(
|
|
1362
1409
|
attributes: IDocumentAttributes,
|
|
1363
|
-
|
|
1364
|
-
proposals: [number, ISequencedProposal, string[]][],
|
|
1365
|
-
values: [string, any][],
|
|
1410
|
+
quorumSnapshot: IQuorumSnapshot,
|
|
1366
1411
|
): Promise<IProtocolHandler> {
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
attributes
|
|
1371
|
-
|
|
1372
|
-
proposals,
|
|
1373
|
-
values,
|
|
1412
|
+
const protocolHandlerBuilder =
|
|
1413
|
+
this.protocolHandlerBuilder ?? ((...args) => new ProtocolHandler(...args, new Audience()));
|
|
1414
|
+
const protocol = protocolHandlerBuilder(
|
|
1415
|
+
attributes,
|
|
1416
|
+
quorumSnapshot,
|
|
1374
1417
|
(key, value) => this.submitMessage(MessageType.Propose, { key, value }),
|
|
1418
|
+
this._initialClients ?? [],
|
|
1375
1419
|
);
|
|
1376
1420
|
|
|
1421
|
+
this._initialClients = undefined;
|
|
1422
|
+
|
|
1377
1423
|
const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
|
|
1378
1424
|
|
|
1379
1425
|
protocol.quorum.on("error", (error) => {
|
|
@@ -1494,17 +1540,30 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1494
1540
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1495
1541
|
deltaManager.inboundSignal.pause();
|
|
1496
1542
|
|
|
1497
|
-
deltaManager.on("connect", (details: IConnectionDetails,
|
|
1498
|
-
|
|
1499
|
-
|
|
1543
|
+
deltaManager.on("connect", (details: IConnectionDetails, _opsBehind?: number) => {
|
|
1544
|
+
if (this._protocolHandler === undefined) {
|
|
1545
|
+
// Store the initial clients so that they can be submitted to the
|
|
1546
|
+
// protocol handler when it is created.
|
|
1547
|
+
this._initialClients = details.initialClients;
|
|
1548
|
+
} else {
|
|
1549
|
+
// When reconnecting, the protocol handler is already created,
|
|
1550
|
+
// so we can update the audience right now.
|
|
1551
|
+
this._protocolHandler.audience.clear();
|
|
1500
1552
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1553
|
+
for (const priorClient of details.initialClients ?? []) {
|
|
1554
|
+
this._protocolHandler.audience.addMember(priorClient.clientId, priorClient.client);
|
|
1555
|
+
}
|
|
1503
1556
|
}
|
|
1504
1557
|
|
|
1558
|
+
const deltaManagerForCatchingUp =
|
|
1559
|
+
this.mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true ?
|
|
1560
|
+
this.deltaManager
|
|
1561
|
+
: undefined;
|
|
1562
|
+
|
|
1505
1563
|
this.connectionStateHandler.receivedConnectEvent(
|
|
1506
1564
|
this.connectionMode,
|
|
1507
1565
|
details,
|
|
1566
|
+
deltaManagerForCatchingUp,
|
|
1508
1567
|
);
|
|
1509
1568
|
});
|
|
1510
1569
|
|
|
@@ -1730,14 +1789,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1730
1789
|
private processSignal(message: ISignalMessage) {
|
|
1731
1790
|
// No clientId indicates a system signal message.
|
|
1732
1791
|
if (message.clientId === null) {
|
|
1733
|
-
|
|
1734
|
-
if (innerContent.type === MessageType.ClientJoin) {
|
|
1735
|
-
const newClient = innerContent.content as ISignalClient;
|
|
1736
|
-
this._audience.addMember(newClient.clientId, newClient.client);
|
|
1737
|
-
} else if (innerContent.type === MessageType.ClientLeave) {
|
|
1738
|
-
const leftClientId = innerContent.content as string;
|
|
1739
|
-
this._audience.removeMember(leftClientId);
|
|
1740
|
-
}
|
|
1792
|
+
this.protocolHandler.processSignal(message);
|
|
1741
1793
|
} else {
|
|
1742
1794
|
const local = this.clientId === message.clientId;
|
|
1743
1795
|
this.context.processSignal(message, local);
|
package/src/deltaManager.ts
CHANGED
|
@@ -410,7 +410,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
410
410
|
|
|
411
411
|
if (prefetchType !== "none") {
|
|
412
412
|
const cacheOnly = prefetchType === "cached";
|
|
413
|
-
await this.fetchMissingDeltasCore(
|
|
413
|
+
await this.fetchMissingDeltasCore(`DocumentOpen_${prefetchType}`, cacheOnly);
|
|
414
414
|
|
|
415
415
|
// Keep going with fetching ops from storage once we have all cached ops in.
|
|
416
416
|
// But do not block load and make this request async / not blocking this api.
|
|
@@ -418,7 +418,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
418
418
|
// (which in most cases will happen when we are done processing cached ops)
|
|
419
419
|
if (cacheOnly) {
|
|
420
420
|
// fire and forget
|
|
421
|
-
this.fetchMissingDeltas("
|
|
421
|
+
this.fetchMissingDeltas("PostDocumentOpen");
|
|
422
422
|
}
|
|
423
423
|
}
|
|
424
424
|
|
|
@@ -453,6 +453,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
453
453
|
private async getDeltas(
|
|
454
454
|
from: number, // inclusive
|
|
455
455
|
to: number | undefined, // exclusive
|
|
456
|
+
fetchReason: string,
|
|
456
457
|
callback: (messages: ISequencedDocumentMessage[]) => void,
|
|
457
458
|
cacheOnly: boolean) {
|
|
458
459
|
const docService = this.serviceProvider();
|
|
@@ -473,7 +474,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
473
474
|
// received through delta stream. Validate that before moving forward.
|
|
474
475
|
if (this.lastQueuedSequenceNumber >= lastExpectedOp) {
|
|
475
476
|
this.logger.sendPerformanceEvent({
|
|
476
|
-
reason:
|
|
477
|
+
reason: fetchReason,
|
|
477
478
|
eventName: "ExtraStorageCall",
|
|
478
479
|
early: true,
|
|
479
480
|
from,
|
|
@@ -521,7 +522,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
521
522
|
to, // exclusive
|
|
522
523
|
controller.signal,
|
|
523
524
|
cacheOnly,
|
|
524
|
-
|
|
525
|
+
fetchReason);
|
|
525
526
|
|
|
526
527
|
// eslint-disable-next-line no-constant-condition
|
|
527
528
|
while (true) {
|
|
@@ -876,6 +877,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
876
877
|
await this.getDeltas(
|
|
877
878
|
from,
|
|
878
879
|
to,
|
|
880
|
+
fetchReason,
|
|
879
881
|
(messages) => {
|
|
880
882
|
this.refreshDelayInfo(this.deltaStorageDelayId);
|
|
881
883
|
this.enqueueMessages(messages, fetchReason);
|