@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.
Files changed (136) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/container-runtime.test-files.tar +0 -0
  3. package/dist/channelCollection.d.ts +1 -1
  4. package/dist/channelCollection.d.ts.map +1 -1
  5. package/dist/channelCollection.js +4 -4
  6. package/dist/channelCollection.js.map +1 -1
  7. package/dist/compatUtils.d.ts +24 -1
  8. package/dist/compatUtils.d.ts.map +1 -1
  9. package/dist/compatUtils.js +109 -7
  10. package/dist/compatUtils.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +36 -15
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +186 -71
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/dataStore.d.ts.map +1 -1
  16. package/dist/dataStore.js +5 -0
  17. package/dist/dataStore.js.map +1 -1
  18. package/dist/gc/garbageCollection.d.ts.map +1 -1
  19. package/dist/gc/garbageCollection.js +2 -0
  20. package/dist/gc/garbageCollection.js.map +1 -1
  21. package/dist/gc/gcDefinitions.d.ts +1 -1
  22. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  23. package/dist/gc/gcDefinitions.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/messageTypes.d.ts +5 -4
  28. package/dist/messageTypes.d.ts.map +1 -1
  29. package/dist/messageTypes.js.map +1 -1
  30. package/dist/metadata.d.ts +1 -1
  31. package/dist/metadata.d.ts.map +1 -1
  32. package/dist/metadata.js.map +1 -1
  33. package/dist/opLifecycle/definitions.d.ts +6 -5
  34. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  35. package/dist/opLifecycle/definitions.js.map +1 -1
  36. package/dist/opLifecycle/index.d.ts +1 -1
  37. package/dist/opLifecycle/index.d.ts.map +1 -1
  38. package/dist/opLifecycle/index.js.map +1 -1
  39. package/dist/opLifecycle/opGroupingManager.d.ts +9 -0
  40. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  41. package/dist/opLifecycle/opGroupingManager.js +6 -4
  42. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  43. package/dist/opLifecycle/opSerialization.d.ts +2 -1
  44. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  45. package/dist/opLifecycle/opSerialization.js.map +1 -1
  46. package/dist/packageVersion.d.ts +1 -1
  47. package/dist/packageVersion.d.ts.map +1 -1
  48. package/dist/packageVersion.js +1 -1
  49. package/dist/packageVersion.js.map +1 -1
  50. package/dist/pendingStateManager.d.ts +18 -5
  51. package/dist/pendingStateManager.d.ts.map +1 -1
  52. package/dist/pendingStateManager.js +20 -13
  53. package/dist/pendingStateManager.js.map +1 -1
  54. package/dist/summary/documentSchema.d.ts +79 -16
  55. package/dist/summary/documentSchema.d.ts.map +1 -1
  56. package/dist/summary/documentSchema.js +119 -53
  57. package/dist/summary/documentSchema.js.map +1 -1
  58. package/dist/summary/index.d.ts +1 -1
  59. package/dist/summary/index.d.ts.map +1 -1
  60. package/dist/summary/index.js.map +1 -1
  61. package/lib/channelCollection.d.ts +1 -1
  62. package/lib/channelCollection.d.ts.map +1 -1
  63. package/lib/channelCollection.js +4 -4
  64. package/lib/channelCollection.js.map +1 -1
  65. package/lib/compatUtils.d.ts +24 -1
  66. package/lib/compatUtils.d.ts.map +1 -1
  67. package/lib/compatUtils.js +102 -3
  68. package/lib/compatUtils.js.map +1 -1
  69. package/lib/containerRuntime.d.ts +36 -15
  70. package/lib/containerRuntime.d.ts.map +1 -1
  71. package/lib/containerRuntime.js +188 -73
  72. package/lib/containerRuntime.js.map +1 -1
  73. package/lib/dataStore.d.ts.map +1 -1
  74. package/lib/dataStore.js +5 -0
  75. package/lib/dataStore.js.map +1 -1
  76. package/lib/gc/garbageCollection.d.ts.map +1 -1
  77. package/lib/gc/garbageCollection.js +2 -0
  78. package/lib/gc/garbageCollection.js.map +1 -1
  79. package/lib/gc/gcDefinitions.d.ts +1 -1
  80. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  81. package/lib/gc/gcDefinitions.js.map +1 -1
  82. package/lib/index.d.ts +2 -2
  83. package/lib/index.d.ts.map +1 -1
  84. package/lib/index.js.map +1 -1
  85. package/lib/messageTypes.d.ts +5 -4
  86. package/lib/messageTypes.d.ts.map +1 -1
  87. package/lib/messageTypes.js.map +1 -1
  88. package/lib/metadata.d.ts +1 -1
  89. package/lib/metadata.d.ts.map +1 -1
  90. package/lib/metadata.js.map +1 -1
  91. package/lib/opLifecycle/definitions.d.ts +6 -5
  92. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  93. package/lib/opLifecycle/definitions.js.map +1 -1
  94. package/lib/opLifecycle/index.d.ts +1 -1
  95. package/lib/opLifecycle/index.d.ts.map +1 -1
  96. package/lib/opLifecycle/index.js.map +1 -1
  97. package/lib/opLifecycle/opGroupingManager.d.ts +9 -0
  98. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  99. package/lib/opLifecycle/opGroupingManager.js +6 -4
  100. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  101. package/lib/opLifecycle/opSerialization.d.ts +2 -1
  102. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  103. package/lib/opLifecycle/opSerialization.js.map +1 -1
  104. package/lib/packageVersion.d.ts +1 -1
  105. package/lib/packageVersion.d.ts.map +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/pendingStateManager.d.ts +18 -5
  109. package/lib/pendingStateManager.d.ts.map +1 -1
  110. package/lib/pendingStateManager.js +20 -13
  111. package/lib/pendingStateManager.js.map +1 -1
  112. package/lib/summary/documentSchema.d.ts +79 -16
  113. package/lib/summary/documentSchema.d.ts.map +1 -1
  114. package/lib/summary/documentSchema.js +119 -53
  115. package/lib/summary/documentSchema.js.map +1 -1
  116. package/lib/summary/index.d.ts +1 -1
  117. package/lib/summary/index.d.ts.map +1 -1
  118. package/lib/summary/index.js.map +1 -1
  119. package/package.json +18 -18
  120. package/src/channelCollection.ts +4 -4
  121. package/src/compatUtils.ts +147 -10
  122. package/src/containerRuntime.ts +242 -85
  123. package/src/dataStore.ts +7 -0
  124. package/src/gc/garbageCollection.ts +2 -0
  125. package/src/gc/gcDefinitions.ts +1 -1
  126. package/src/index.ts +4 -2
  127. package/src/messageTypes.ts +12 -5
  128. package/src/metadata.ts +1 -1
  129. package/src/opLifecycle/definitions.ts +7 -3
  130. package/src/opLifecycle/index.ts +1 -0
  131. package/src/opLifecycle/opGroupingManager.ts +17 -4
  132. package/src/opLifecycle/opSerialization.ts +6 -1
  133. package/src/packageVersion.ts +1 -1
  134. package/src/pendingStateManager.ts +49 -22
  135. package/src/summary/documentSchema.ts +228 -83
  136. package/src/summary/index.ts +3 -1
@@ -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, minVersionForCollab, requestHandler, undefined, // summaryConfiguration
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._connected;
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 Error("already in staging mode");
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 all BatchManagers are empty before entering staging mode,
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.outbox.flush();
631
+ this.flush();
601
632
  const exitStagingMode = (discardOrCommit) => () => {
602
- // Final flush of any last staged changes
603
- this.outbox.flush();
604
- this.stageControls = undefined;
605
- // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
606
- // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
607
- this.submitIdAllocationOpIfNeeded({ staged: false });
608
- discardOrCommit();
609
- this.channelCollection.notifyStagingMode(false);
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.rollback(runtimeOp, localOpMetadata);
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
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
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._connected = connected;
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.canSendOps(),
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._idCompressor = this.createIdCompressorFn();
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
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
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.canSendOps()) {
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._idCompressor = this.createIdCompressorFn();
1532
- // Finalize any ranges we received while the compressor was turned off.
1533
- const ops = this.pendingIdCompressorOps;
1534
- this.pendingIdCompressorOps = [];
1535
- for (const range of ops) {
1536
- this._idCompressor.finalizeCreationRange(range);
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(connected, clientId) {
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 (connected && this.sessionSchema.idCompressorMode === "delayed") {
1606
+ if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
1547
1607
  this.loadIdCompressor();
1548
1608
  }
1549
- if (connected === false && this.delayConnectClientId !== undefined) {
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 = connected && !this._connected;
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(connected, clientId);
1627
+ this.setConnectionStateCore(canSendOps, clientId);
1571
1628
  }
1572
- setConnectionStateCore(connected, clientId) {
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 changeOfState = this._connected !== connected;
1577
- const reconnection = changeOfState && !connected;
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 (changeOfState && connected) {
1643
+ if (canSendOpsChanged && canSendOps) {
1582
1644
  this.flush();
1583
1645
  }
1584
- this._connected = connected;
1585
- if (connected) {
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 (changeOfState) {
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 (changeOfState) {
1665
+ if (canSendOpsChanged) {
1604
1666
  this.replayPendingStates();
1605
1667
  }
1606
- this.channelCollection.setConnectionState(connected, clientId);
1607
- this.garbageCollector.setConnectionState(connected, clientId);
1608
- raiseConnectedEvent(this.mc.logger, this, connected, clientId);
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) => this.rollback(message.runtimeOp, message.localOpMetadata));
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
- canSendOps() {
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.maybeSendSchemaMessage();
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
- * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
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
- this.reSubmit(message, squash);
2975
+ resubmitFn(message);
2900
2976
  }
2901
2977
  }, resubmitInfo);
2902
2978
  this.flush(resubmitInfo);
2903
2979
  }
2904
- reSubmit(message, squash) {
2905
- this.reSubmitCore(message.runtimeOp, message.localOpMetadata, message.opMetadata, squash);
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
- * Finds the right store and asks it to resubmit the message. This typically happens when we
2909
- * reconnect and there are pending messages.
2910
- * ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
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
- reSubmitCore(message, localOpMetadata, opMetadata, squash) {
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, squash);
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
- // There is no need to resend this message. Document schema controller will properly resend it again (if needed)
2949
- // on a first occasion (any ops sent after reconnect). There is a good chance, though, that it will not want to
2950
- // send any ops, as some other client already changed schema.
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
- rollback(runtimeOp, localOpMetadata) {
2961
- const { type, contents } = runtimeOp;
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
- throw new Error(`Can't rollback ${type}`);
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);