@fluidframework/container-runtime 2.41.0 → 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.
Files changed (134) 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 +22 -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 +34 -13
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +158 -59
  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 +1 -1
  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.js +1 -1
  48. package/dist/packageVersion.js.map +1 -1
  49. package/dist/pendingStateManager.d.ts +18 -5
  50. package/dist/pendingStateManager.d.ts.map +1 -1
  51. package/dist/pendingStateManager.js +20 -13
  52. package/dist/pendingStateManager.js.map +1 -1
  53. package/dist/summary/documentSchema.d.ts +42 -18
  54. package/dist/summary/documentSchema.d.ts.map +1 -1
  55. package/dist/summary/documentSchema.js +62 -52
  56. package/dist/summary/documentSchema.js.map +1 -1
  57. package/dist/summary/index.d.ts +1 -1
  58. package/dist/summary/index.d.ts.map +1 -1
  59. package/dist/summary/index.js.map +1 -1
  60. package/lib/channelCollection.d.ts +1 -1
  61. package/lib/channelCollection.d.ts.map +1 -1
  62. package/lib/channelCollection.js +4 -4
  63. package/lib/channelCollection.js.map +1 -1
  64. package/lib/compatUtils.d.ts +22 -1
  65. package/lib/compatUtils.d.ts.map +1 -1
  66. package/lib/compatUtils.js +102 -3
  67. package/lib/compatUtils.js.map +1 -1
  68. package/lib/containerRuntime.d.ts +34 -13
  69. package/lib/containerRuntime.d.ts.map +1 -1
  70. package/lib/containerRuntime.js +160 -61
  71. package/lib/containerRuntime.js.map +1 -1
  72. package/lib/dataStore.d.ts.map +1 -1
  73. package/lib/dataStore.js +5 -0
  74. package/lib/dataStore.js.map +1 -1
  75. package/lib/gc/garbageCollection.d.ts.map +1 -1
  76. package/lib/gc/garbageCollection.js +2 -0
  77. package/lib/gc/garbageCollection.js.map +1 -1
  78. package/lib/gc/gcDefinitions.d.ts +1 -1
  79. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  80. package/lib/gc/gcDefinitions.js.map +1 -1
  81. package/lib/index.d.ts +1 -1
  82. package/lib/index.d.ts.map +1 -1
  83. package/lib/index.js.map +1 -1
  84. package/lib/messageTypes.d.ts +5 -4
  85. package/lib/messageTypes.d.ts.map +1 -1
  86. package/lib/messageTypes.js.map +1 -1
  87. package/lib/metadata.d.ts +1 -1
  88. package/lib/metadata.d.ts.map +1 -1
  89. package/lib/metadata.js.map +1 -1
  90. package/lib/opLifecycle/definitions.d.ts +6 -5
  91. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  92. package/lib/opLifecycle/definitions.js.map +1 -1
  93. package/lib/opLifecycle/index.d.ts +1 -1
  94. package/lib/opLifecycle/index.d.ts.map +1 -1
  95. package/lib/opLifecycle/index.js.map +1 -1
  96. package/lib/opLifecycle/opGroupingManager.d.ts +9 -0
  97. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  98. package/lib/opLifecycle/opGroupingManager.js +6 -4
  99. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  100. package/lib/opLifecycle/opSerialization.d.ts +2 -1
  101. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  102. package/lib/opLifecycle/opSerialization.js.map +1 -1
  103. package/lib/packageVersion.d.ts +1 -1
  104. package/lib/packageVersion.js +1 -1
  105. package/lib/packageVersion.js.map +1 -1
  106. package/lib/pendingStateManager.d.ts +18 -5
  107. package/lib/pendingStateManager.d.ts.map +1 -1
  108. package/lib/pendingStateManager.js +20 -13
  109. package/lib/pendingStateManager.js.map +1 -1
  110. package/lib/summary/documentSchema.d.ts +42 -18
  111. package/lib/summary/documentSchema.d.ts.map +1 -1
  112. package/lib/summary/documentSchema.js +62 -52
  113. package/lib/summary/documentSchema.js.map +1 -1
  114. package/lib/summary/index.d.ts +1 -1
  115. package/lib/summary/index.d.ts.map +1 -1
  116. package/lib/summary/index.js.map +1 -1
  117. package/package.json +18 -18
  118. package/src/channelCollection.ts +4 -4
  119. package/src/compatUtils.ts +145 -10
  120. package/src/containerRuntime.ts +209 -73
  121. package/src/dataStore.ts +7 -0
  122. package/src/gc/garbageCollection.ts +2 -0
  123. package/src/gc/gcDefinitions.ts +1 -1
  124. package/src/index.ts +2 -1
  125. package/src/messageTypes.ts +12 -5
  126. package/src/metadata.ts +1 -1
  127. package/src/opLifecycle/definitions.ts +7 -3
  128. package/src/opLifecycle/index.ts +1 -0
  129. package/src/opLifecycle/opGroupingManager.ts +17 -4
  130. package/src/opLifecycle/opSerialization.ts +6 -1
  131. package/src/packageVersion.ts +1 -1
  132. package/src/pendingStateManager.ts +49 -22
  133. package/src/summary/documentSchema.ts +111 -86
  134. package/src/summary/index.ts +2 -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";
@@ -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 = {
@@ -504,8 +521,12 @@ export class ContainerRuntime extends TypedEventEmitter {
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._connected;
529
+ return this.canSendOps;
509
530
  }
510
531
  /**
511
532
  * clientId of parent (non-summarizing) container that owns summarizer container
@@ -593,7 +614,10 @@ export class ContainerRuntime extends TypedEventEmitter {
593
614
  // eslint-disable-next-line import/no-deprecated
594
615
  this.enterStagingMode = () => {
595
616
  if (this.stageControls !== undefined) {
596
- throw new Error("already in staging mode");
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");
597
621
  }
598
622
  // Make sure all BatchManagers are empty before entering staging mode,
599
623
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
@@ -614,7 +638,7 @@ export class ContainerRuntime extends TypedEventEmitter {
614
638
  // Pop all staged batches from the PSM and roll them back in LIFO order
615
639
  this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
616
640
  assert(runtimeOp !== undefined, 0xb82 /* Staged batches expected to have runtimeOp defined */);
617
- this.rollback(runtimeOp, localOpMetadata);
641
+ this.rollbackStagedChanges(runtimeOp, localOpMetadata);
618
642
  });
619
643
  this.updateDocumentDirtyState();
620
644
  }),
@@ -651,6 +675,11 @@ export class ContainerRuntime extends TypedEventEmitter {
651
675
  this.mc = createChildMonitoringContext({
652
676
  logger: this.baseLogger,
653
677
  namespace: "ContainerRuntime",
678
+ properties: {
679
+ all: {
680
+ inStagingMode: this.inStagingMode,
681
+ },
682
+ },
654
683
  });
655
684
  // If we support multiple algorithms in the future, then we would need to manage it here carefully.
656
685
  // We can use runtimeOptions.compressionOptions.compressionAlgorithm, but only if it's in the schema list!
@@ -691,7 +720,7 @@ export class ContainerRuntime extends TypedEventEmitter {
691
720
  // Values are generally expected to be set from the runtime side.
692
721
  this.options = options ?? {};
693
722
  this.clientDetails = clientDetails;
694
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
723
+ this.isSummarizerClient = this.clientDetails.type === summarizerClientType;
695
724
  this.loadedFromVersionId = context.getLoadedFromVersion()?.id;
696
725
  // eslint-disable-next-line unicorn/consistent-destructuring
697
726
  this._getClientId = () => context.clientId;
@@ -728,7 +757,7 @@ export class ContainerRuntime extends TypedEventEmitter {
728
757
  details: { attachState: this.attachState },
729
758
  }));
730
759
  // In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
731
- this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
760
+ this.closeFn = this.isSummarizerClient ? this.disposeFn : closeFn;
732
761
  let loadSummaryNumber;
733
762
  // Get the container creation metadata. For new container, we initialize these. For existing containers,
734
763
  // get the values from the metadata blob.
@@ -752,7 +781,7 @@ export class ContainerRuntime extends TypedEventEmitter {
752
781
  this.messageAtLastSummary = lastMessageFromMetadata(metadata);
753
782
  // Note that we only need to pull the *initial* connected state from the context.
754
783
  // Later updates come through calls to setConnectionState.
755
- this._connected = connected;
784
+ this.canSendOps = connected;
756
785
  this.mc.logger.sendTelemetryEvent({
757
786
  eventName: "GCFeatureMatrix",
758
787
  metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
@@ -845,7 +874,7 @@ export class ContainerRuntime extends TypedEventEmitter {
845
874
  existing,
846
875
  metadata,
847
876
  createContainerMetadata: this.createContainerMetadata,
848
- isSummarizerClient,
877
+ isSummarizerClient: this.isSummarizerClient,
849
878
  getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
850
879
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
851
880
  readAndParseBlob: async (id) => readAndParse(this.storage, id),
@@ -929,7 +958,7 @@ export class ContainerRuntime extends TypedEventEmitter {
929
958
  // Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
930
959
  this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
931
960
  this.outbox = new Outbox({
932
- shouldSend: () => this.canSendOps(),
961
+ shouldSend: () => this.shouldSendOps(),
933
962
  pendingStateManager: this.pendingStateManager,
934
963
  submitBatchFn,
935
964
  legacySendBatchFn,
@@ -1068,7 +1097,14 @@ export class ContainerRuntime extends TypedEventEmitter {
1068
1097
  await this.initializeSummarizer(loader);
1069
1098
  if (this.sessionSchema.idCompressorMode === "on" ||
1070
1099
  (this.sessionSchema.idCompressorMode === "delayed" && this.connected)) {
1071
- this._idCompressor = this.createIdCompressorFn();
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
+ });
1072
1108
  // This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
1073
1109
  assert(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
1074
1110
  }
@@ -1097,8 +1133,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1097
1133
  const orderedClientCollection = new OrderedClientCollection(orderedClientLogger, this.innerDeltaManager, this._quorum);
1098
1134
  const orderedClientElectionForSummarizer = new OrderedClientElection(orderedClientLogger, orderedClientCollection, this.electedSummarizerData ?? this.innerDeltaManager.lastSequenceNumber, SummarizerClientElection.isClientEligible, this.mc.config.getBoolean("Fluid.ContainerRuntime.OrderedClientElection.EnablePerformanceEvents"));
1099
1135
  this.summarizerClientElection = new SummarizerClientElection(orderedClientLogger, summaryCollection, orderedClientElectionForSummarizer, maxOpsSinceLastSummary);
1100
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
1101
- if (isSummarizerClient) {
1136
+ if (this.isSummarizerClient) {
1102
1137
  // We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
1103
1138
  // so that all non summarizer clients don't have to load the code inside this module.
1104
1139
  const module = await import(
@@ -1446,7 +1481,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1446
1481
  }
1447
1482
  replayPendingStates() {
1448
1483
  // We need to be able to send ops to replay states
1449
- if (!this.canSendOps()) {
1484
+ if (!this.shouldSendOps()) {
1450
1485
  return;
1451
1486
  }
1452
1487
  // Replaying is an internal operation and we don't want to generate noise while doing it.
@@ -1528,25 +1563,35 @@ export class ContainerRuntime extends TypedEventEmitter {
1528
1563
  loadIdCompressor() {
1529
1564
  if (this._idCompressor === undefined &&
1530
1565
  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
- }
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
+ });
1538
1583
  assert(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
1539
1584
  }
1540
1585
  }
1541
- setConnectionState(connected, clientId) {
1586
+ setConnectionState(canSendOps, clientId) {
1542
1587
  // Validate we have consistent state
1543
1588
  const currentClientId = this._audience.getSelf()?.clientId;
1544
1589
  assert(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
1545
1590
  assert(this.clientId === currentClientId, 0x978 /* this.clientId does not match Audience */);
1546
- if (connected && this.sessionSchema.idCompressorMode === "delayed") {
1591
+ if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
1547
1592
  this.loadIdCompressor();
1548
1593
  }
1549
- if (connected === false && this.delayConnectClientId !== undefined) {
1594
+ if (canSendOps === false && this.delayConnectClientId !== undefined) {
1550
1595
  this.delayConnectClientId = undefined;
1551
1596
  this.mc.logger.sendTelemetryEvent({
1552
1597
  eventName: "UnsuccessfulConnectedTransition",
@@ -1554,37 +1599,39 @@ export class ContainerRuntime extends TypedEventEmitter {
1554
1599
  // Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
1555
1600
  return;
1556
1601
  }
1557
- if (!connected) {
1558
- this.documentsSchemaController.onDisconnect();
1559
- }
1560
1602
  // If there are stashed blobs in the pending state, we need to delay
1561
1603
  // propagation of the "connected" event until we have uploaded them to
1562
1604
  // ensure we don't submit ops referencing a blob that has not been uploaded
1563
- const connecting = connected && !this._connected;
1605
+ const connecting = canSendOps && !this.canSendOps;
1564
1606
  if (connecting && this.blobManager.hasPendingStashedUploads()) {
1565
1607
  assert(!this.delayConnectClientId, 0x791 /* Connect event delay must be canceled before subsequent connect event */);
1566
1608
  assert(!!clientId, 0x792 /* Must have clientId when connecting */);
1567
1609
  this.delayConnectClientId = clientId;
1568
1610
  return;
1569
1611
  }
1570
- this.setConnectionStateCore(connected, clientId);
1612
+ this.setConnectionStateCore(canSendOps, clientId);
1571
1613
  }
1572
- setConnectionStateCore(connected, clientId) {
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) {
1573
1620
  assert(!this.delayConnectClientId, 0x394 /* connect event delay must be cleared before propagating connect event */);
1574
1621
  this.verifyNotClosed();
1575
1622
  // 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;
1623
+ const canSendOpsChanged = this.canSendOps !== canSendOps;
1624
+ const reconnection = canSendOpsChanged && !canSendOps;
1578
1625
  // We need to flush the ops currently collected by Outbox to preserve original order.
1579
1626
  // This flush NEEDS to happen before we set the ContainerRuntime to "connected".
1580
1627
  // 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) {
1628
+ if (canSendOpsChanged && canSendOps) {
1582
1629
  this.flush();
1583
1630
  }
1584
- this._connected = connected;
1585
- if (connected) {
1631
+ this.canSendOps = canSendOps;
1632
+ if (canSendOps) {
1586
1633
  assert(this.attachState === AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
1587
- if (changeOfState) {
1634
+ if (canSendOpsChanged) {
1588
1635
  this.signalTelemetryManager.resetTracking();
1589
1636
  }
1590
1637
  }
@@ -1600,12 +1647,12 @@ export class ContainerRuntime extends TypedEventEmitter {
1600
1647
  return;
1601
1648
  }
1602
1649
  }
1603
- if (changeOfState) {
1650
+ if (canSendOpsChanged) {
1604
1651
  this.replayPendingStates();
1605
1652
  }
1606
- this.channelCollection.setConnectionState(connected, clientId);
1607
- this.garbageCollector.setConnectionState(connected, clientId);
1608
- raiseConnectedEvent(this.mc.logger, this, connected, clientId);
1653
+ this.channelCollection.setConnectionState(canSendOps, clientId);
1654
+ this.garbageCollector.setConnectionState(canSendOps, clientId);
1655
+ raiseConnectedEvent(this.mc.logger, this, this.connected /* canSendOps */, clientId);
1609
1656
  }
1610
1657
  async notifyOpReplay(message) {
1611
1658
  await this.pendingStateManager.applyStashedOpsAt(message.sequenceNumber);
@@ -2012,7 +2059,9 @@ export class ContainerRuntime extends TypedEventEmitter {
2012
2059
  if (checkpoint) {
2013
2060
  // This will throw and close the container if rollback fails
2014
2061
  try {
2015
- checkpoint.rollback((message) => this.rollback(message.runtimeOp, message.localOpMetadata));
2062
+ checkpoint.rollback((message) =>
2063
+ // These changes are staged since we entered staging mode above
2064
+ this.rollbackStagedChanges(message.runtimeOp, message.localOpMetadata));
2016
2065
  this.updateDocumentDirtyState();
2017
2066
  stageControls?.discardChanges();
2018
2067
  stageControls = undefined;
@@ -2089,7 +2138,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2089
2138
  const context = this.channelCollection.createDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], loadingGroupId);
2090
2139
  return channelToDataStore(await context.realize(), context.id, this.channelCollection, this.mc.logger);
2091
2140
  }
2092
- canSendOps() {
2141
+ shouldSendOps() {
2093
2142
  // Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
2094
2143
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
2095
2144
  return (this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure);
@@ -2778,6 +2827,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2778
2827
  try {
2779
2828
  // If we're resubmitting a batch, keep the same "staged" value as before. Otherwise, use the current "global" state.
2780
2829
  const staged = this.batchRunner.resubmitInfo?.staged ?? this.inStagingMode;
2830
+ assert(!staged || canStageMessageOfType(type), 0xbba /* Unexpected message type submitted in Staging Mode */);
2781
2831
  // Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
2782
2832
  if (!staged) {
2783
2833
  this.submitIdAllocationOpIfNeeded({ staged: false });
@@ -2785,7 +2835,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2785
2835
  // Allow document schema controller to send a message if it needs to propose change in document schema.
2786
2836
  // If it needs to send a message, it will call provided callback with payload of such message and rely
2787
2837
  // on this callback to do actual sending.
2788
- const schemaChangeMessage = this.documentsSchemaController.maybeSendSchemaMessage();
2838
+ const schemaChangeMessage = this.documentsSchemaController.maybeGenerateSchemaMessage();
2789
2839
  if (schemaChangeMessage) {
2790
2840
  this.mc.logger.sendTelemetryEvent({
2791
2841
  eventName: "SchemaChangeProposal",
@@ -2883,43 +2933,72 @@ export class ContainerRuntime extends TypedEventEmitter {
2883
2933
  }
2884
2934
  /**
2885
2935
  * Resubmits each message in the batch, and then flushes the outbox.
2936
+ * This typically happens when we reconnect and there are pending messages.
2886
2937
  *
2887
- * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
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)
2941
+ *
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,
2888
2944
  * for correlation to detect container forking.
2889
2945
  */
2890
2946
  reSubmitBatch(batch, { batchId, staged, squash }) {
2947
+ assert(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
2891
2948
  const resubmitInfo = {
2892
2949
  // Only include Batch ID if "Offline Load" feature is enabled
2893
2950
  // It's only needed to identify batches across container forks arising from misuse of offline load.
2894
2951
  batchId: this.offlineEnabled ? batchId : undefined,
2895
2952
  staged,
2896
2953
  };
2954
+ const resubmitFn = squash
2955
+ ? this.reSubmitWithSquashing.bind(this)
2956
+ : this.reSubmit.bind(this);
2897
2957
  this.batchRunner.run(() => {
2898
2958
  for (const message of batch) {
2899
- this.reSubmit(message, squash);
2959
+ resubmitFn(message);
2900
2960
  }
2901
2961
  }, resubmitInfo);
2902
2962
  this.flush(resubmitInfo);
2903
2963
  }
2904
- reSubmit(message, squash) {
2905
- this.reSubmitCore(message.runtimeOp, message.localOpMetadata, message.opMetadata, squash);
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
+ }
2906
2987
  }
2907
2988
  /**
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.
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
2913
2992
  */
2914
- reSubmitCore(message, localOpMetadata, opMetadata, squash) {
2915
- assert(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
2993
+ reSubmit({ runtimeOp: message, localOpMetadata, opMetadata, }) {
2916
2994
  switch (message.type) {
2917
2995
  case ContainerMessageType.FluidDataStoreOp:
2918
2996
  case ContainerMessageType.Attach:
2919
2997
  case ContainerMessageType.Alias: {
2920
2998
  // For Operations, call resubmitDataStoreOp which will find the right store
2921
2999
  // and trigger resubmission on it.
2922
- this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata, squash);
3000
+ this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata,
3001
+ /* squash: */ false);
2923
3002
  break;
2924
3003
  }
2925
3004
  case ContainerMessageType.IdAllocation: {
@@ -2945,9 +3024,9 @@ export class ContainerRuntime extends TypedEventEmitter {
2945
3024
  break;
2946
3025
  }
2947
3026
  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.
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();
2951
3030
  break;
2952
3031
  }
2953
3032
  default: {
@@ -2957,8 +3036,11 @@ export class ContainerRuntime extends TypedEventEmitter {
2957
3036
  }
2958
3037
  }
2959
3038
  }
2960
- rollback(runtimeOp, localOpMetadata) {
2961
- const { type, contents } = runtimeOp;
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 */);
2962
3044
  switch (type) {
2963
3045
  case ContainerMessageType.FluidDataStoreOp: {
2964
3046
  // For operations, call rollbackDataStoreOp which will find the right store
@@ -2966,8 +3048,24 @@ export class ContainerRuntime extends TypedEventEmitter {
2966
3048
  this.channelCollection.rollback(type, contents, localOpMetadata);
2967
3049
  break;
2968
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
+ }
2969
3067
  default: {
2970
- throw new Error(`Can't rollback ${type}`);
3068
+ unreachableCase(type);
2971
3069
  }
2972
3070
  }
2973
3071
  }
@@ -3145,6 +3243,7 @@ export class ContainerRuntime extends TypedEventEmitter {
3145
3243
  },
3146
3244
  getQuorum: this.getQuorum.bind(this),
3147
3245
  getAudience: this.getAudience.bind(this),
3246
+ supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
3148
3247
  };
3149
3248
  entry = new factory(runtime, ...useContext);
3150
3249
  this.extensions.set(id, entry);