@fluidframework/container-runtime 2.41.0-338401 → 2.42.0
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 +8 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/channelCollection.d.ts +1 -1
- package/dist/channelCollection.d.ts.map +1 -1
- package/dist/channelCollection.js +4 -4
- package/dist/channelCollection.js.map +1 -1
- package/dist/compatUtils.d.ts +22 -1
- package/dist/compatUtils.d.ts.map +1 -1
- package/dist/compatUtils.js +109 -7
- package/dist/compatUtils.js.map +1 -1
- package/dist/containerRuntime.d.ts +67 -28
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +332 -186
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.d.ts.map +1 -1
- package/dist/dataStore.js +5 -0
- package/dist/dataStore.js.map +1 -1
- package/dist/gc/garbageCollection.d.ts.map +1 -1
- package/dist/gc/garbageCollection.js +2 -0
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/gc/gcDefinitions.d.ts +1 -1
- package/dist/gc/gcDefinitions.d.ts.map +1 -1
- package/dist/gc/gcDefinitions.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/messageTypes.d.ts +5 -4
- package/dist/messageTypes.d.ts.map +1 -1
- package/dist/messageTypes.js.map +1 -1
- package/dist/metadata.d.ts +1 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js.map +1 -1
- package/dist/opLifecycle/batchManager.d.ts +4 -0
- package/dist/opLifecycle/batchManager.d.ts.map +1 -1
- package/dist/opLifecycle/batchManager.js +7 -0
- package/dist/opLifecycle/batchManager.js.map +1 -1
- package/dist/opLifecycle/definitions.d.ts +6 -5
- package/dist/opLifecycle/definitions.d.ts.map +1 -1
- package/dist/opLifecycle/definitions.js.map +1 -1
- package/dist/opLifecycle/index.d.ts +1 -1
- package/dist/opLifecycle/index.d.ts.map +1 -1
- package/dist/opLifecycle/index.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.d.ts +9 -0
- package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/dist/opLifecycle/opGroupingManager.js +6 -4
- package/dist/opLifecycle/opGroupingManager.js.map +1 -1
- package/dist/opLifecycle/opSerialization.d.ts +2 -1
- package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
- package/dist/opLifecycle/opSerialization.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts +1 -0
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- package/dist/opLifecycle/outbox.js +6 -1
- package/dist/opLifecycle/outbox.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/pendingStateManager.d.ts +22 -5
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +34 -11
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runCounter.d.ts.map +1 -1
- package/dist/runCounter.js +1 -1
- package/dist/runCounter.js.map +1 -1
- package/dist/summary/documentSchema.d.ts +42 -18
- package/dist/summary/documentSchema.d.ts.map +1 -1
- package/dist/summary/documentSchema.js +62 -52
- package/dist/summary/documentSchema.js.map +1 -1
- package/dist/summary/index.d.ts +1 -1
- package/dist/summary/index.d.ts.map +1 -1
- package/dist/summary/index.js.map +1 -1
- package/lib/channelCollection.d.ts +1 -1
- package/lib/channelCollection.d.ts.map +1 -1
- package/lib/channelCollection.js +4 -4
- package/lib/channelCollection.js.map +1 -1
- package/lib/compatUtils.d.ts +22 -1
- package/lib/compatUtils.d.ts.map +1 -1
- package/lib/compatUtils.js +102 -3
- package/lib/compatUtils.js.map +1 -1
- package/lib/containerRuntime.d.ts +67 -28
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +333 -188
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.d.ts.map +1 -1
- package/lib/dataStore.js +5 -0
- package/lib/dataStore.js.map +1 -1
- package/lib/gc/garbageCollection.d.ts.map +1 -1
- package/lib/gc/garbageCollection.js +2 -0
- package/lib/gc/garbageCollection.js.map +1 -1
- package/lib/gc/gcDefinitions.d.ts +1 -1
- package/lib/gc/gcDefinitions.d.ts.map +1 -1
- package/lib/gc/gcDefinitions.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/messageTypes.d.ts +5 -4
- package/lib/messageTypes.d.ts.map +1 -1
- package/lib/messageTypes.js.map +1 -1
- package/lib/metadata.d.ts +1 -1
- package/lib/metadata.d.ts.map +1 -1
- package/lib/metadata.js.map +1 -1
- package/lib/opLifecycle/batchManager.d.ts +4 -0
- package/lib/opLifecycle/batchManager.d.ts.map +1 -1
- package/lib/opLifecycle/batchManager.js +7 -0
- package/lib/opLifecycle/batchManager.js.map +1 -1
- package/lib/opLifecycle/definitions.d.ts +6 -5
- package/lib/opLifecycle/definitions.d.ts.map +1 -1
- package/lib/opLifecycle/definitions.js.map +1 -1
- package/lib/opLifecycle/index.d.ts +1 -1
- package/lib/opLifecycle/index.d.ts.map +1 -1
- package/lib/opLifecycle/index.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.d.ts +9 -0
- package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/lib/opLifecycle/opGroupingManager.js +6 -4
- package/lib/opLifecycle/opGroupingManager.js.map +1 -1
- package/lib/opLifecycle/opSerialization.d.ts +2 -1
- package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
- package/lib/opLifecycle/opSerialization.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts +1 -0
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- package/lib/opLifecycle/outbox.js +6 -1
- package/lib/opLifecycle/outbox.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/pendingStateManager.d.ts +22 -5
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +34 -11
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runCounter.d.ts.map +1 -1
- package/lib/runCounter.js +1 -1
- package/lib/runCounter.js.map +1 -1
- package/lib/summary/documentSchema.d.ts +42 -18
- package/lib/summary/documentSchema.d.ts.map +1 -1
- package/lib/summary/documentSchema.js +62 -52
- package/lib/summary/documentSchema.js.map +1 -1
- package/lib/summary/index.d.ts +1 -1
- package/lib/summary/index.d.ts.map +1 -1
- package/lib/summary/index.js.map +1 -1
- package/package.json +19 -19
- package/src/channelCollection.ts +4 -4
- package/src/compatUtils.ts +145 -10
- package/src/containerRuntime.ts +472 -225
- package/src/dataStore.ts +7 -0
- package/src/gc/garbageCollection.ts +2 -0
- package/src/gc/gcDefinitions.ts +1 -1
- package/src/index.ts +2 -1
- package/src/messageTypes.ts +12 -5
- package/src/metadata.ts +1 -1
- package/src/opLifecycle/batchManager.ts +8 -0
- package/src/opLifecycle/definitions.ts +7 -3
- package/src/opLifecycle/index.ts +1 -0
- package/src/opLifecycle/opGroupingManager.ts +17 -4
- package/src/opLifecycle/opSerialization.ts +6 -1
- package/src/opLifecycle/outbox.ts +8 -1
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +64 -20
- package/src/runCounter.ts +4 -1
- package/src/summary/documentSchema.ts +111 -86
- package/src/summary/index.ts +2 -1
package/lib/containerRuntime.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
|
-
import { Trace, TypedEventEmitter } from "@fluid-internal/client-utils";
|
|
5
|
+
import { createEmitter, Trace, TypedEventEmitter } from "@fluid-internal/client-utils";
|
|
6
6
|
import { AttachState } from "@fluidframework/container-definitions";
|
|
7
7
|
import { isIDeltaManagerFull } from "@fluidframework/container-definitions/internal";
|
|
8
|
-
import { assert, Deferred, LazyPromise, PromiseCache, delay, } from "@fluidframework/core-utils/internal";
|
|
8
|
+
import { assert, Deferred, Lazy, LazyPromise, PromiseCache, delay, fail, unreachableCase, } from "@fluidframework/core-utils/internal";
|
|
9
9
|
import { SummaryType } from "@fluidframework/driver-definitions";
|
|
10
10
|
import { FetchSource, MessageType } from "@fluidframework/driver-definitions/internal";
|
|
11
11
|
import { readAndParse } from "@fluidframework/driver-utils/internal";
|
|
@@ -19,7 +19,7 @@ import { v4 as uuid } from "uuid";
|
|
|
19
19
|
import { BindBatchTracker } from "./batchTracker.js";
|
|
20
20
|
import { BlobManager, blobManagerBasePath, blobsTreeName, isBlobPath, loadBlobManagerLoadInfo, } from "./blobManager/index.js";
|
|
21
21
|
import { ChannelCollection, getSummaryForDatastores, wrapContext, } from "./channelCollection.js";
|
|
22
|
-
import { defaultMinVersionForCollab, getMinVersionForCollabDefaults, isValidMinVersionForCollab, } from "./compatUtils.js";
|
|
22
|
+
import { defaultMinVersionForCollab, getMinVersionForCollabDefaults, isValidMinVersionForCollab, validateRuntimeOptions, } from "./compatUtils.js";
|
|
23
23
|
import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
|
|
24
24
|
import { ReportOpPerfTelemetry } from "./connectionTelemetry.js";
|
|
25
25
|
import { ContainerFluidHandleContext } from "./containerHandleContext.js";
|
|
@@ -191,6 +191,20 @@ export async function loadContainerRuntime(params) {
|
|
|
191
191
|
return ContainerRuntime.loadRuntime(params);
|
|
192
192
|
}
|
|
193
193
|
const defaultMaxConsecutiveReconnects = 7;
|
|
194
|
+
/**
|
|
195
|
+
* These are the ONLY message types that are allowed to be submitted while in staging mode
|
|
196
|
+
* (Does not apply to pre-StagingMode batches that are resubmitted, those are not considered to be staged)
|
|
197
|
+
*/
|
|
198
|
+
function canStageMessageOfType(type) {
|
|
199
|
+
return (
|
|
200
|
+
// These are user changes coming up from the runtime's DataStores
|
|
201
|
+
type === ContainerMessageType.FluidDataStoreOp ||
|
|
202
|
+
// GC ops are used to detect issues in the reference graph so all clients can repair their GC state.
|
|
203
|
+
// These can be submitted at any time, including while in Staging Mode.
|
|
204
|
+
type === ContainerMessageType.GC ||
|
|
205
|
+
// These are typically sent shortly after boot and will not be common in Staging Mode, but it's possible.
|
|
206
|
+
type === ContainerMessageType.DocumentSchemaChange);
|
|
207
|
+
}
|
|
194
208
|
/**
|
|
195
209
|
* Represents the runtime of the container. Contains helper functions/state of the container.
|
|
196
210
|
* It will define the store level mappings.
|
|
@@ -239,6 +253,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
239
253
|
if (!isValidMinVersionForCollab(minVersionForCollab)) {
|
|
240
254
|
throw new UsageError(`Invalid minVersionForCollab: ${minVersionForCollab}. It must be an existing FF version (i.e. 2.22.1).`);
|
|
241
255
|
}
|
|
256
|
+
// We also validate that there is not a mismatch between `minVersionForCollab` and runtime options that
|
|
257
|
+
// were manually set.
|
|
258
|
+
validateRuntimeOptions(minVersionForCollab, runtimeOptions);
|
|
242
259
|
const defaultsAffectingDocSchema = getMinVersionForCollabDefaults(minVersionForCollab);
|
|
243
260
|
// The following are the default values for the options that do not affect the DocumentSchema.
|
|
244
261
|
const defaultsNotAffectingDocSchema = {
|
|
@@ -497,15 +514,19 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
497
514
|
}
|
|
498
515
|
/**
|
|
499
516
|
* Invokes the given callback and expects that no ops are submitted
|
|
500
|
-
* until execution finishes. If an op is submitted,
|
|
517
|
+
* until execution finishes. If an op is submitted, it will be marked as reentrant.
|
|
501
518
|
*
|
|
502
519
|
* @param callback - the callback to be invoked
|
|
503
520
|
*/
|
|
504
521
|
ensureNoDataModelChanges(callback) {
|
|
505
522
|
return this.dataModelChangeRunner.run(callback);
|
|
506
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Indicates whether the container is in a state where it is able to send
|
|
526
|
+
* ops (connected to op stream and not in readonly mode).
|
|
527
|
+
*/
|
|
507
528
|
get connected() {
|
|
508
|
-
return this.
|
|
529
|
+
return this.canSendOps;
|
|
509
530
|
}
|
|
510
531
|
/**
|
|
511
532
|
* clientId of parent (non-summarizing) container that owns summarizer container
|
|
@@ -569,7 +590,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
569
590
|
// Once it loads, it will process all such ops and we will stop accumulating further ops - ops will be processes as they come in.
|
|
570
591
|
this.pendingIdCompressorOps = [];
|
|
571
592
|
this.batchRunner = new BatchRunCounter();
|
|
572
|
-
this.
|
|
593
|
+
this.flushScheduled = false;
|
|
573
594
|
this.consecutiveReconnects = 0;
|
|
574
595
|
this.dataModelChangeRunner = new RunCounter();
|
|
575
596
|
this._disposed = false;
|
|
@@ -582,6 +603,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
582
603
|
this.snapshotCacheForLoadingGroupIds = new PromiseCache({
|
|
583
604
|
expiry: { policy: "absolute", durationMs: 60000 },
|
|
584
605
|
});
|
|
606
|
+
this.extensions = new Map();
|
|
585
607
|
this.notifyReadOnlyState = (readonly) => this.channelCollection.notifyReadOnlyState(readonly);
|
|
586
608
|
/**
|
|
587
609
|
* Enter Staging Mode, such that ops submitted to the ContainerRuntime will not be sent to the ordering service.
|
|
@@ -592,7 +614,10 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
592
614
|
// eslint-disable-next-line import/no-deprecated
|
|
593
615
|
this.enterStagingMode = () => {
|
|
594
616
|
if (this.stageControls !== undefined) {
|
|
595
|
-
throw new
|
|
617
|
+
throw new UsageError("already in staging mode");
|
|
618
|
+
}
|
|
619
|
+
if (this.attachState === AttachState.Detached) {
|
|
620
|
+
throw new UsageError("cannot enter staging mode while detached");
|
|
596
621
|
}
|
|
597
622
|
// Make sure all BatchManagers are empty before entering staging mode,
|
|
598
623
|
// since we mark whole batches as "staged" or not to indicate whether to submit them.
|
|
@@ -613,11 +638,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
613
638
|
// Pop all staged batches from the PSM and roll them back in LIFO order
|
|
614
639
|
this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
|
|
615
640
|
assert(runtimeOp !== undefined, 0xb82 /* Staged batches expected to have runtimeOp defined */);
|
|
616
|
-
this.
|
|
641
|
+
this.rollbackStagedChanges(runtimeOp, localOpMetadata);
|
|
617
642
|
});
|
|
618
|
-
|
|
619
|
-
this.updateDocumentDirtyState(this.pendingMessagesCount !== 0);
|
|
620
|
-
}
|
|
643
|
+
this.updateDocumentDirtyState();
|
|
621
644
|
}),
|
|
622
645
|
commitChanges: (optionsParam) => {
|
|
623
646
|
const options = { ...defaultStagingCommitOptions, ...optionsParam };
|
|
@@ -634,6 +657,15 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
634
657
|
return this.stageControls;
|
|
635
658
|
};
|
|
636
659
|
this.readAndParseBlob = async (id) => readAndParse(this.storage, id);
|
|
660
|
+
// While internal, ContainerRuntime has not been converted to use the new events support.
|
|
661
|
+
// Recreate the required events (new pattern) with injected, wrapper new emitter.
|
|
662
|
+
// It is lazily create to avoid listeners (old events) that ultimately go nowhere.
|
|
663
|
+
this.lazyEventsForExtensions = new Lazy(() => {
|
|
664
|
+
const eventEmitter = createEmitter();
|
|
665
|
+
this.on("connected", (clientId) => eventEmitter.emit("connected", clientId));
|
|
666
|
+
this.on("disconnected", () => eventEmitter.emit("disconnected"));
|
|
667
|
+
return eventEmitter;
|
|
668
|
+
});
|
|
637
669
|
const { options, clientDetails, connected, baseSnapshot, submitFn, submitBatchFn, submitSummaryFn, submitSignalFn, disposeFn, closeFn, deltaManager, quorum, audience, pendingLocalState, supportedFeatures, snapshotWithContents, } = context;
|
|
638
670
|
// In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
|
|
639
671
|
this.disposeFn = disposeFn ?? closeFn;
|
|
@@ -643,6 +675,11 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
643
675
|
this.mc = createChildMonitoringContext({
|
|
644
676
|
logger: this.baseLogger,
|
|
645
677
|
namespace: "ContainerRuntime",
|
|
678
|
+
properties: {
|
|
679
|
+
all: {
|
|
680
|
+
inStagingMode: this.inStagingMode,
|
|
681
|
+
},
|
|
682
|
+
},
|
|
646
683
|
});
|
|
647
684
|
// If we support multiple algorithms in the future, then we would need to manage it here carefully.
|
|
648
685
|
// We can use runtimeOptions.compressionOptions.compressionAlgorithm, but only if it's in the schema list!
|
|
@@ -662,12 +699,28 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
662
699
|
this.submitSummaryFn =
|
|
663
700
|
submitSummaryFn ??
|
|
664
701
|
((summaryOp, refseq) => submitFn(MessageType.Summarize, summaryOp, false));
|
|
665
|
-
|
|
702
|
+
const sequenceAndSubmitSignal = (envelope, targetClientId) => {
|
|
703
|
+
if (targetClientId === undefined) {
|
|
704
|
+
this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
|
|
705
|
+
}
|
|
706
|
+
submitSignalFn(envelope, targetClientId);
|
|
707
|
+
};
|
|
708
|
+
this.submitSignalFn = (envelope, targetClientId) => {
|
|
709
|
+
if (envelope.address?.startsWith("/")) {
|
|
710
|
+
throw new Error("General path based addressing is not implemented");
|
|
711
|
+
}
|
|
712
|
+
sequenceAndSubmitSignal(envelope, targetClientId);
|
|
713
|
+
};
|
|
714
|
+
this.submitExtensionSignal = (id, addressChain, message) => {
|
|
715
|
+
this.verifyNotClosed();
|
|
716
|
+
const envelope = createNewSignalEnvelope(`/ext/${id}/${addressChain.join("/")}`, message.type, message.content);
|
|
717
|
+
sequenceAndSubmitSignal(envelope, message.targetClientId);
|
|
718
|
+
};
|
|
666
719
|
// TODO: After IContainerContext.options is removed, we'll just create a new blank object {} here.
|
|
667
720
|
// Values are generally expected to be set from the runtime side.
|
|
668
721
|
this.options = options ?? {};
|
|
669
722
|
this.clientDetails = clientDetails;
|
|
670
|
-
|
|
723
|
+
this.isSummarizerClient = this.clientDetails.type === summarizerClientType;
|
|
671
724
|
this.loadedFromVersionId = context.getLoadedFromVersion()?.id;
|
|
672
725
|
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
673
726
|
this._getClientId = () => context.clientId;
|
|
@@ -694,8 +747,8 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
694
747
|
this.mc.logger.sendTelemetryEvent({
|
|
695
748
|
eventName: "Attached",
|
|
696
749
|
details: {
|
|
697
|
-
|
|
698
|
-
|
|
750
|
+
lastEmittedDirty: this.lastEmittedDirty,
|
|
751
|
+
currentDirtyState: this.computeCurrentDirtyState(),
|
|
699
752
|
},
|
|
700
753
|
});
|
|
701
754
|
});
|
|
@@ -704,7 +757,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
704
757
|
details: { attachState: this.attachState },
|
|
705
758
|
}));
|
|
706
759
|
// In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
|
|
707
|
-
this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
|
|
760
|
+
this.closeFn = this.isSummarizerClient ? this.disposeFn : closeFn;
|
|
708
761
|
let loadSummaryNumber;
|
|
709
762
|
// Get the container creation metadata. For new container, we initialize these. For existing containers,
|
|
710
763
|
// get the values from the metadata blob.
|
|
@@ -728,7 +781,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
728
781
|
this.messageAtLastSummary = lastMessageFromMetadata(metadata);
|
|
729
782
|
// Note that we only need to pull the *initial* connected state from the context.
|
|
730
783
|
// Later updates come through calls to setConnectionState.
|
|
731
|
-
this.
|
|
784
|
+
this.canSendOps = connected;
|
|
732
785
|
this.mc.logger.sendTelemetryEvent({
|
|
733
786
|
eventName: "GCFeatureMatrix",
|
|
734
787
|
metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
|
|
@@ -821,7 +874,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
821
874
|
existing,
|
|
822
875
|
metadata,
|
|
823
876
|
createContainerMetadata: this.createContainerMetadata,
|
|
824
|
-
isSummarizerClient,
|
|
877
|
+
isSummarizerClient: this.isSummarizerClient,
|
|
825
878
|
getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
|
|
826
879
|
getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
|
|
827
880
|
readAndParseBlob: async (id) => readAndParse(this.storage, id),
|
|
@@ -862,9 +915,6 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
862
915
|
// verifyNotClosed is called in FluidDataStoreContext, which is *the* expected caller.
|
|
863
916
|
const envelope1 = content;
|
|
864
917
|
const envelope2 = createNewSignalEnvelope(envelope1.address, type, envelope1.contents);
|
|
865
|
-
if (targetClientId === undefined) {
|
|
866
|
-
this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope2);
|
|
867
|
-
}
|
|
868
918
|
this.submitSignalFn(envelope2, targetClientId);
|
|
869
919
|
};
|
|
870
920
|
let snapshot = getSummaryForDatastores(baseSnapshot, metadata);
|
|
@@ -908,7 +958,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
908
958
|
// Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
|
|
909
959
|
this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
|
|
910
960
|
this.outbox = new Outbox({
|
|
911
|
-
shouldSend: () => this.
|
|
961
|
+
shouldSend: () => this.shouldSendOps(),
|
|
912
962
|
pendingStateManager: this.pendingStateManager,
|
|
913
963
|
submitBatchFn,
|
|
914
964
|
legacySendBatchFn,
|
|
@@ -958,9 +1008,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
958
1008
|
const closeSummarizerDelayOverride = this.mc.config.getNumber("Fluid.ContainerRuntime.Test.CloseSummarizerDelayOverrideMs");
|
|
959
1009
|
this.closeSummarizerDelayMs =
|
|
960
1010
|
closeSummarizerDelayOverride ?? defaultCloseSummarizerDelayMs;
|
|
961
|
-
this
|
|
962
|
-
|
|
963
|
-
context.updateDirtyContainerState(this.
|
|
1011
|
+
// We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
|
|
1012
|
+
this.lastEmittedDirty = this.computeCurrentDirtyState();
|
|
1013
|
+
context.updateDirtyContainerState(this.lastEmittedDirty);
|
|
964
1014
|
if (!this.skipSafetyFlushDuringProcessStack) {
|
|
965
1015
|
// Reference Sequence Number may have just changed, and it must be consistent across a batch,
|
|
966
1016
|
// so we should flush now to clear the way for the next ops.
|
|
@@ -1047,7 +1097,14 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1047
1097
|
await this.initializeSummarizer(loader);
|
|
1048
1098
|
if (this.sessionSchema.idCompressorMode === "on" ||
|
|
1049
1099
|
(this.sessionSchema.idCompressorMode === "delayed" && this.connected)) {
|
|
1050
|
-
this.
|
|
1100
|
+
PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnBoot" }, (event) => {
|
|
1101
|
+
this._idCompressor = this.createIdCompressorFn();
|
|
1102
|
+
event.end({
|
|
1103
|
+
details: {
|
|
1104
|
+
idCompressorMode: this.sessionSchema.idCompressorMode,
|
|
1105
|
+
},
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1051
1108
|
// This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
|
|
1052
1109
|
assert(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
|
|
1053
1110
|
}
|
|
@@ -1076,8 +1133,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1076
1133
|
const orderedClientCollection = new OrderedClientCollection(orderedClientLogger, this.innerDeltaManager, this._quorum);
|
|
1077
1134
|
const orderedClientElectionForSummarizer = new OrderedClientElection(orderedClientLogger, orderedClientCollection, this.electedSummarizerData ?? this.innerDeltaManager.lastSequenceNumber, SummarizerClientElection.isClientEligible, this.mc.config.getBoolean("Fluid.ContainerRuntime.OrderedClientElection.EnablePerformanceEvents"));
|
|
1078
1135
|
this.summarizerClientElection = new SummarizerClientElection(orderedClientLogger, summaryCollection, orderedClientElectionForSummarizer, maxOpsSinceLastSummary);
|
|
1079
|
-
|
|
1080
|
-
if (isSummarizerClient) {
|
|
1136
|
+
if (this.isSummarizerClient) {
|
|
1081
1137
|
// We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
|
|
1082
1138
|
// so that all non summarizer clients don't have to load the code inside this module.
|
|
1083
1139
|
const module = await import(
|
|
@@ -1425,35 +1481,32 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1425
1481
|
}
|
|
1426
1482
|
replayPendingStates() {
|
|
1427
1483
|
// We need to be able to send ops to replay states
|
|
1428
|
-
if (!this.
|
|
1484
|
+
if (!this.shouldSendOps()) {
|
|
1429
1485
|
return;
|
|
1430
1486
|
}
|
|
1431
|
-
//
|
|
1432
|
-
// dirty state change events
|
|
1433
|
-
//
|
|
1434
|
-
|
|
1435
|
-
const oldState = this.dirtyContainer;
|
|
1436
|
-
this.dirtyContainer = false;
|
|
1487
|
+
// Replaying is an internal operation and we don't want to generate noise while doing it.
|
|
1488
|
+
// So temporarily disable dirty state change events, and save the old state.
|
|
1489
|
+
// When we're done, we'll emit the event if the state changed.
|
|
1490
|
+
const oldState = this.lastEmittedDirty;
|
|
1437
1491
|
assert(this.emitDirtyDocumentEvent, 0x127 /* "dirty document event not set on replay" */);
|
|
1438
1492
|
this.emitDirtyDocumentEvent = false;
|
|
1439
|
-
let newState;
|
|
1440
1493
|
try {
|
|
1441
1494
|
// Any ID Allocation ops that failed to submit after the pending state was queued need to have
|
|
1442
1495
|
// the corresponding ranges resubmitted (note this call replaces the typical resubmit flow).
|
|
1443
1496
|
// Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
|
|
1444
1497
|
// before staging mode so we can simply say staged: false.
|
|
1445
1498
|
this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
|
|
1499
|
+
this.scheduleFlush();
|
|
1446
1500
|
// replay the ops
|
|
1447
1501
|
this.pendingStateManager.replayPendingStates();
|
|
1448
1502
|
}
|
|
1449
1503
|
finally {
|
|
1450
|
-
//
|
|
1451
|
-
|
|
1452
|
-
this.dirtyContainer = oldState;
|
|
1504
|
+
// Restore the old state, re-enable event emit
|
|
1505
|
+
this.lastEmittedDirty = oldState;
|
|
1453
1506
|
this.emitDirtyDocumentEvent = true;
|
|
1454
1507
|
}
|
|
1455
|
-
//
|
|
1456
|
-
this.updateDocumentDirtyState(
|
|
1508
|
+
// This will emit an event if the state changed relative to before replay
|
|
1509
|
+
this.updateDocumentDirtyState();
|
|
1457
1510
|
}
|
|
1458
1511
|
/**
|
|
1459
1512
|
* Parse an op's type and actual content from given serialized content
|
|
@@ -1510,25 +1563,35 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1510
1563
|
loadIdCompressor() {
|
|
1511
1564
|
if (this._idCompressor === undefined &&
|
|
1512
1565
|
this.sessionSchema.idCompressorMode !== undefined) {
|
|
1513
|
-
this.
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1566
|
+
PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnDelayedLoad" }, (event) => {
|
|
1567
|
+
this._idCompressor = this.createIdCompressorFn();
|
|
1568
|
+
// Finalize any ranges we received while the compressor was turned off.
|
|
1569
|
+
const ops = this.pendingIdCompressorOps;
|
|
1570
|
+
this.pendingIdCompressorOps = [];
|
|
1571
|
+
const trace = Trace.start();
|
|
1572
|
+
for (const range of ops) {
|
|
1573
|
+
this._idCompressor.finalizeCreationRange(range);
|
|
1574
|
+
}
|
|
1575
|
+
event.end({
|
|
1576
|
+
details: {
|
|
1577
|
+
finalizeCreationRangeDuration: trace.trace().duration,
|
|
1578
|
+
idCompressorMode: this.sessionSchema.idCompressorMode,
|
|
1579
|
+
pendingIdCompressorOps: ops.length,
|
|
1580
|
+
},
|
|
1581
|
+
});
|
|
1582
|
+
});
|
|
1520
1583
|
assert(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
|
|
1521
1584
|
}
|
|
1522
1585
|
}
|
|
1523
|
-
setConnectionState(
|
|
1586
|
+
setConnectionState(canSendOps, clientId) {
|
|
1524
1587
|
// Validate we have consistent state
|
|
1525
1588
|
const currentClientId = this._audience.getSelf()?.clientId;
|
|
1526
1589
|
assert(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
|
|
1527
1590
|
assert(this.clientId === currentClientId, 0x978 /* this.clientId does not match Audience */);
|
|
1528
|
-
if (
|
|
1591
|
+
if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
|
|
1529
1592
|
this.loadIdCompressor();
|
|
1530
1593
|
}
|
|
1531
|
-
if (
|
|
1594
|
+
if (canSendOps === false && this.delayConnectClientId !== undefined) {
|
|
1532
1595
|
this.delayConnectClientId = undefined;
|
|
1533
1596
|
this.mc.logger.sendTelemetryEvent({
|
|
1534
1597
|
eventName: "UnsuccessfulConnectedTransition",
|
|
@@ -1536,37 +1599,39 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1536
1599
|
// Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
|
|
1537
1600
|
return;
|
|
1538
1601
|
}
|
|
1539
|
-
if (!connected) {
|
|
1540
|
-
this.documentsSchemaController.onDisconnect();
|
|
1541
|
-
}
|
|
1542
1602
|
// If there are stashed blobs in the pending state, we need to delay
|
|
1543
1603
|
// propagation of the "connected" event until we have uploaded them to
|
|
1544
1604
|
// ensure we don't submit ops referencing a blob that has not been uploaded
|
|
1545
|
-
const connecting =
|
|
1605
|
+
const connecting = canSendOps && !this.canSendOps;
|
|
1546
1606
|
if (connecting && this.blobManager.hasPendingStashedUploads()) {
|
|
1547
1607
|
assert(!this.delayConnectClientId, 0x791 /* Connect event delay must be canceled before subsequent connect event */);
|
|
1548
1608
|
assert(!!clientId, 0x792 /* Must have clientId when connecting */);
|
|
1549
1609
|
this.delayConnectClientId = clientId;
|
|
1550
1610
|
return;
|
|
1551
1611
|
}
|
|
1552
|
-
this.setConnectionStateCore(
|
|
1612
|
+
this.setConnectionStateCore(canSendOps, clientId);
|
|
1553
1613
|
}
|
|
1554
|
-
|
|
1614
|
+
/**
|
|
1615
|
+
* Raises and propagates connected events.
|
|
1616
|
+
* @param canSendOps - Indicates whether the container can send ops or not (connected and not readonly).
|
|
1617
|
+
* @remarks The connection state from container context used here when raising connected events.
|
|
1618
|
+
*/
|
|
1619
|
+
setConnectionStateCore(canSendOps, clientId) {
|
|
1555
1620
|
assert(!this.delayConnectClientId, 0x394 /* connect event delay must be cleared before propagating connect event */);
|
|
1556
1621
|
this.verifyNotClosed();
|
|
1557
1622
|
// There might be no change of state due to Container calling this API after loading runtime.
|
|
1558
|
-
const
|
|
1559
|
-
const reconnection =
|
|
1623
|
+
const canSendOpsChanged = this.canSendOps !== canSendOps;
|
|
1624
|
+
const reconnection = canSendOpsChanged && !canSendOps;
|
|
1560
1625
|
// We need to flush the ops currently collected by Outbox to preserve original order.
|
|
1561
1626
|
// This flush NEEDS to happen before we set the ContainerRuntime to "connected".
|
|
1562
1627
|
// We want these ops to get to the PendingStateManager without sending to service and have them return to the Outbox upon calling "replayPendingStates".
|
|
1563
|
-
if (
|
|
1628
|
+
if (canSendOpsChanged && canSendOps) {
|
|
1564
1629
|
this.flush();
|
|
1565
1630
|
}
|
|
1566
|
-
this.
|
|
1567
|
-
if (
|
|
1631
|
+
this.canSendOps = canSendOps;
|
|
1632
|
+
if (canSendOps) {
|
|
1568
1633
|
assert(this.attachState === AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
|
|
1569
|
-
if (
|
|
1634
|
+
if (canSendOpsChanged) {
|
|
1570
1635
|
this.signalTelemetryManager.resetTracking();
|
|
1571
1636
|
}
|
|
1572
1637
|
}
|
|
@@ -1582,12 +1647,12 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1582
1647
|
return;
|
|
1583
1648
|
}
|
|
1584
1649
|
}
|
|
1585
|
-
if (
|
|
1650
|
+
if (canSendOpsChanged) {
|
|
1586
1651
|
this.replayPendingStates();
|
|
1587
1652
|
}
|
|
1588
|
-
this.channelCollection.setConnectionState(
|
|
1589
|
-
this.garbageCollector.setConnectionState(
|
|
1590
|
-
raiseConnectedEvent(this.mc.logger, this, connected
|
|
1653
|
+
this.channelCollection.setConnectionState(canSendOps, clientId);
|
|
1654
|
+
this.garbageCollector.setConnectionState(canSendOps, clientId);
|
|
1655
|
+
raiseConnectedEvent(this.mc.logger, this, this.connected /* canSendOps */, clientId);
|
|
1591
1656
|
}
|
|
1592
1657
|
async notifyOpReplay(message) {
|
|
1593
1658
|
await this.pendingStateManager.applyStashedOpsAt(message.sequenceNumber);
|
|
@@ -1715,6 +1780,8 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1715
1780
|
* @param groupedBatch - true if these messages are part of a grouped op batch.
|
|
1716
1781
|
*/
|
|
1717
1782
|
processInboundMessages(messagesWithMetadata, locationInBatch, local, savedOp, runtimeBatch, groupedBatch) {
|
|
1783
|
+
// This message could have been the last pending local (dirtyable) message, in which case we need to update dirty state to "saved"
|
|
1784
|
+
this.updateDocumentDirtyState();
|
|
1718
1785
|
if (locationInBatch.batchStart) {
|
|
1719
1786
|
const firstMessage = messagesWithMetadata[0]?.message;
|
|
1720
1787
|
assert(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
|
|
@@ -1809,11 +1876,6 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1809
1876
|
message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
|
|
1810
1877
|
}
|
|
1811
1878
|
this._processedClientSequenceNumber = message.clientSequenceNumber;
|
|
1812
|
-
// If there are no more pending messages after processing a local message,
|
|
1813
|
-
// the document is no longer dirty.
|
|
1814
|
-
if (!this.hasPendingMessages()) {
|
|
1815
|
-
this.updateDocumentDirtyState(false);
|
|
1816
|
-
}
|
|
1817
1879
|
// The DeltaManager used to do this, but doesn't anymore as of Loader v2.4
|
|
1818
1880
|
// Anyone listening to our "op" event would expect the contents to be parsed per this same logic
|
|
1819
1881
|
if (typeof message.contents === "string" &&
|
|
@@ -1835,11 +1897,6 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1835
1897
|
*
|
|
1836
1898
|
*/
|
|
1837
1899
|
validateAndProcessRuntimeMessages(message, messagesContent, local, savedOp) {
|
|
1838
|
-
// If there are no more pending messages after processing a local message,
|
|
1839
|
-
// the document is no longer dirty.
|
|
1840
|
-
if (!this.hasPendingMessages()) {
|
|
1841
|
-
this.updateDocumentDirtyState(false);
|
|
1842
|
-
}
|
|
1843
1900
|
// Get the contents without the localOpMetadata because not all message types know about localOpMetadata.
|
|
1844
1901
|
const contents = messagesContent.map((c) => c.contents);
|
|
1845
1902
|
switch (message.type) {
|
|
@@ -1915,20 +1972,44 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1915
1972
|
if (message.clientId === this.clientId) {
|
|
1916
1973
|
this.signalTelemetryManager.trackReceivedSignal(envelope, this.mc.logger, this.consecutiveReconnects);
|
|
1917
1974
|
}
|
|
1918
|
-
|
|
1975
|
+
const fullAddress = envelope.address;
|
|
1976
|
+
if (fullAddress === undefined) {
|
|
1919
1977
|
// No address indicates a container signal message.
|
|
1920
1978
|
this.emit("signal", transformed, local);
|
|
1921
1979
|
return;
|
|
1922
1980
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1981
|
+
this.routeNonContainerSignal(fullAddress, transformed, local);
|
|
1982
|
+
}
|
|
1983
|
+
routeNonContainerSignal(address, signalMessage, local) {
|
|
1984
|
+
// channelCollection signals are identified by no starting `/` in address.
|
|
1985
|
+
if (!address.startsWith("/")) {
|
|
1986
|
+
// Due to a mismatch between different layers in terms of
|
|
1987
|
+
// what is the interface of passing signals, we need to adjust
|
|
1988
|
+
// the signal envelope before sending it to the datastores to be processed
|
|
1989
|
+
const envelope = {
|
|
1990
|
+
address,
|
|
1991
|
+
contents: signalMessage.content,
|
|
1992
|
+
};
|
|
1993
|
+
signalMessage.content = envelope;
|
|
1994
|
+
this.channelCollection.processSignal(signalMessage, local);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const addresses = address.split("/");
|
|
1998
|
+
if (addresses.length > 2 && addresses[1] === "ext") {
|
|
1999
|
+
const id = addresses[2];
|
|
2000
|
+
const entry = this.extensions.get(id);
|
|
2001
|
+
if (entry !== undefined) {
|
|
2002
|
+
entry.extension.processSignal?.(addresses.slice(3), signalMessage, local);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
assert(!local, 0xba0 /* No recipient found for local signal */);
|
|
2007
|
+
this.mc.logger.sendTelemetryEvent({
|
|
2008
|
+
eventName: "SignalAddressNotFound",
|
|
2009
|
+
...tagCodeArtifacts({
|
|
2010
|
+
address,
|
|
2011
|
+
}),
|
|
2012
|
+
});
|
|
1932
2013
|
}
|
|
1933
2014
|
/**
|
|
1934
2015
|
* Flush the current batch of ops to the ordering service for sequencing
|
|
@@ -1938,6 +2019,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1938
2019
|
* @param resubmitInfo - If defined, indicates this is a resubmission of a batch with the given Batch info needed for resubmit.
|
|
1939
2020
|
*/
|
|
1940
2021
|
flush(resubmitInfo) {
|
|
2022
|
+
this.flushScheduled = false;
|
|
1941
2023
|
try {
|
|
1942
2024
|
assert(!this.batchRunner.running, 0x24c /* "Cannot call `flush()` while manually accumulating a batch (e.g. under orderSequentially) */);
|
|
1943
2025
|
this.outbox.flush(resubmitInfo);
|
|
@@ -1958,7 +2040,6 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1958
2040
|
*/
|
|
1959
2041
|
orderSequentially(callback) {
|
|
1960
2042
|
let checkpoint;
|
|
1961
|
-
const checkpointDirtyState = this.dirtyContainer;
|
|
1962
2043
|
// eslint-disable-next-line import/no-deprecated
|
|
1963
2044
|
let stageControls;
|
|
1964
2045
|
if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
|
|
@@ -1978,11 +2059,10 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1978
2059
|
if (checkpoint) {
|
|
1979
2060
|
// This will throw and close the container if rollback fails
|
|
1980
2061
|
try {
|
|
1981
|
-
checkpoint.rollback((message) =>
|
|
1982
|
-
//
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
}
|
|
2062
|
+
checkpoint.rollback((message) =>
|
|
2063
|
+
// These changes are staged since we entered staging mode above
|
|
2064
|
+
this.rollbackStagedChanges(message.runtimeOp, message.localOpMetadata));
|
|
2065
|
+
this.updateDocumentDirtyState();
|
|
1986
2066
|
stageControls?.discardChanges();
|
|
1987
2067
|
stageControls = undefined;
|
|
1988
2068
|
}
|
|
@@ -2058,17 +2138,11 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2058
2138
|
const context = this.channelCollection.createDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], loadingGroupId);
|
|
2059
2139
|
return channelToDataStore(await context.realize(), context.id, this.channelCollection, this.mc.logger);
|
|
2060
2140
|
}
|
|
2061
|
-
|
|
2141
|
+
shouldSendOps() {
|
|
2062
2142
|
// Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
|
|
2063
2143
|
// container runtime's ability to send ops depend on the actual readonly state of the delta manager.
|
|
2064
2144
|
return (this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure);
|
|
2065
2145
|
}
|
|
2066
|
-
/**
|
|
2067
|
-
* Typically ops are batched and later flushed together, but in some cases we want to flush immediately.
|
|
2068
|
-
*/
|
|
2069
|
-
currentlyBatching() {
|
|
2070
|
-
return this.flushMode !== FlushMode.Immediate || this.batchRunner.running;
|
|
2071
|
-
}
|
|
2072
2146
|
getQuorum() {
|
|
2073
2147
|
return this._quorum;
|
|
2074
2148
|
}
|
|
@@ -2080,36 +2154,17 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2080
2154
|
* either were not sent out to delta stream or were not yet acknowledged.
|
|
2081
2155
|
*/
|
|
2082
2156
|
get isDirty() {
|
|
2083
|
-
|
|
2157
|
+
// Rather than recomputing the dirty state in this moment,
|
|
2158
|
+
// just regurgitate the last emitted dirty state.
|
|
2159
|
+
return this.lastEmittedDirty;
|
|
2084
2160
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
return false;
|
|
2093
|
-
}
|
|
2094
|
-
break;
|
|
2095
|
-
}
|
|
2096
|
-
case ContainerMessageType.FluidDataStoreOp: {
|
|
2097
|
-
const envelope = contents;
|
|
2098
|
-
if (envelope.address === agentSchedulerId) {
|
|
2099
|
-
return false;
|
|
2100
|
-
}
|
|
2101
|
-
break;
|
|
2102
|
-
}
|
|
2103
|
-
case ContainerMessageType.IdAllocation:
|
|
2104
|
-
case ContainerMessageType.DocumentSchemaChange:
|
|
2105
|
-
case ContainerMessageType.GC: {
|
|
2106
|
-
return false;
|
|
2107
|
-
}
|
|
2108
|
-
default: {
|
|
2109
|
-
break;
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
return true;
|
|
2161
|
+
/**
|
|
2162
|
+
* Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
|
|
2163
|
+
*/
|
|
2164
|
+
computeCurrentDirtyState() {
|
|
2165
|
+
return (this.attachState !== AttachState.Attached ||
|
|
2166
|
+
this.pendingStateManager.hasPendingUserChanges() ||
|
|
2167
|
+
this.outbox.containsUserChanges());
|
|
2113
2168
|
}
|
|
2114
2169
|
/**
|
|
2115
2170
|
* Submits the signal to be sent to other clients.
|
|
@@ -2126,9 +2181,6 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2126
2181
|
submitSignal(type, content, targetClientId) {
|
|
2127
2182
|
this.verifyNotClosed();
|
|
2128
2183
|
const envelope = createNewSignalEnvelope(undefined /* address */, type, content);
|
|
2129
|
-
if (targetClientId === undefined) {
|
|
2130
|
-
this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
|
|
2131
|
-
}
|
|
2132
2184
|
this.submitSignalFn(envelope, targetClientId);
|
|
2133
2185
|
}
|
|
2134
2186
|
setAttachState(attachState) {
|
|
@@ -2139,9 +2191,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2139
2191
|
assert(this.attachState === AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
|
|
2140
2192
|
this.emit("attached");
|
|
2141
2193
|
}
|
|
2142
|
-
|
|
2143
|
-
this.updateDocumentDirtyState(false);
|
|
2144
|
-
}
|
|
2194
|
+
this.updateDocumentDirtyState();
|
|
2145
2195
|
this.channelCollection.setAttachState(attachState);
|
|
2146
2196
|
}
|
|
2147
2197
|
/**
|
|
@@ -2709,18 +2759,20 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2709
2759
|
hasPendingMessages() {
|
|
2710
2760
|
return this.pendingMessagesCount !== 0;
|
|
2711
2761
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2762
|
+
/**
|
|
2763
|
+
* Emit "dirty" or "saved" event based on the current dirty state of the document.
|
|
2764
|
+
* This must be called every time the states underlying the dirty state change.
|
|
2765
|
+
*
|
|
2766
|
+
* @privateRemarks - It's helpful to think of this as an event handler registered
|
|
2767
|
+
* for hypothetical "changed" events for PendingStateManager, Outbox, and Container Attach machinery.
|
|
2768
|
+
* But those events don't exist so we manually call this wherever we know those changes happen.
|
|
2769
|
+
*/
|
|
2770
|
+
updateDocumentDirtyState() {
|
|
2771
|
+
const dirty = this.computeCurrentDirtyState();
|
|
2772
|
+
if (this.lastEmittedDirty === dirty) {
|
|
2721
2773
|
return;
|
|
2722
2774
|
}
|
|
2723
|
-
this.
|
|
2775
|
+
this.lastEmittedDirty = dirty;
|
|
2724
2776
|
if (this.emitDirtyDocumentEvent) {
|
|
2725
2777
|
this.emit(dirty ? "dirty" : "saved");
|
|
2726
2778
|
}
|
|
@@ -2775,6 +2827,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2775
2827
|
try {
|
|
2776
2828
|
// If we're resubmitting a batch, keep the same "staged" value as before. Otherwise, use the current "global" state.
|
|
2777
2829
|
const staged = this.batchRunner.resubmitInfo?.staged ?? this.inStagingMode;
|
|
2830
|
+
assert(!staged || canStageMessageOfType(type), 0xbba /* Unexpected message type submitted in Staging Mode */);
|
|
2778
2831
|
// Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
|
|
2779
2832
|
if (!staged) {
|
|
2780
2833
|
this.submitIdAllocationOpIfNeeded({ staged: false });
|
|
@@ -2782,7 +2835,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2782
2835
|
// Allow document schema controller to send a message if it needs to propose change in document schema.
|
|
2783
2836
|
// If it needs to send a message, it will call provided callback with payload of such message and rely
|
|
2784
2837
|
// on this callback to do actual sending.
|
|
2785
|
-
const schemaChangeMessage = this.documentsSchemaController.
|
|
2838
|
+
const schemaChangeMessage = this.documentsSchemaController.maybeGenerateSchemaMessage();
|
|
2786
2839
|
if (schemaChangeMessage) {
|
|
2787
2840
|
this.mc.logger.sendTelemetryEvent({
|
|
2788
2841
|
eventName: "SchemaChangeProposal",
|
|
@@ -2818,14 +2871,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2818
2871
|
else {
|
|
2819
2872
|
this.outbox.submit(message);
|
|
2820
2873
|
}
|
|
2821
|
-
|
|
2822
|
-
const flushImmediatelyOnSubmit = !this.currentlyBatching();
|
|
2823
|
-
if (flushImmediatelyOnSubmit) {
|
|
2824
|
-
this.flush();
|
|
2825
|
-
}
|
|
2826
|
-
else {
|
|
2827
|
-
this.scheduleFlush();
|
|
2828
|
-
}
|
|
2874
|
+
this.scheduleFlush();
|
|
2829
2875
|
}
|
|
2830
2876
|
catch (error) {
|
|
2831
2877
|
const dpe = DataProcessingError.wrapIfUnrecognized(error, "ContainerRuntime.submit", {
|
|
@@ -2834,27 +2880,26 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2834
2880
|
this.closeFn(dpe);
|
|
2835
2881
|
throw dpe;
|
|
2836
2882
|
}
|
|
2837
|
-
|
|
2838
|
-
this.updateDocumentDirtyState(true);
|
|
2839
|
-
}
|
|
2883
|
+
this.updateDocumentDirtyState();
|
|
2840
2884
|
}
|
|
2841
2885
|
scheduleFlush() {
|
|
2842
|
-
if (this.
|
|
2886
|
+
if (this.flushScheduled) {
|
|
2843
2887
|
return;
|
|
2844
2888
|
}
|
|
2845
|
-
this.
|
|
2846
|
-
// TODO: hoist this out of the function scope to save unnecessary allocations
|
|
2847
|
-
// eslint-disable-next-line unicorn/consistent-function-scoping -- Separate `flush` method already exists in outer scope
|
|
2848
|
-
const flush = () => {
|
|
2849
|
-
this.flushTaskExists = false;
|
|
2850
|
-
this.flush();
|
|
2851
|
-
};
|
|
2889
|
+
this.flushScheduled = true;
|
|
2852
2890
|
switch (this.flushMode) {
|
|
2891
|
+
case FlushMode.Immediate: {
|
|
2892
|
+
// When in Immediate flush mode, flush immediately unless we are intentionally batching multiple ops (e.g. via orderSequentially)
|
|
2893
|
+
if (!this.batchRunner.running) {
|
|
2894
|
+
this.flush();
|
|
2895
|
+
}
|
|
2896
|
+
break;
|
|
2897
|
+
}
|
|
2853
2898
|
case FlushMode.TurnBased: {
|
|
2854
2899
|
// When in TurnBased flush mode the runtime will buffer operations in the current turn and send them as a single
|
|
2855
2900
|
// batch at the end of the turn
|
|
2856
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
2857
|
-
Promise.resolve().then(flush);
|
|
2901
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- Container will close if flush throws
|
|
2902
|
+
Promise.resolve().then(() => this.flush());
|
|
2858
2903
|
break;
|
|
2859
2904
|
}
|
|
2860
2905
|
// FlushModeExperimental is experimental and not exposed directly in the runtime APIs
|
|
@@ -2862,12 +2907,11 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2862
2907
|
// When in Async flush mode, the runtime will accumulate all operations across JS turns and send them as a single
|
|
2863
2908
|
// batch when all micro-tasks are complete.
|
|
2864
2909
|
// Compared to TurnBased, this flush mode will capture more ops into the same batch.
|
|
2865
|
-
setTimeout(flush, 0);
|
|
2910
|
+
setTimeout(() => this.flush(), 0);
|
|
2866
2911
|
break;
|
|
2867
2912
|
}
|
|
2868
2913
|
default: {
|
|
2869
|
-
|
|
2870
|
-
break;
|
|
2914
|
+
fail(0x587 /* Unreachable unless manually accumulating a batch */);
|
|
2871
2915
|
}
|
|
2872
2916
|
}
|
|
2873
2917
|
}
|
|
@@ -2889,43 +2933,72 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2889
2933
|
}
|
|
2890
2934
|
/**
|
|
2891
2935
|
* Resubmits each message in the batch, and then flushes the outbox.
|
|
2936
|
+
* This typically happens when we reconnect and there are pending messages.
|
|
2937
|
+
*
|
|
2938
|
+
* @remarks
|
|
2939
|
+
* Attempting to resubmit a batch that has been successfully sequenced will not happen due to
|
|
2940
|
+
* checks in the ConnectionStateHandler (Loader layer)
|
|
2892
2941
|
*
|
|
2893
|
-
*
|
|
2942
|
+
* The only exception to this would be if the Container "forks" due to misuse of the "Offline Load" feature.
|
|
2943
|
+
* If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
|
|
2894
2944
|
* for correlation to detect container forking.
|
|
2895
2945
|
*/
|
|
2896
2946
|
reSubmitBatch(batch, { batchId, staged, squash }) {
|
|
2947
|
+
assert(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
|
|
2897
2948
|
const resubmitInfo = {
|
|
2898
2949
|
// Only include Batch ID if "Offline Load" feature is enabled
|
|
2899
2950
|
// It's only needed to identify batches across container forks arising from misuse of offline load.
|
|
2900
2951
|
batchId: this.offlineEnabled ? batchId : undefined,
|
|
2901
2952
|
staged,
|
|
2902
2953
|
};
|
|
2954
|
+
const resubmitFn = squash
|
|
2955
|
+
? this.reSubmitWithSquashing.bind(this)
|
|
2956
|
+
: this.reSubmit.bind(this);
|
|
2903
2957
|
this.batchRunner.run(() => {
|
|
2904
2958
|
for (const message of batch) {
|
|
2905
|
-
|
|
2959
|
+
resubmitFn(message);
|
|
2906
2960
|
}
|
|
2907
2961
|
}, resubmitInfo);
|
|
2908
2962
|
this.flush(resubmitInfo);
|
|
2909
2963
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2964
|
+
/**
|
|
2965
|
+
* Resubmit the given message as part of a squash rebase upon exiting Staging Mode.
|
|
2966
|
+
* How exactly to resubmit the message is up to the subsystem that submitted the op to begin with.
|
|
2967
|
+
*/
|
|
2968
|
+
reSubmitWithSquashing(resubmitData) {
|
|
2969
|
+
const message = resubmitData.runtimeOp;
|
|
2970
|
+
assert(canStageMessageOfType(message.type), 0xbbb /* Expected message type to be compatible with staging */);
|
|
2971
|
+
switch (message.type) {
|
|
2972
|
+
case ContainerMessageType.FluidDataStoreOp: {
|
|
2973
|
+
this.channelCollection.reSubmit(message.type, message.contents, resubmitData.localOpMetadata,
|
|
2974
|
+
/* squash: */ true);
|
|
2975
|
+
break;
|
|
2976
|
+
}
|
|
2977
|
+
// NOTE: Squash doesn't apply to GC or DocumentSchemaChange ops, fallback to typical resubmit logic.
|
|
2978
|
+
case ContainerMessageType.GC:
|
|
2979
|
+
case ContainerMessageType.DocumentSchemaChange: {
|
|
2980
|
+
this.reSubmit(resubmitData);
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
default: {
|
|
2984
|
+
unreachableCase(message.type);
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2912
2987
|
}
|
|
2913
2988
|
/**
|
|
2914
|
-
*
|
|
2915
|
-
*
|
|
2916
|
-
*
|
|
2917
|
-
* @param message - The original LocalContainerRuntimeMessage.
|
|
2918
|
-
* @param localOpMetadata - The local metadata associated with the original message.
|
|
2989
|
+
* Resubmit the given message which was previously submitted to the ContainerRuntime but not successfully
|
|
2990
|
+
* transmitted to the ordering service (e.g. due to a disconnect, or being in Staging Mode)
|
|
2991
|
+
* How to resubmit is up to the subsystem that submitted the op to begin with
|
|
2919
2992
|
*/
|
|
2920
|
-
|
|
2921
|
-
assert(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
|
|
2993
|
+
reSubmit({ runtimeOp: message, localOpMetadata, opMetadata, }) {
|
|
2922
2994
|
switch (message.type) {
|
|
2923
2995
|
case ContainerMessageType.FluidDataStoreOp:
|
|
2924
2996
|
case ContainerMessageType.Attach:
|
|
2925
2997
|
case ContainerMessageType.Alias: {
|
|
2926
2998
|
// For Operations, call resubmitDataStoreOp which will find the right store
|
|
2927
2999
|
// and trigger resubmission on it.
|
|
2928
|
-
this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata,
|
|
3000
|
+
this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata,
|
|
3001
|
+
/* squash: */ false);
|
|
2929
3002
|
break;
|
|
2930
3003
|
}
|
|
2931
3004
|
case ContainerMessageType.IdAllocation: {
|
|
@@ -2951,9 +3024,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2951
3024
|
break;
|
|
2952
3025
|
}
|
|
2953
3026
|
case ContainerMessageType.DocumentSchemaChange: {
|
|
2954
|
-
//
|
|
2955
|
-
//
|
|
2956
|
-
|
|
3027
|
+
// We shouldn't directly resubmit due to Compare-And-Swap semantics.
|
|
3028
|
+
// If needed it will be generated from scratch before other ops are submitted.
|
|
3029
|
+
this.documentsSchemaController.pendingOpNotAcked();
|
|
2957
3030
|
break;
|
|
2958
3031
|
}
|
|
2959
3032
|
default: {
|
|
@@ -2963,8 +3036,11 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2963
3036
|
}
|
|
2964
3037
|
}
|
|
2965
3038
|
}
|
|
2966
|
-
|
|
2967
|
-
|
|
3039
|
+
/**
|
|
3040
|
+
* Rollback the given op which was only staged but not yet submitted.
|
|
3041
|
+
*/
|
|
3042
|
+
rollbackStagedChanges({ type, contents }, localOpMetadata) {
|
|
3043
|
+
assert(canStageMessageOfType(type), 0xbbc /* Unexpected message type to be rolled back */);
|
|
2968
3044
|
switch (type) {
|
|
2969
3045
|
case ContainerMessageType.FluidDataStoreOp: {
|
|
2970
3046
|
// For operations, call rollbackDataStoreOp which will find the right store
|
|
@@ -2972,8 +3048,24 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2972
3048
|
this.channelCollection.rollback(type, contents, localOpMetadata);
|
|
2973
3049
|
break;
|
|
2974
3050
|
}
|
|
3051
|
+
case ContainerMessageType.GC: {
|
|
3052
|
+
// Just drop it, but log an error, this is not expected and not ideal, but not critical failure either.
|
|
3053
|
+
// Currently the only expected type here is TombstoneLoaded, which will have been preceded by one of these events as well:
|
|
3054
|
+
// GC_Tombstone_DataStore_Requested, GC_Tombstone_SubDataStore_Requested, GC_Tombstone_Blob_Requested
|
|
3055
|
+
this.mc.logger.sendErrorEvent({
|
|
3056
|
+
eventName: "GC_OpDiscarded",
|
|
3057
|
+
details: { subType: contents.type },
|
|
3058
|
+
});
|
|
3059
|
+
break;
|
|
3060
|
+
}
|
|
3061
|
+
case ContainerMessageType.DocumentSchemaChange: {
|
|
3062
|
+
// Notify the document schema controller that the pending op was not acked.
|
|
3063
|
+
// This will allow it to propose the schema change again if needed.
|
|
3064
|
+
this.documentsSchemaController.pendingOpNotAcked();
|
|
3065
|
+
break;
|
|
3066
|
+
}
|
|
2975
3067
|
default: {
|
|
2976
|
-
|
|
3068
|
+
unreachableCase(type);
|
|
2977
3069
|
}
|
|
2978
3070
|
}
|
|
2979
3071
|
}
|
|
@@ -3138,6 +3230,30 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
3138
3230
|
return this.summaryManager.enqueueSummarize(options);
|
|
3139
3231
|
}
|
|
3140
3232
|
}
|
|
3233
|
+
acquireExtension(id, factory, ...useContext) {
|
|
3234
|
+
let entry = this.extensions.get(id);
|
|
3235
|
+
if (entry === undefined) {
|
|
3236
|
+
const runtime = {
|
|
3237
|
+
isConnected: () => this.connected,
|
|
3238
|
+
getClientId: () => this.clientId,
|
|
3239
|
+
events: this.lazyEventsForExtensions.value,
|
|
3240
|
+
logger: this.baseLogger,
|
|
3241
|
+
submitAddressedSignal: (addressChain, message) => {
|
|
3242
|
+
this.submitExtensionSignal(id, addressChain, message);
|
|
3243
|
+
},
|
|
3244
|
+
getQuorum: this.getQuorum.bind(this),
|
|
3245
|
+
getAudience: this.getAudience.bind(this),
|
|
3246
|
+
supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
|
|
3247
|
+
};
|
|
3248
|
+
entry = new factory(runtime, ...useContext);
|
|
3249
|
+
this.extensions.set(id, entry);
|
|
3250
|
+
}
|
|
3251
|
+
else {
|
|
3252
|
+
assert(entry instanceof factory, 0xba1 /* Extension entry is not of the expected type */);
|
|
3253
|
+
entry.extension.onNewUse(...useContext);
|
|
3254
|
+
}
|
|
3255
|
+
return entry.interface;
|
|
3256
|
+
}
|
|
3141
3257
|
get groupedBatchingEnabled() {
|
|
3142
3258
|
return this.sessionSchema.opGroupingEnabled === true;
|
|
3143
3259
|
}
|
|
@@ -3149,4 +3265,33 @@ export function createNewSignalEnvelope(address, type, content) {
|
|
|
3149
3265
|
};
|
|
3150
3266
|
return newEnvelope;
|
|
3151
3267
|
}
|
|
3268
|
+
export function isContainerMessageDirtyable({ type, contents, }) {
|
|
3269
|
+
// Certain container runtime messages should not mark the container dirty such as the old built-in
|
|
3270
|
+
// AgentScheduler and Garbage collector messages.
|
|
3271
|
+
switch (type) {
|
|
3272
|
+
case ContainerMessageType.Attach: {
|
|
3273
|
+
const attachMessage = contents;
|
|
3274
|
+
if (attachMessage.id === agentSchedulerId) {
|
|
3275
|
+
return false;
|
|
3276
|
+
}
|
|
3277
|
+
break;
|
|
3278
|
+
}
|
|
3279
|
+
case ContainerMessageType.FluidDataStoreOp: {
|
|
3280
|
+
const envelope = contents;
|
|
3281
|
+
if (envelope.address === agentSchedulerId) {
|
|
3282
|
+
return false;
|
|
3283
|
+
}
|
|
3284
|
+
break;
|
|
3285
|
+
}
|
|
3286
|
+
case ContainerMessageType.IdAllocation:
|
|
3287
|
+
case ContainerMessageType.DocumentSchemaChange:
|
|
3288
|
+
case ContainerMessageType.GC: {
|
|
3289
|
+
return false;
|
|
3290
|
+
}
|
|
3291
|
+
default: {
|
|
3292
|
+
break;
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
return true;
|
|
3296
|
+
}
|
|
3152
3297
|
//# sourceMappingURL=containerRuntime.js.map
|