@fluidframework/container-runtime 2.41.0 → 2.43.0-343119
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 +4 -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 +24 -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 +36 -15
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +186 -71
- 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 +2 -2
- 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/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/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 +18 -5
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +20 -13
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/summary/documentSchema.d.ts +79 -16
- package/dist/summary/documentSchema.d.ts.map +1 -1
- package/dist/summary/documentSchema.js +119 -53
- 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 +24 -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 +36 -15
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +188 -73
- 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 +2 -2
- 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/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/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 +18 -5
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +20 -13
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/summary/documentSchema.d.ts +79 -16
- package/lib/summary/documentSchema.d.ts.map +1 -1
- package/lib/summary/documentSchema.js +119 -53
- 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 +18 -18
- package/src/channelCollection.ts +4 -4
- package/src/compatUtils.ts +147 -10
- package/src/containerRuntime.ts +242 -85
- package/src/dataStore.ts +7 -0
- package/src/gc/garbageCollection.ts +2 -0
- package/src/gc/gcDefinitions.ts +1 -1
- package/src/index.ts +4 -2
- package/src/messageTypes.ts +12 -5
- package/src/metadata.ts +1 -1
- 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/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +49 -22
- package/src/summary/documentSchema.ts +228 -83
- package/src/summary/index.ts +3 -1
package/lib/containerRuntime.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
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, Lazy, LazyPromise, PromiseCache, delay, fail, } 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";
|
|
@@ -15,11 +15,12 @@ import { GCDataBuilder, RequestParser, RuntimeHeaders, TelemetryContext, addBlob
|
|
|
15
15
|
import { DataCorruptionError, DataProcessingError, extractSafePropertiesFromMessage, GenericError, LoggingError, PerformanceEvent,
|
|
16
16
|
// eslint-disable-next-line import/no-deprecated
|
|
17
17
|
TaggedLoggerAdapter, UsageError, createChildLogger, createChildMonitoringContext, createSampledLogger, loggerToMonitoringContext, raiseConnectedEvent, wrapError, tagCodeArtifacts, normalizeError, } from "@fluidframework/telemetry-utils/internal";
|
|
18
|
+
import { gt } from "semver-ts";
|
|
18
19
|
import { v4 as uuid } from "uuid";
|
|
19
20
|
import { BindBatchTracker } from "./batchTracker.js";
|
|
20
21
|
import { BlobManager, blobManagerBasePath, blobsTreeName, isBlobPath, loadBlobManagerLoadInfo, } from "./blobManager/index.js";
|
|
21
22
|
import { ChannelCollection, getSummaryForDatastores, wrapContext, } from "./channelCollection.js";
|
|
22
|
-
import { defaultMinVersionForCollab, getMinVersionForCollabDefaults, isValidMinVersionForCollab, } from "./compatUtils.js";
|
|
23
|
+
import { defaultMinVersionForCollab, getMinVersionForCollabDefaults, isValidMinVersionForCollab, validateRuntimeOptions, } from "./compatUtils.js";
|
|
23
24
|
import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
|
|
24
25
|
import { ReportOpPerfTelemetry } from "./connectionTelemetry.js";
|
|
25
26
|
import { ContainerFluidHandleContext } from "./containerHandleContext.js";
|
|
@@ -191,6 +192,20 @@ export async function loadContainerRuntime(params) {
|
|
|
191
192
|
return ContainerRuntime.loadRuntime(params);
|
|
192
193
|
}
|
|
193
194
|
const defaultMaxConsecutiveReconnects = 7;
|
|
195
|
+
/**
|
|
196
|
+
* These are the ONLY message types that are allowed to be submitted while in staging mode
|
|
197
|
+
* (Does not apply to pre-StagingMode batches that are resubmitted, those are not considered to be staged)
|
|
198
|
+
*/
|
|
199
|
+
function canStageMessageOfType(type) {
|
|
200
|
+
return (
|
|
201
|
+
// These are user changes coming up from the runtime's DataStores
|
|
202
|
+
type === ContainerMessageType.FluidDataStoreOp ||
|
|
203
|
+
// GC ops are used to detect issues in the reference graph so all clients can repair their GC state.
|
|
204
|
+
// These can be submitted at any time, including while in Staging Mode.
|
|
205
|
+
type === ContainerMessageType.GC ||
|
|
206
|
+
// These are typically sent shortly after boot and will not be common in Staging Mode, but it's possible.
|
|
207
|
+
type === ContainerMessageType.DocumentSchemaChange);
|
|
208
|
+
}
|
|
194
209
|
/**
|
|
195
210
|
* Represents the runtime of the container. Contains helper functions/state of the container.
|
|
196
211
|
* It will define the store level mappings.
|
|
@@ -239,6 +254,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
239
254
|
if (!isValidMinVersionForCollab(minVersionForCollab)) {
|
|
240
255
|
throw new UsageError(`Invalid minVersionForCollab: ${minVersionForCollab}. It must be an existing FF version (i.e. 2.22.1).`);
|
|
241
256
|
}
|
|
257
|
+
// We also validate that there is not a mismatch between `minVersionForCollab` and runtime options that
|
|
258
|
+
// were manually set.
|
|
259
|
+
validateRuntimeOptions(minVersionForCollab, runtimeOptions);
|
|
242
260
|
const defaultsAffectingDocSchema = getMinVersionForCollabDefaults(minVersionForCollab);
|
|
243
261
|
// The following are the default values for the options that do not affect the DocumentSchema.
|
|
244
262
|
const defaultsNotAffectingDocSchema = {
|
|
@@ -396,7 +414,13 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
396
414
|
disallowedVersions: [],
|
|
397
415
|
}, (schema) => {
|
|
398
416
|
runtime.onSchemaChange(schema);
|
|
399
|
-
});
|
|
417
|
+
}, { minVersionForCollab }, logger);
|
|
418
|
+
// If the minVersionForCollab for this client is greater than the existing one, we should use that one going forward.
|
|
419
|
+
const existingMinVersionForCollab = documentSchemaController.sessionSchema.info.minVersionForCollab;
|
|
420
|
+
const updatedMinVersionForCollab = existingMinVersionForCollab === undefined ||
|
|
421
|
+
gt(minVersionForCollab, existingMinVersionForCollab)
|
|
422
|
+
? minVersionForCollab
|
|
423
|
+
: existingMinVersionForCollab;
|
|
400
424
|
if (compressionLz4 && !enableGroupedBatching) {
|
|
401
425
|
throw new UsageError("If compression is enabled, op grouping must be enabled too");
|
|
402
426
|
}
|
|
@@ -415,7 +439,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
415
439
|
explicitSchemaControl,
|
|
416
440
|
createBlobPayloadPending,
|
|
417
441
|
};
|
|
418
|
-
const runtime = new containerRuntimeCtor(context, registry, metadata, electedSummarizerData, chunks ?? [], aliases ?? [], internalRuntimeOptions, containerScope, logger, existing, blobManagerLoadInfo, context.storage, createIdCompressorFn, documentSchemaController, featureGatesForTelemetry, provideEntryPoint,
|
|
442
|
+
const runtime = new containerRuntimeCtor(context, registry, metadata, electedSummarizerData, chunks ?? [], aliases ?? [], internalRuntimeOptions, containerScope, logger, existing, blobManagerLoadInfo, context.storage, createIdCompressorFn, documentSchemaController, featureGatesForTelemetry, provideEntryPoint, updatedMinVersionForCollab, requestHandler, undefined, // summaryConfiguration
|
|
419
443
|
recentBatchInfo);
|
|
420
444
|
runtime.blobManager.stashedBlobsUploadP.then(() => {
|
|
421
445
|
// make sure we didn't reconnect before the promise resolved
|
|
@@ -504,8 +528,12 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
504
528
|
ensureNoDataModelChanges(callback) {
|
|
505
529
|
return this.dataModelChangeRunner.run(callback);
|
|
506
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Indicates whether the container is in a state where it is able to send
|
|
533
|
+
* ops (connected to op stream and not in readonly mode).
|
|
534
|
+
*/
|
|
507
535
|
get connected() {
|
|
508
|
-
return this.
|
|
536
|
+
return this.canSendOps;
|
|
509
537
|
}
|
|
510
538
|
/**
|
|
511
539
|
* clientId of parent (non-summarizing) container that owns summarizer container
|
|
@@ -593,20 +621,31 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
593
621
|
// eslint-disable-next-line import/no-deprecated
|
|
594
622
|
this.enterStagingMode = () => {
|
|
595
623
|
if (this.stageControls !== undefined) {
|
|
596
|
-
throw new
|
|
624
|
+
throw new UsageError("already in staging mode");
|
|
625
|
+
}
|
|
626
|
+
if (this.attachState === AttachState.Detached) {
|
|
627
|
+
throw new UsageError("cannot enter staging mode while detached");
|
|
597
628
|
}
|
|
598
|
-
// Make sure
|
|
629
|
+
// Make sure Outbox is empty before entering staging mode,
|
|
599
630
|
// since we mark whole batches as "staged" or not to indicate whether to submit them.
|
|
600
|
-
this.
|
|
631
|
+
this.flush();
|
|
601
632
|
const exitStagingMode = (discardOrCommit) => () => {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
633
|
+
try {
|
|
634
|
+
// Final flush of any last staged changes
|
|
635
|
+
// NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
|
|
636
|
+
this.outbox.flush();
|
|
637
|
+
this.stageControls = undefined;
|
|
638
|
+
// During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
|
|
639
|
+
// Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
|
|
640
|
+
this.submitIdAllocationOpIfNeeded({ staged: false });
|
|
641
|
+
discardOrCommit();
|
|
642
|
+
this.channelCollection.notifyStagingMode(false);
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
const normalizedError = normalizeError(error);
|
|
646
|
+
this.closeFn(normalizedError);
|
|
647
|
+
throw normalizedError;
|
|
648
|
+
}
|
|
610
649
|
};
|
|
611
650
|
// eslint-disable-next-line import/no-deprecated
|
|
612
651
|
const stageControls = {
|
|
@@ -614,7 +653,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
614
653
|
// Pop all staged batches from the PSM and roll them back in LIFO order
|
|
615
654
|
this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
|
|
616
655
|
assert(runtimeOp !== undefined, 0xb82 /* Staged batches expected to have runtimeOp defined */);
|
|
617
|
-
this.
|
|
656
|
+
this.rollbackStagedChanges(runtimeOp, localOpMetadata);
|
|
618
657
|
});
|
|
619
658
|
this.updateDocumentDirtyState();
|
|
620
659
|
}),
|
|
@@ -651,6 +690,11 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
651
690
|
this.mc = createChildMonitoringContext({
|
|
652
691
|
logger: this.baseLogger,
|
|
653
692
|
namespace: "ContainerRuntime",
|
|
693
|
+
properties: {
|
|
694
|
+
all: {
|
|
695
|
+
inStagingMode: this.inStagingMode,
|
|
696
|
+
},
|
|
697
|
+
},
|
|
654
698
|
});
|
|
655
699
|
// If we support multiple algorithms in the future, then we would need to manage it here carefully.
|
|
656
700
|
// We can use runtimeOptions.compressionOptions.compressionAlgorithm, but only if it's in the schema list!
|
|
@@ -691,7 +735,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
691
735
|
// Values are generally expected to be set from the runtime side.
|
|
692
736
|
this.options = options ?? {};
|
|
693
737
|
this.clientDetails = clientDetails;
|
|
694
|
-
|
|
738
|
+
this.isSummarizerClient = this.clientDetails.type === summarizerClientType;
|
|
695
739
|
this.loadedFromVersionId = context.getLoadedFromVersion()?.id;
|
|
696
740
|
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
697
741
|
this._getClientId = () => context.clientId;
|
|
@@ -728,7 +772,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
728
772
|
details: { attachState: this.attachState },
|
|
729
773
|
}));
|
|
730
774
|
// In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
|
|
731
|
-
this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
|
|
775
|
+
this.closeFn = this.isSummarizerClient ? this.disposeFn : closeFn;
|
|
732
776
|
let loadSummaryNumber;
|
|
733
777
|
// Get the container creation metadata. For new container, we initialize these. For existing containers,
|
|
734
778
|
// get the values from the metadata blob.
|
|
@@ -752,7 +796,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
752
796
|
this.messageAtLastSummary = lastMessageFromMetadata(metadata);
|
|
753
797
|
// Note that we only need to pull the *initial* connected state from the context.
|
|
754
798
|
// Later updates come through calls to setConnectionState.
|
|
755
|
-
this.
|
|
799
|
+
this.canSendOps = connected;
|
|
756
800
|
this.mc.logger.sendTelemetryEvent({
|
|
757
801
|
eventName: "GCFeatureMatrix",
|
|
758
802
|
metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
|
|
@@ -845,7 +889,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
845
889
|
existing,
|
|
846
890
|
metadata,
|
|
847
891
|
createContainerMetadata: this.createContainerMetadata,
|
|
848
|
-
isSummarizerClient,
|
|
892
|
+
isSummarizerClient: this.isSummarizerClient,
|
|
849
893
|
getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
|
|
850
894
|
getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
|
|
851
895
|
readAndParseBlob: async (id) => readAndParse(this.storage, id),
|
|
@@ -929,7 +973,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
929
973
|
// Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
|
|
930
974
|
this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
|
|
931
975
|
this.outbox = new Outbox({
|
|
932
|
-
shouldSend: () => this.
|
|
976
|
+
shouldSend: () => this.shouldSendOps(),
|
|
933
977
|
pendingStateManager: this.pendingStateManager,
|
|
934
978
|
submitBatchFn,
|
|
935
979
|
legacySendBatchFn,
|
|
@@ -1068,7 +1112,14 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1068
1112
|
await this.initializeSummarizer(loader);
|
|
1069
1113
|
if (this.sessionSchema.idCompressorMode === "on" ||
|
|
1070
1114
|
(this.sessionSchema.idCompressorMode === "delayed" && this.connected)) {
|
|
1071
|
-
this.
|
|
1115
|
+
PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnBoot" }, (event) => {
|
|
1116
|
+
this._idCompressor = this.createIdCompressorFn();
|
|
1117
|
+
event.end({
|
|
1118
|
+
details: {
|
|
1119
|
+
idCompressorMode: this.sessionSchema.idCompressorMode,
|
|
1120
|
+
},
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1072
1123
|
// This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
|
|
1073
1124
|
assert(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
|
|
1074
1125
|
}
|
|
@@ -1097,8 +1148,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1097
1148
|
const orderedClientCollection = new OrderedClientCollection(orderedClientLogger, this.innerDeltaManager, this._quorum);
|
|
1098
1149
|
const orderedClientElectionForSummarizer = new OrderedClientElection(orderedClientLogger, orderedClientCollection, this.electedSummarizerData ?? this.innerDeltaManager.lastSequenceNumber, SummarizerClientElection.isClientEligible, this.mc.config.getBoolean("Fluid.ContainerRuntime.OrderedClientElection.EnablePerformanceEvents"));
|
|
1099
1150
|
this.summarizerClientElection = new SummarizerClientElection(orderedClientLogger, summaryCollection, orderedClientElectionForSummarizer, maxOpsSinceLastSummary);
|
|
1100
|
-
|
|
1101
|
-
if (isSummarizerClient) {
|
|
1151
|
+
if (this.isSummarizerClient) {
|
|
1102
1152
|
// We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
|
|
1103
1153
|
// so that all non summarizer clients don't have to load the code inside this module.
|
|
1104
1154
|
const module = await import(
|
|
@@ -1446,7 +1496,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1446
1496
|
}
|
|
1447
1497
|
replayPendingStates() {
|
|
1448
1498
|
// We need to be able to send ops to replay states
|
|
1449
|
-
if (!this.
|
|
1499
|
+
if (!this.shouldSendOps()) {
|
|
1450
1500
|
return;
|
|
1451
1501
|
}
|
|
1452
1502
|
// Replaying is an internal operation and we don't want to generate noise while doing it.
|
|
@@ -1528,25 +1578,35 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1528
1578
|
loadIdCompressor() {
|
|
1529
1579
|
if (this._idCompressor === undefined &&
|
|
1530
1580
|
this.sessionSchema.idCompressorMode !== undefined) {
|
|
1531
|
-
this.
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1581
|
+
PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnDelayedLoad" }, (event) => {
|
|
1582
|
+
this._idCompressor = this.createIdCompressorFn();
|
|
1583
|
+
// Finalize any ranges we received while the compressor was turned off.
|
|
1584
|
+
const ops = this.pendingIdCompressorOps;
|
|
1585
|
+
this.pendingIdCompressorOps = [];
|
|
1586
|
+
const trace = Trace.start();
|
|
1587
|
+
for (const range of ops) {
|
|
1588
|
+
this._idCompressor.finalizeCreationRange(range);
|
|
1589
|
+
}
|
|
1590
|
+
event.end({
|
|
1591
|
+
details: {
|
|
1592
|
+
finalizeCreationRangeDuration: trace.trace().duration,
|
|
1593
|
+
idCompressorMode: this.sessionSchema.idCompressorMode,
|
|
1594
|
+
pendingIdCompressorOps: ops.length,
|
|
1595
|
+
},
|
|
1596
|
+
});
|
|
1597
|
+
});
|
|
1538
1598
|
assert(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
|
|
1539
1599
|
}
|
|
1540
1600
|
}
|
|
1541
|
-
setConnectionState(
|
|
1601
|
+
setConnectionState(canSendOps, clientId) {
|
|
1542
1602
|
// Validate we have consistent state
|
|
1543
1603
|
const currentClientId = this._audience.getSelf()?.clientId;
|
|
1544
1604
|
assert(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
|
|
1545
1605
|
assert(this.clientId === currentClientId, 0x978 /* this.clientId does not match Audience */);
|
|
1546
|
-
if (
|
|
1606
|
+
if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
|
|
1547
1607
|
this.loadIdCompressor();
|
|
1548
1608
|
}
|
|
1549
|
-
if (
|
|
1609
|
+
if (canSendOps === false && this.delayConnectClientId !== undefined) {
|
|
1550
1610
|
this.delayConnectClientId = undefined;
|
|
1551
1611
|
this.mc.logger.sendTelemetryEvent({
|
|
1552
1612
|
eventName: "UnsuccessfulConnectedTransition",
|
|
@@ -1554,37 +1614,39 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1554
1614
|
// Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
|
|
1555
1615
|
return;
|
|
1556
1616
|
}
|
|
1557
|
-
if (!connected) {
|
|
1558
|
-
this.documentsSchemaController.onDisconnect();
|
|
1559
|
-
}
|
|
1560
1617
|
// If there are stashed blobs in the pending state, we need to delay
|
|
1561
1618
|
// propagation of the "connected" event until we have uploaded them to
|
|
1562
1619
|
// ensure we don't submit ops referencing a blob that has not been uploaded
|
|
1563
|
-
const connecting =
|
|
1620
|
+
const connecting = canSendOps && !this.canSendOps;
|
|
1564
1621
|
if (connecting && this.blobManager.hasPendingStashedUploads()) {
|
|
1565
1622
|
assert(!this.delayConnectClientId, 0x791 /* Connect event delay must be canceled before subsequent connect event */);
|
|
1566
1623
|
assert(!!clientId, 0x792 /* Must have clientId when connecting */);
|
|
1567
1624
|
this.delayConnectClientId = clientId;
|
|
1568
1625
|
return;
|
|
1569
1626
|
}
|
|
1570
|
-
this.setConnectionStateCore(
|
|
1627
|
+
this.setConnectionStateCore(canSendOps, clientId);
|
|
1571
1628
|
}
|
|
1572
|
-
|
|
1629
|
+
/**
|
|
1630
|
+
* Raises and propagates connected events.
|
|
1631
|
+
* @param canSendOps - Indicates whether the container can send ops or not (connected and not readonly).
|
|
1632
|
+
* @remarks The connection state from container context used here when raising connected events.
|
|
1633
|
+
*/
|
|
1634
|
+
setConnectionStateCore(canSendOps, clientId) {
|
|
1573
1635
|
assert(!this.delayConnectClientId, 0x394 /* connect event delay must be cleared before propagating connect event */);
|
|
1574
1636
|
this.verifyNotClosed();
|
|
1575
1637
|
// There might be no change of state due to Container calling this API after loading runtime.
|
|
1576
|
-
const
|
|
1577
|
-
const reconnection =
|
|
1638
|
+
const canSendOpsChanged = this.canSendOps !== canSendOps;
|
|
1639
|
+
const reconnection = canSendOpsChanged && !canSendOps;
|
|
1578
1640
|
// We need to flush the ops currently collected by Outbox to preserve original order.
|
|
1579
1641
|
// This flush NEEDS to happen before we set the ContainerRuntime to "connected".
|
|
1580
1642
|
// We want these ops to get to the PendingStateManager without sending to service and have them return to the Outbox upon calling "replayPendingStates".
|
|
1581
|
-
if (
|
|
1643
|
+
if (canSendOpsChanged && canSendOps) {
|
|
1582
1644
|
this.flush();
|
|
1583
1645
|
}
|
|
1584
|
-
this.
|
|
1585
|
-
if (
|
|
1646
|
+
this.canSendOps = canSendOps;
|
|
1647
|
+
if (canSendOps) {
|
|
1586
1648
|
assert(this.attachState === AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
|
|
1587
|
-
if (
|
|
1649
|
+
if (canSendOpsChanged) {
|
|
1588
1650
|
this.signalTelemetryManager.resetTracking();
|
|
1589
1651
|
}
|
|
1590
1652
|
}
|
|
@@ -1600,12 +1662,12 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1600
1662
|
return;
|
|
1601
1663
|
}
|
|
1602
1664
|
}
|
|
1603
|
-
if (
|
|
1665
|
+
if (canSendOpsChanged) {
|
|
1604
1666
|
this.replayPendingStates();
|
|
1605
1667
|
}
|
|
1606
|
-
this.channelCollection.setConnectionState(
|
|
1607
|
-
this.garbageCollector.setConnectionState(
|
|
1608
|
-
raiseConnectedEvent(this.mc.logger, this, connected
|
|
1668
|
+
this.channelCollection.setConnectionState(canSendOps, clientId);
|
|
1669
|
+
this.garbageCollector.setConnectionState(canSendOps, clientId);
|
|
1670
|
+
raiseConnectedEvent(this.mc.logger, this, this.connected /* canSendOps */, clientId);
|
|
1609
1671
|
}
|
|
1610
1672
|
async notifyOpReplay(message) {
|
|
1611
1673
|
await this.pendingStateManager.applyStashedOpsAt(message.sequenceNumber);
|
|
@@ -2012,7 +2074,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2012
2074
|
if (checkpoint) {
|
|
2013
2075
|
// This will throw and close the container if rollback fails
|
|
2014
2076
|
try {
|
|
2015
|
-
checkpoint.rollback((message) =>
|
|
2077
|
+
checkpoint.rollback((message) =>
|
|
2078
|
+
// These changes are staged since we entered staging mode above
|
|
2079
|
+
this.rollbackStagedChanges(message.runtimeOp, message.localOpMetadata));
|
|
2016
2080
|
this.updateDocumentDirtyState();
|
|
2017
2081
|
stageControls?.discardChanges();
|
|
2018
2082
|
stageControls = undefined;
|
|
@@ -2089,7 +2153,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2089
2153
|
const context = this.channelCollection.createDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], loadingGroupId);
|
|
2090
2154
|
return channelToDataStore(await context.realize(), context.id, this.channelCollection, this.mc.logger);
|
|
2091
2155
|
}
|
|
2092
|
-
|
|
2156
|
+
shouldSendOps() {
|
|
2093
2157
|
// Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
|
|
2094
2158
|
// container runtime's ability to send ops depend on the actual readonly state of the delta manager.
|
|
2095
2159
|
return (this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure);
|
|
@@ -2778,6 +2842,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2778
2842
|
try {
|
|
2779
2843
|
// If we're resubmitting a batch, keep the same "staged" value as before. Otherwise, use the current "global" state.
|
|
2780
2844
|
const staged = this.batchRunner.resubmitInfo?.staged ?? this.inStagingMode;
|
|
2845
|
+
assert(!staged || canStageMessageOfType(type), 0xbba /* Unexpected message type submitted in Staging Mode */);
|
|
2781
2846
|
// Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
|
|
2782
2847
|
if (!staged) {
|
|
2783
2848
|
this.submitIdAllocationOpIfNeeded({ staged: false });
|
|
@@ -2785,7 +2850,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2785
2850
|
// Allow document schema controller to send a message if it needs to propose change in document schema.
|
|
2786
2851
|
// If it needs to send a message, it will call provided callback with payload of such message and rely
|
|
2787
2852
|
// on this callback to do actual sending.
|
|
2788
|
-
const schemaChangeMessage = this.documentsSchemaController.
|
|
2853
|
+
const schemaChangeMessage = this.documentsSchemaController.maybeGenerateSchemaMessage();
|
|
2789
2854
|
if (schemaChangeMessage) {
|
|
2790
2855
|
this.mc.logger.sendTelemetryEvent({
|
|
2791
2856
|
eventName: "SchemaChangeProposal",
|
|
@@ -2794,6 +2859,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2794
2859
|
newRuntimeSchema: JSON.stringify(schemaChangeMessage.runtime),
|
|
2795
2860
|
sessionRuntimeSchema: JSON.stringify(this.sessionSchema),
|
|
2796
2861
|
oldRuntimeSchema: JSON.stringify(this.metadata?.documentSchema?.runtime),
|
|
2862
|
+
minVersionForCollab: schemaChangeMessage.info?.minVersionForCollab,
|
|
2797
2863
|
});
|
|
2798
2864
|
const msg = {
|
|
2799
2865
|
type: ContainerMessageType.DocumentSchemaChange,
|
|
@@ -2883,43 +2949,72 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2883
2949
|
}
|
|
2884
2950
|
/**
|
|
2885
2951
|
* Resubmits each message in the batch, and then flushes the outbox.
|
|
2952
|
+
* This typically happens when we reconnect and there are pending messages.
|
|
2953
|
+
*
|
|
2954
|
+
* @remarks
|
|
2955
|
+
* Attempting to resubmit a batch that has been successfully sequenced will not happen due to
|
|
2956
|
+
* checks in the ConnectionStateHandler (Loader layer)
|
|
2886
2957
|
*
|
|
2887
|
-
*
|
|
2958
|
+
* The only exception to this would be if the Container "forks" due to misuse of the "Offline Load" feature.
|
|
2959
|
+
* If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
|
|
2888
2960
|
* for correlation to detect container forking.
|
|
2889
2961
|
*/
|
|
2890
2962
|
reSubmitBatch(batch, { batchId, staged, squash }) {
|
|
2963
|
+
assert(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
|
|
2891
2964
|
const resubmitInfo = {
|
|
2892
2965
|
// Only include Batch ID if "Offline Load" feature is enabled
|
|
2893
2966
|
// It's only needed to identify batches across container forks arising from misuse of offline load.
|
|
2894
2967
|
batchId: this.offlineEnabled ? batchId : undefined,
|
|
2895
2968
|
staged,
|
|
2896
2969
|
};
|
|
2970
|
+
const resubmitFn = squash
|
|
2971
|
+
? this.reSubmitWithSquashing.bind(this)
|
|
2972
|
+
: this.reSubmit.bind(this);
|
|
2897
2973
|
this.batchRunner.run(() => {
|
|
2898
2974
|
for (const message of batch) {
|
|
2899
|
-
|
|
2975
|
+
resubmitFn(message);
|
|
2900
2976
|
}
|
|
2901
2977
|
}, resubmitInfo);
|
|
2902
2978
|
this.flush(resubmitInfo);
|
|
2903
2979
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2980
|
+
/**
|
|
2981
|
+
* Resubmit the given message as part of a squash rebase upon exiting Staging Mode.
|
|
2982
|
+
* How exactly to resubmit the message is up to the subsystem that submitted the op to begin with.
|
|
2983
|
+
*/
|
|
2984
|
+
reSubmitWithSquashing(resubmitData) {
|
|
2985
|
+
const message = resubmitData.runtimeOp;
|
|
2986
|
+
assert(canStageMessageOfType(message.type), 0xbbb /* Expected message type to be compatible with staging */);
|
|
2987
|
+
switch (message.type) {
|
|
2988
|
+
case ContainerMessageType.FluidDataStoreOp: {
|
|
2989
|
+
this.channelCollection.reSubmit(message.type, message.contents, resubmitData.localOpMetadata,
|
|
2990
|
+
/* squash: */ true);
|
|
2991
|
+
break;
|
|
2992
|
+
}
|
|
2993
|
+
// NOTE: Squash doesn't apply to GC or DocumentSchemaChange ops, fallback to typical resubmit logic.
|
|
2994
|
+
case ContainerMessageType.GC:
|
|
2995
|
+
case ContainerMessageType.DocumentSchemaChange: {
|
|
2996
|
+
this.reSubmit(resubmitData);
|
|
2997
|
+
break;
|
|
2998
|
+
}
|
|
2999
|
+
default: {
|
|
3000
|
+
unreachableCase(message.type);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
2906
3003
|
}
|
|
2907
3004
|
/**
|
|
2908
|
-
*
|
|
2909
|
-
*
|
|
2910
|
-
*
|
|
2911
|
-
* @param message - The original LocalContainerRuntimeMessage.
|
|
2912
|
-
* @param localOpMetadata - The local metadata associated with the original message.
|
|
3005
|
+
* Resubmit the given message which was previously submitted to the ContainerRuntime but not successfully
|
|
3006
|
+
* transmitted to the ordering service (e.g. due to a disconnect, or being in Staging Mode)
|
|
3007
|
+
* How to resubmit is up to the subsystem that submitted the op to begin with
|
|
2913
3008
|
*/
|
|
2914
|
-
|
|
2915
|
-
assert(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
|
|
3009
|
+
reSubmit({ runtimeOp: message, localOpMetadata, opMetadata, }) {
|
|
2916
3010
|
switch (message.type) {
|
|
2917
3011
|
case ContainerMessageType.FluidDataStoreOp:
|
|
2918
3012
|
case ContainerMessageType.Attach:
|
|
2919
3013
|
case ContainerMessageType.Alias: {
|
|
2920
3014
|
// For Operations, call resubmitDataStoreOp which will find the right store
|
|
2921
3015
|
// and trigger resubmission on it.
|
|
2922
|
-
this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata,
|
|
3016
|
+
this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata,
|
|
3017
|
+
/* squash: */ false);
|
|
2923
3018
|
break;
|
|
2924
3019
|
}
|
|
2925
3020
|
case ContainerMessageType.IdAllocation: {
|
|
@@ -2945,9 +3040,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2945
3040
|
break;
|
|
2946
3041
|
}
|
|
2947
3042
|
case ContainerMessageType.DocumentSchemaChange: {
|
|
2948
|
-
//
|
|
2949
|
-
//
|
|
2950
|
-
|
|
3043
|
+
// We shouldn't directly resubmit due to Compare-And-Swap semantics.
|
|
3044
|
+
// If needed it will be generated from scratch before other ops are submitted.
|
|
3045
|
+
this.documentsSchemaController.pendingOpNotAcked();
|
|
2951
3046
|
break;
|
|
2952
3047
|
}
|
|
2953
3048
|
default: {
|
|
@@ -2957,8 +3052,11 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2957
3052
|
}
|
|
2958
3053
|
}
|
|
2959
3054
|
}
|
|
2960
|
-
|
|
2961
|
-
|
|
3055
|
+
/**
|
|
3056
|
+
* Rollback the given op which was only staged but not yet submitted.
|
|
3057
|
+
*/
|
|
3058
|
+
rollbackStagedChanges({ type, contents }, localOpMetadata) {
|
|
3059
|
+
assert(canStageMessageOfType(type), 0xbbc /* Unexpected message type to be rolled back */);
|
|
2962
3060
|
switch (type) {
|
|
2963
3061
|
case ContainerMessageType.FluidDataStoreOp: {
|
|
2964
3062
|
// For operations, call rollbackDataStoreOp which will find the right store
|
|
@@ -2966,8 +3064,24 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2966
3064
|
this.channelCollection.rollback(type, contents, localOpMetadata);
|
|
2967
3065
|
break;
|
|
2968
3066
|
}
|
|
3067
|
+
case ContainerMessageType.GC: {
|
|
3068
|
+
// Just drop it, but log an error, this is not expected and not ideal, but not critical failure either.
|
|
3069
|
+
// Currently the only expected type here is TombstoneLoaded, which will have been preceded by one of these events as well:
|
|
3070
|
+
// GC_Tombstone_DataStore_Requested, GC_Tombstone_SubDataStore_Requested, GC_Tombstone_Blob_Requested
|
|
3071
|
+
this.mc.logger.sendErrorEvent({
|
|
3072
|
+
eventName: "GC_OpDiscarded",
|
|
3073
|
+
details: { subType: contents.type },
|
|
3074
|
+
});
|
|
3075
|
+
break;
|
|
3076
|
+
}
|
|
3077
|
+
case ContainerMessageType.DocumentSchemaChange: {
|
|
3078
|
+
// Notify the document schema controller that the pending op was not acked.
|
|
3079
|
+
// This will allow it to propose the schema change again if needed.
|
|
3080
|
+
this.documentsSchemaController.pendingOpNotAcked();
|
|
3081
|
+
break;
|
|
3082
|
+
}
|
|
2969
3083
|
default: {
|
|
2970
|
-
|
|
3084
|
+
unreachableCase(type);
|
|
2971
3085
|
}
|
|
2972
3086
|
}
|
|
2973
3087
|
}
|
|
@@ -3145,6 +3259,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
3145
3259
|
},
|
|
3146
3260
|
getQuorum: this.getQuorum.bind(this),
|
|
3147
3261
|
getAudience: this.getAudience.bind(this),
|
|
3262
|
+
supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
|
|
3148
3263
|
};
|
|
3149
3264
|
entry = new factory(runtime, ...useContext);
|
|
3150
3265
|
this.extensions.set(id, entry);
|