@fluidframework/container-loader 2.0.0-internal.6.4.0 → 2.0.0-internal.7.0.1
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/CHANGELOG.md +108 -0
- package/dist/catchUpMonitor.d.ts +2 -2
- package/dist/catchUpMonitor.d.ts.map +1 -1
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +89 -67
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionState.js +1 -1
- package/dist/connectionState.js.map +1 -1
- package/dist/connectionStateHandler.js +9 -9
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +6 -1
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +215 -215
- package/dist/container.js.map +1 -1
- package/dist/containerContext.js +16 -16
- package/dist/containerContext.js.map +1 -1
- package/dist/containerStorageAdapter.d.ts.map +1 -1
- package/dist/containerStorageAdapter.js +6 -8
- package/dist/containerStorageAdapter.js.map +1 -1
- package/dist/contracts.d.ts +2 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/debugLogger.js +4 -4
- package/dist/debugLogger.js.map +1 -1
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +86 -89
- package/dist/deltaManager.js.map +1 -1
- package/dist/deltaQueue.js +14 -14
- package/dist/deltaQueue.js.map +1 -1
- package/dist/loader.d.ts +2 -5
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +19 -84
- package/dist/loader.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/protocol.d.ts +1 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +3 -5
- package/dist/protocol.js.map +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/lib/catchUpMonitor.d.ts +2 -2
- package/lib/catchUpMonitor.d.ts.map +1 -1
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +92 -68
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionStateHandler.js +9 -9
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +6 -1
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +216 -216
- package/lib/container.js.map +1 -1
- package/lib/containerContext.js +16 -16
- package/lib/containerContext.js.map +1 -1
- package/lib/containerStorageAdapter.d.ts.map +1 -1
- package/lib/containerStorageAdapter.js +6 -8
- package/lib/containerStorageAdapter.js.map +1 -1
- package/lib/contracts.d.ts +2 -1
- package/lib/contracts.d.ts.map +1 -1
- package/lib/contracts.js.map +1 -1
- package/lib/debugLogger.js +4 -4
- package/lib/debugLogger.js.map +1 -1
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +86 -89
- package/lib/deltaManager.js.map +1 -1
- package/lib/deltaQueue.js +14 -14
- package/lib/deltaQueue.js.map +1 -1
- package/lib/loader.d.ts +2 -5
- package/lib/loader.d.ts.map +1 -1
- package/lib/loader.js +19 -84
- package/lib/loader.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/protocol.d.ts +1 -2
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +1 -3
- package/lib/protocol.js.map +1 -1
- package/package.json +24 -18
- package/src/connectionManager.ts +37 -5
- package/src/container.ts +15 -15
- package/src/containerStorageAdapter.ts +0 -6
- package/src/contracts.ts +5 -1
- package/src/deltaManager.ts +6 -8
- package/src/loader.ts +23 -91
- package/src/packageVersion.ts +1 -1
- package/src/protocol.ts +2 -6
package/lib/container.js
CHANGED
|
@@ -25,7 +25,7 @@ import { initQuorumValuesFromCodeDetails } from "./quorum";
|
|
|
25
25
|
import { NoopHeuristic } from "./noopHeuristic";
|
|
26
26
|
import { ConnectionManager } from "./connectionManager";
|
|
27
27
|
import { ConnectionState } from "./connectionState";
|
|
28
|
-
import {
|
|
28
|
+
import { ProtocolHandler, protocolHandlerShouldProcessSignal, } from "./protocol";
|
|
29
29
|
const detachedContainerRefSeqNumber = 0;
|
|
30
30
|
const dirtyContainerEvent = "dirty";
|
|
31
31
|
const savedContainerEvent = "saved";
|
|
@@ -121,208 +121,6 @@ export async function ReportIfTooLong(logger, eventName, action) {
|
|
|
121
121
|
}
|
|
122
122
|
const summarizerClientType = "summarizer";
|
|
123
123
|
export class Container extends EventEmitterWithErrorHandling {
|
|
124
|
-
/**
|
|
125
|
-
* @internal
|
|
126
|
-
*/
|
|
127
|
-
constructor(createProps, loadProps) {
|
|
128
|
-
super((name, error) => {
|
|
129
|
-
this.mc.logger.sendErrorEvent({
|
|
130
|
-
eventName: "ContainerEventHandlerException",
|
|
131
|
-
name: typeof name === "string" ? name : undefined,
|
|
132
|
-
}, error);
|
|
133
|
-
});
|
|
134
|
-
/**
|
|
135
|
-
* Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
|
|
136
|
-
*
|
|
137
|
-
* States are allowed to progress to further states:
|
|
138
|
-
* "loading" - "loaded" - "closing" - "disposing" - "closed" - "disposed"
|
|
139
|
-
*
|
|
140
|
-
* For example, moving from "closed" to "disposing" is not allowed since it is an earlier state.
|
|
141
|
-
*
|
|
142
|
-
* loading: Container has been created, but is not yet in normal/loaded state
|
|
143
|
-
* loaded: Container is in normal/loaded state
|
|
144
|
-
* closing: Container has started closing process (for re-entrancy prevention)
|
|
145
|
-
* disposing: Container has started disposing process (for re-entrancy prevention)
|
|
146
|
-
* closed: Container has closed
|
|
147
|
-
* disposed: Container has been disposed
|
|
148
|
-
*/
|
|
149
|
-
this._lifecycleState = "loading";
|
|
150
|
-
this._attachState = AttachState.Detached;
|
|
151
|
-
/** During initialization we pause the inbound queues. We track this state to ensure we only call resume once */
|
|
152
|
-
this.inboundQueuePausedFromInit = true;
|
|
153
|
-
this.firstConnection = true;
|
|
154
|
-
this.connectionTransitionTimes = [];
|
|
155
|
-
this.attachStarted = false;
|
|
156
|
-
this._dirtyContainer = false;
|
|
157
|
-
this.savedOps = [];
|
|
158
|
-
this.clientsWhoShouldHaveLeft = new Set();
|
|
159
|
-
this.setAutoReconnectTime = performance.now();
|
|
160
|
-
this._lifecycleEvents = new TypedEventEmitter();
|
|
161
|
-
this._disposed = false;
|
|
162
|
-
this.getAbsoluteUrl = async (relativeUrl) => {
|
|
163
|
-
if (this.resolvedUrl === undefined) {
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
return this.urlResolver.getAbsoluteUrl(this.resolvedUrl, relativeUrl, getPackageName(this._loadedCodeDetails));
|
|
167
|
-
};
|
|
168
|
-
this.updateDirtyContainerState = (dirty) => {
|
|
169
|
-
if (this._dirtyContainer === dirty) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
this._dirtyContainer = dirty;
|
|
173
|
-
this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
|
|
174
|
-
};
|
|
175
|
-
const { canReconnect, clientDetailsOverride, urlResolver, documentServiceFactory, codeLoader, options, scope, subLogger, detachedBlobStorage, protocolHandlerBuilder, } = createProps;
|
|
176
|
-
this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
|
|
177
|
-
const pendingLocalState = loadProps?.pendingLocalState;
|
|
178
|
-
this._clientId = pendingLocalState?.clientId;
|
|
179
|
-
this._canReconnect = canReconnect ?? true;
|
|
180
|
-
this.clientDetailsOverride = clientDetailsOverride;
|
|
181
|
-
this.urlResolver = urlResolver;
|
|
182
|
-
this.serviceFactory = documentServiceFactory;
|
|
183
|
-
this.codeLoader = codeLoader;
|
|
184
|
-
// Warning: this is only a shallow clone. Mutation of any individual loader option will mutate it for
|
|
185
|
-
// all clients that were loaded from the same loader (including summarizer clients).
|
|
186
|
-
// Tracking alternative ways to handle this in AB#4129.
|
|
187
|
-
this.options = { ...options };
|
|
188
|
-
this.scope = scope;
|
|
189
|
-
this.detachedBlobStorage = detachedBlobStorage;
|
|
190
|
-
this.protocolHandlerBuilder =
|
|
191
|
-
protocolHandlerBuilder ??
|
|
192
|
-
((attributes, quorumSnapshot, sendProposal) => new ProtocolHandler(attributes, quorumSnapshot, sendProposal, new Audience(), (clientId) => this.clientsWhoShouldHaveLeft.has(clientId)));
|
|
193
|
-
// Note that we capture the createProps here so we can replicate the creation call when we want to clone.
|
|
194
|
-
this.clone = async (_loadProps, createParamOverrides) => {
|
|
195
|
-
return Container.load(_loadProps, {
|
|
196
|
-
...createProps,
|
|
197
|
-
...createParamOverrides,
|
|
198
|
-
});
|
|
199
|
-
};
|
|
200
|
-
// Create logger for data stores to use
|
|
201
|
-
const type = this.client.details.type;
|
|
202
|
-
const interactive = this.client.details.capabilities.interactive;
|
|
203
|
-
const clientType = `${interactive ? "interactive" : "noninteractive"}${type !== undefined && type !== "" ? `/${type}` : ""}`;
|
|
204
|
-
// Need to use the property getter for docId because for detached flow we don't have the docId initially.
|
|
205
|
-
// We assign the id later so property getter is used.
|
|
206
|
-
this.subLogger = createChildLogger({
|
|
207
|
-
logger: subLogger,
|
|
208
|
-
properties: {
|
|
209
|
-
all: {
|
|
210
|
-
clientType,
|
|
211
|
-
containerId: uuid(),
|
|
212
|
-
docId: () => this.resolvedUrl?.id,
|
|
213
|
-
containerAttachState: () => this._attachState,
|
|
214
|
-
containerLifecycleState: () => this._lifecycleState,
|
|
215
|
-
containerConnectionState: () => ConnectionState[this.connectionState],
|
|
216
|
-
serializedContainer: pendingLocalState !== undefined,
|
|
217
|
-
},
|
|
218
|
-
// we need to be judicious with our logging here to avoid generating too much data
|
|
219
|
-
// all data logged here should be broadly applicable, and not specific to a
|
|
220
|
-
// specific error or class of errors
|
|
221
|
-
error: {
|
|
222
|
-
// load information to associate errors with the specific load point
|
|
223
|
-
dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
|
|
224
|
-
dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
|
|
225
|
-
dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
|
|
226
|
-
containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
|
|
227
|
-
containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
|
|
228
|
-
// message information to associate errors with the specific execution state
|
|
229
|
-
// dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
|
|
230
|
-
dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
|
|
231
|
-
dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
|
|
232
|
-
dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId === null
|
|
233
|
-
? "null"
|
|
234
|
-
: this.deltaManager?.lastMessage?.clientId,
|
|
235
|
-
dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
|
|
236
|
-
connectionStateDuration: () => performance.now() - this.connectionTransitionTimes[this.connectionState],
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
// Prefix all events in this file with container-loader
|
|
241
|
-
this.mc = createChildMonitoringContext({ logger: this.subLogger, namespace: "Container" });
|
|
242
|
-
this._deltaManager = this.createDeltaManager();
|
|
243
|
-
this.connectionStateHandler = createConnectionStateHandler({
|
|
244
|
-
logger: this.mc.logger,
|
|
245
|
-
connectionStateChanged: (value, oldState, reason) => {
|
|
246
|
-
if (value === ConnectionState.Connected) {
|
|
247
|
-
this._clientId = this.connectionStateHandler.pendingClientId;
|
|
248
|
-
}
|
|
249
|
-
this.logConnectionStateChangeTelemetry(value, oldState, reason);
|
|
250
|
-
if (this._lifecycleState === "loaded") {
|
|
251
|
-
this.propagateConnectionState(false /* initial transition */, value === ConnectionState.Disconnected
|
|
252
|
-
? reason
|
|
253
|
-
: undefined /* disconnectedReason */);
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
|
|
257
|
-
maxClientLeaveWaitTime: options.maxClientLeaveWaitTime,
|
|
258
|
-
logConnectionIssue: (eventName, category, details) => {
|
|
259
|
-
const mode = this.connectionMode;
|
|
260
|
-
// We get here when socket does not receive any ops on "write" connection, including
|
|
261
|
-
// its own join op.
|
|
262
|
-
// Report issues only if we already loaded container - op processing is paused while container is loading,
|
|
263
|
-
// so we always time-out processing of join op in cases where fetching snapshot takes a minute.
|
|
264
|
-
// It's not a problem with op processing itself - such issues should be tracked as part of boot perf monitoring instead.
|
|
265
|
-
this._deltaManager.logConnectionIssue({
|
|
266
|
-
eventName,
|
|
267
|
-
mode,
|
|
268
|
-
category: this._lifecycleState === "loading" ? "generic" : category,
|
|
269
|
-
duration: performance.now() -
|
|
270
|
-
this.connectionTransitionTimes[ConnectionState.CatchingUp],
|
|
271
|
-
...(details === undefined ? {} : { details: JSON.stringify(details) }),
|
|
272
|
-
});
|
|
273
|
-
// If this is "write" connection, it took too long to receive join op. But in most cases that's due
|
|
274
|
-
// to very slow op fetches and we will eventually get there.
|
|
275
|
-
// For "read" connections, we get here due to self join signal not arriving on time. We will need to
|
|
276
|
-
// better understand when and why it may happen.
|
|
277
|
-
// For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
|
|
278
|
-
// current state of audience.
|
|
279
|
-
// Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
|
|
280
|
-
// to call this.applyForConnectedState("addMemberEvent") for "read" connections)
|
|
281
|
-
if (mode === "read") {
|
|
282
|
-
const reason = { text: "NoJoinSignal" };
|
|
283
|
-
this.disconnectInternal(reason);
|
|
284
|
-
this.connectInternal({ reason, fetchOpsFromStorage: false });
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
clientShouldHaveLeft: (clientId) => {
|
|
288
|
-
this.clientsWhoShouldHaveLeft.add(clientId);
|
|
289
|
-
},
|
|
290
|
-
}, this.deltaManager, pendingLocalState?.clientId);
|
|
291
|
-
this.on(savedContainerEvent, () => {
|
|
292
|
-
this.connectionStateHandler.containerSaved();
|
|
293
|
-
});
|
|
294
|
-
// We expose our storage publicly, so it's possible others may call uploadSummaryWithContext() with a
|
|
295
|
-
// non-combined summary tree (in particular, ContainerRuntime.submitSummary). We'll intercept those calls
|
|
296
|
-
// using this callback and fix them up.
|
|
297
|
-
const addProtocolSummaryIfMissing = (summaryTree) => isCombinedAppAndProtocolSummary(summaryTree) === true
|
|
298
|
-
? summaryTree
|
|
299
|
-
: combineAppAndProtocolSummary(summaryTree, this.captureProtocolSummary());
|
|
300
|
-
// Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
|
|
301
|
-
// Even if not forced on via this flag, combined summaries may still be enabled by service policy.
|
|
302
|
-
const forceEnableSummarizeProtocolTree = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
|
|
303
|
-
options.summarizeProtocolTree;
|
|
304
|
-
this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
|
|
305
|
-
const isDomAvailable = typeof document === "object" &&
|
|
306
|
-
document !== null &&
|
|
307
|
-
typeof document.addEventListener === "function" &&
|
|
308
|
-
document.addEventListener !== null;
|
|
309
|
-
// keep track of last time page was visible for telemetry (on interactive clients only)
|
|
310
|
-
if (isDomAvailable && interactive) {
|
|
311
|
-
this.lastVisible = document.hidden ? performance.now() : undefined;
|
|
312
|
-
this.visibilityEventHandler = () => {
|
|
313
|
-
if (document.hidden) {
|
|
314
|
-
this.lastVisible = performance.now();
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
// settimeout so this will hopefully fire after disconnect event if being hidden caused it
|
|
318
|
-
setTimeout(() => {
|
|
319
|
-
this.lastVisible = undefined;
|
|
320
|
-
}, 0);
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
document.addEventListener("visibilitychange", this.visibilityEventHandler);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
124
|
/**
|
|
327
125
|
* Load an existing container.
|
|
328
126
|
* @internal
|
|
@@ -417,6 +215,10 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
417
215
|
get connectionMode() {
|
|
418
216
|
return this._deltaManager.connectionManager.connectionMode;
|
|
419
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* @deprecated Will be removed in future major release. Migrate all usage of IFluidRouter to the "entryPoint" pattern. Refer to Removing-IFluidRouter.md
|
|
220
|
+
*/
|
|
221
|
+
// eslint-disable-next-line import/no-deprecated
|
|
420
222
|
get IFluidRouter() {
|
|
421
223
|
return this;
|
|
422
224
|
}
|
|
@@ -532,6 +334,208 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
532
334
|
this._lifecycleEvents.once("disposed", disposedHandler);
|
|
533
335
|
});
|
|
534
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* @internal
|
|
339
|
+
*/
|
|
340
|
+
constructor(createProps, loadProps) {
|
|
341
|
+
super((name, error) => {
|
|
342
|
+
this.mc.logger.sendErrorEvent({
|
|
343
|
+
eventName: "ContainerEventHandlerException",
|
|
344
|
+
name: typeof name === "string" ? name : undefined,
|
|
345
|
+
}, error);
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
|
|
349
|
+
*
|
|
350
|
+
* States are allowed to progress to further states:
|
|
351
|
+
* "loading" - "loaded" - "closing" - "disposing" - "closed" - "disposed"
|
|
352
|
+
*
|
|
353
|
+
* For example, moving from "closed" to "disposing" is not allowed since it is an earlier state.
|
|
354
|
+
*
|
|
355
|
+
* loading: Container has been created, but is not yet in normal/loaded state
|
|
356
|
+
* loaded: Container is in normal/loaded state
|
|
357
|
+
* closing: Container has started closing process (for re-entrancy prevention)
|
|
358
|
+
* disposing: Container has started disposing process (for re-entrancy prevention)
|
|
359
|
+
* closed: Container has closed
|
|
360
|
+
* disposed: Container has been disposed
|
|
361
|
+
*/
|
|
362
|
+
this._lifecycleState = "loading";
|
|
363
|
+
this._attachState = AttachState.Detached;
|
|
364
|
+
/** During initialization we pause the inbound queues. We track this state to ensure we only call resume once */
|
|
365
|
+
this.inboundQueuePausedFromInit = true;
|
|
366
|
+
this.firstConnection = true;
|
|
367
|
+
this.connectionTransitionTimes = [];
|
|
368
|
+
this.attachStarted = false;
|
|
369
|
+
this._dirtyContainer = false;
|
|
370
|
+
this.savedOps = [];
|
|
371
|
+
this.clientsWhoShouldHaveLeft = new Set();
|
|
372
|
+
this.setAutoReconnectTime = performance.now();
|
|
373
|
+
this._lifecycleEvents = new TypedEventEmitter();
|
|
374
|
+
this._disposed = false;
|
|
375
|
+
this.getAbsoluteUrl = async (relativeUrl) => {
|
|
376
|
+
if (this.resolvedUrl === undefined) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
return this.urlResolver.getAbsoluteUrl(this.resolvedUrl, relativeUrl, getPackageName(this._loadedCodeDetails));
|
|
380
|
+
};
|
|
381
|
+
this.updateDirtyContainerState = (dirty) => {
|
|
382
|
+
if (this._dirtyContainer === dirty) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
this._dirtyContainer = dirty;
|
|
386
|
+
this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
|
|
387
|
+
};
|
|
388
|
+
const { canReconnect, clientDetailsOverride, urlResolver, documentServiceFactory, codeLoader, options, scope, subLogger, detachedBlobStorage, protocolHandlerBuilder, } = createProps;
|
|
389
|
+
this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
|
|
390
|
+
const pendingLocalState = loadProps?.pendingLocalState;
|
|
391
|
+
this._clientId = pendingLocalState?.clientId;
|
|
392
|
+
this._canReconnect = canReconnect ?? true;
|
|
393
|
+
this.clientDetailsOverride = clientDetailsOverride;
|
|
394
|
+
this.urlResolver = urlResolver;
|
|
395
|
+
this.serviceFactory = documentServiceFactory;
|
|
396
|
+
this.codeLoader = codeLoader;
|
|
397
|
+
// Warning: this is only a shallow clone. Mutation of any individual loader option will mutate it for
|
|
398
|
+
// all clients that were loaded from the same loader (including summarizer clients).
|
|
399
|
+
// Tracking alternative ways to handle this in AB#4129.
|
|
400
|
+
this.options = { ...options };
|
|
401
|
+
this.scope = scope;
|
|
402
|
+
this.detachedBlobStorage = detachedBlobStorage;
|
|
403
|
+
this.protocolHandlerBuilder =
|
|
404
|
+
protocolHandlerBuilder ??
|
|
405
|
+
((attributes, quorumSnapshot, sendProposal) => new ProtocolHandler(attributes, quorumSnapshot, sendProposal, new Audience(), (clientId) => this.clientsWhoShouldHaveLeft.has(clientId)));
|
|
406
|
+
// Note that we capture the createProps here so we can replicate the creation call when we want to clone.
|
|
407
|
+
this.clone = async (_loadProps, createParamOverrides) => {
|
|
408
|
+
return Container.load(_loadProps, {
|
|
409
|
+
...createProps,
|
|
410
|
+
...createParamOverrides,
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
// Create logger for data stores to use
|
|
414
|
+
const type = this.client.details.type;
|
|
415
|
+
const interactive = this.client.details.capabilities.interactive;
|
|
416
|
+
const clientType = `${interactive ? "interactive" : "noninteractive"}${type !== undefined && type !== "" ? `/${type}` : ""}`;
|
|
417
|
+
// Need to use the property getter for docId because for detached flow we don't have the docId initially.
|
|
418
|
+
// We assign the id later so property getter is used.
|
|
419
|
+
this.subLogger = createChildLogger({
|
|
420
|
+
logger: subLogger,
|
|
421
|
+
properties: {
|
|
422
|
+
all: {
|
|
423
|
+
clientType,
|
|
424
|
+
containerId: uuid(),
|
|
425
|
+
docId: () => this.resolvedUrl?.id,
|
|
426
|
+
containerAttachState: () => this._attachState,
|
|
427
|
+
containerLifecycleState: () => this._lifecycleState,
|
|
428
|
+
containerConnectionState: () => ConnectionState[this.connectionState],
|
|
429
|
+
serializedContainer: pendingLocalState !== undefined,
|
|
430
|
+
},
|
|
431
|
+
// we need to be judicious with our logging here to avoid generating too much data
|
|
432
|
+
// all data logged here should be broadly applicable, and not specific to a
|
|
433
|
+
// specific error or class of errors
|
|
434
|
+
error: {
|
|
435
|
+
// load information to associate errors with the specific load point
|
|
436
|
+
dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
|
|
437
|
+
dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
|
|
438
|
+
dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
|
|
439
|
+
containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
|
|
440
|
+
containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
|
|
441
|
+
// message information to associate errors with the specific execution state
|
|
442
|
+
// dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
|
|
443
|
+
dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
|
|
444
|
+
dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
|
|
445
|
+
dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId === null
|
|
446
|
+
? "null"
|
|
447
|
+
: this.deltaManager?.lastMessage?.clientId,
|
|
448
|
+
dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
|
|
449
|
+
connectionStateDuration: () => performance.now() - this.connectionTransitionTimes[this.connectionState],
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
// Prefix all events in this file with container-loader
|
|
454
|
+
this.mc = createChildMonitoringContext({ logger: this.subLogger, namespace: "Container" });
|
|
455
|
+
this._deltaManager = this.createDeltaManager();
|
|
456
|
+
this.connectionStateHandler = createConnectionStateHandler({
|
|
457
|
+
logger: this.mc.logger,
|
|
458
|
+
connectionStateChanged: (value, oldState, reason) => {
|
|
459
|
+
if (value === ConnectionState.Connected) {
|
|
460
|
+
this._clientId = this.connectionStateHandler.pendingClientId;
|
|
461
|
+
}
|
|
462
|
+
this.logConnectionStateChangeTelemetry(value, oldState, reason);
|
|
463
|
+
if (this._lifecycleState === "loaded") {
|
|
464
|
+
this.propagateConnectionState(false /* initial transition */, value === ConnectionState.Disconnected
|
|
465
|
+
? reason
|
|
466
|
+
: undefined /* disconnectedReason */);
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
|
|
470
|
+
maxClientLeaveWaitTime: options.maxClientLeaveWaitTime,
|
|
471
|
+
logConnectionIssue: (eventName, category, details) => {
|
|
472
|
+
const mode = this.connectionMode;
|
|
473
|
+
// We get here when socket does not receive any ops on "write" connection, including
|
|
474
|
+
// its own join op.
|
|
475
|
+
// Report issues only if we already loaded container - op processing is paused while container is loading,
|
|
476
|
+
// so we always time-out processing of join op in cases where fetching snapshot takes a minute.
|
|
477
|
+
// It's not a problem with op processing itself - such issues should be tracked as part of boot perf monitoring instead.
|
|
478
|
+
this._deltaManager.logConnectionIssue({
|
|
479
|
+
eventName,
|
|
480
|
+
mode,
|
|
481
|
+
category: this._lifecycleState === "loading" ? "generic" : category,
|
|
482
|
+
duration: performance.now() -
|
|
483
|
+
this.connectionTransitionTimes[ConnectionState.CatchingUp],
|
|
484
|
+
...(details === undefined ? {} : { details: JSON.stringify(details) }),
|
|
485
|
+
});
|
|
486
|
+
// If this is "write" connection, it took too long to receive join op. But in most cases that's due
|
|
487
|
+
// to very slow op fetches and we will eventually get there.
|
|
488
|
+
// For "read" connections, we get here due to self join signal not arriving on time. We will need to
|
|
489
|
+
// better understand when and why it may happen.
|
|
490
|
+
// For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
|
|
491
|
+
// current state of audience.
|
|
492
|
+
// Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
|
|
493
|
+
// to call this.applyForConnectedState("addMemberEvent") for "read" connections)
|
|
494
|
+
if (mode === "read") {
|
|
495
|
+
const reason = { text: "NoJoinSignal" };
|
|
496
|
+
this.disconnectInternal(reason);
|
|
497
|
+
this.connectInternal({ reason, fetchOpsFromStorage: false });
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
clientShouldHaveLeft: (clientId) => {
|
|
501
|
+
this.clientsWhoShouldHaveLeft.add(clientId);
|
|
502
|
+
},
|
|
503
|
+
}, this.deltaManager, pendingLocalState?.clientId);
|
|
504
|
+
this.on(savedContainerEvent, () => {
|
|
505
|
+
this.connectionStateHandler.containerSaved();
|
|
506
|
+
});
|
|
507
|
+
// We expose our storage publicly, so it's possible others may call uploadSummaryWithContext() with a
|
|
508
|
+
// non-combined summary tree (in particular, ContainerRuntime.submitSummary). We'll intercept those calls
|
|
509
|
+
// using this callback and fix them up.
|
|
510
|
+
const addProtocolSummaryIfMissing = (summaryTree) => isCombinedAppAndProtocolSummary(summaryTree) === true
|
|
511
|
+
? summaryTree
|
|
512
|
+
: combineAppAndProtocolSummary(summaryTree, this.captureProtocolSummary());
|
|
513
|
+
// Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
|
|
514
|
+
// Even if not forced on via this flag, combined summaries may still be enabled by service policy.
|
|
515
|
+
const forceEnableSummarizeProtocolTree = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
|
|
516
|
+
options.summarizeProtocolTree;
|
|
517
|
+
this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
|
|
518
|
+
const isDomAvailable = typeof document === "object" &&
|
|
519
|
+
document !== null &&
|
|
520
|
+
typeof document.addEventListener === "function" &&
|
|
521
|
+
document.addEventListener !== null;
|
|
522
|
+
// keep track of last time page was visible for telemetry (on interactive clients only)
|
|
523
|
+
if (isDomAvailable && interactive) {
|
|
524
|
+
this.lastVisible = document.hidden ? performance.now() : undefined;
|
|
525
|
+
this.visibilityEventHandler = () => {
|
|
526
|
+
if (document.hidden) {
|
|
527
|
+
this.lastVisible = performance.now();
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
// settimeout so this will hopefully fire after disconnect event if being hidden caused it
|
|
531
|
+
setTimeout(() => {
|
|
532
|
+
this.lastVisible = undefined;
|
|
533
|
+
}, 0);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
document.addEventListener("visibilitychange", this.visibilityEventHandler);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
535
539
|
/**
|
|
536
540
|
* Retrieves the quorum associated with the document
|
|
537
541
|
*/
|
|
@@ -666,7 +670,6 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
666
670
|
snapshotBlobs: this.baseSnapshotBlobs,
|
|
667
671
|
savedOps: this.savedOps,
|
|
668
672
|
url: this.resolvedUrl.url,
|
|
669
|
-
term: OnlyValidTermValue,
|
|
670
673
|
// no need to save this if there is no pending runtime state
|
|
671
674
|
clientId: pendingRuntimeState !== undefined ? this.clientId : undefined,
|
|
672
675
|
};
|
|
@@ -788,6 +791,9 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
788
791
|
}
|
|
789
792
|
}, { start: true, end: true, cancel: "generic" });
|
|
790
793
|
}
|
|
794
|
+
/**
|
|
795
|
+
* @deprecated Will be removed in future major release. Migrate all usage of IFluidRouter to the "entryPoint" pattern. Refer to Removing-IFluidRouter.md
|
|
796
|
+
*/
|
|
791
797
|
async request(path) {
|
|
792
798
|
return PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Request" }, async () => this.runtime.request(path), { end: true, cancel: "error" });
|
|
793
799
|
}
|
|
@@ -938,18 +944,14 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
938
944
|
async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState, loadToSequenceNumber) {
|
|
939
945
|
const timings = { phase1: performance.now() };
|
|
940
946
|
this.service = await this.serviceFactory.createDocumentService(resolvedUrl, this.subLogger, this.client.details.type === summarizerClientType);
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
// That all said, "read" does not work with memorylicious workflows (that opens two simultaneous
|
|
947
|
-
// connections to same file) in two ways:
|
|
948
|
-
// A) creation flow breaks (as one of the clients "sees" file as existing, and hits #2 above)
|
|
949
|
-
// B) Once file is created, transition from view-only connection to write does not work - some bugs to be fixed.
|
|
947
|
+
// Except in cases where it has stashed ops or requested by feature gate, the container will connect in "read" mode
|
|
948
|
+
const mode = this.mc.config.getBoolean("Fluid.Container.ForceWriteConnection") === true ||
|
|
949
|
+
(pendingLocalState?.savedOps.length ?? 0) > 0
|
|
950
|
+
? "write"
|
|
951
|
+
: "read";
|
|
950
952
|
const connectionArgs = {
|
|
951
953
|
reason: { text: "DocumentOpen" },
|
|
952
|
-
mode
|
|
954
|
+
mode,
|
|
953
955
|
fetchOpsFromStorage: false,
|
|
954
956
|
};
|
|
955
957
|
// Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
|
|
@@ -1114,7 +1116,6 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1114
1116
|
async createDetached(codeDetails) {
|
|
1115
1117
|
const attributes = {
|
|
1116
1118
|
sequenceNumber: detachedContainerRefSeqNumber,
|
|
1117
|
-
term: OnlyValidTermValue,
|
|
1118
1119
|
minimumSequenceNumber: 0,
|
|
1119
1120
|
};
|
|
1120
1121
|
await this.attachDeltaManagerOpHandler(attributes);
|
|
@@ -1154,7 +1155,6 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1154
1155
|
return {
|
|
1155
1156
|
minimumSequenceNumber: 0,
|
|
1156
1157
|
sequenceNumber: 0,
|
|
1157
|
-
term: OnlyValidTermValue,
|
|
1158
1158
|
};
|
|
1159
1159
|
}
|
|
1160
1160
|
// Backward compatibility: old docs would have ".attributes" instead of "attributes"
|