@fluidframework/container-runtime 2.1.0-276985 → 2.1.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 (199) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +71 -18
  3. package/api-extractor/api-extractor.current.json +5 -0
  4. package/api-extractor/api-extractor.legacy.json +1 -1
  5. package/api-extractor.json +1 -1
  6. package/api-report/container-runtime.legacy.public.api.md +9 -0
  7. package/container-runtime.test-files.tar +0 -0
  8. package/dist/blobManager/blobManager.d.ts +10 -0
  9. package/dist/blobManager/blobManager.d.ts.map +1 -1
  10. package/dist/blobManager/blobManager.js +19 -0
  11. package/dist/blobManager/blobManager.js.map +1 -1
  12. package/dist/channelCollection.d.ts +1 -1
  13. package/dist/channelCollection.d.ts.map +1 -1
  14. package/dist/channelCollection.js +40 -8
  15. package/dist/channelCollection.js.map +1 -1
  16. package/dist/containerRuntime.d.ts +14 -5
  17. package/dist/containerRuntime.d.ts.map +1 -1
  18. package/dist/containerRuntime.js +151 -99
  19. package/dist/containerRuntime.js.map +1 -1
  20. package/dist/dataStoreContext.d.ts +4 -0
  21. package/dist/dataStoreContext.d.ts.map +1 -1
  22. package/dist/dataStoreContext.js +9 -3
  23. package/dist/dataStoreContext.js.map +1 -1
  24. package/dist/gc/garbageCollection.d.ts +1 -1
  25. package/dist/gc/garbageCollection.d.ts.map +1 -1
  26. package/dist/gc/garbageCollection.js +14 -8
  27. package/dist/gc/garbageCollection.js.map +1 -1
  28. package/dist/gc/gcDefinitions.d.ts +4 -2
  29. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  30. package/dist/gc/gcDefinitions.js.map +1 -1
  31. package/dist/gc/gcHelpers.d.ts.map +1 -1
  32. package/dist/gc/gcHelpers.js +12 -0
  33. package/dist/gc/gcHelpers.js.map +1 -1
  34. package/dist/gc/gcTelemetry.d.ts +3 -2
  35. package/dist/gc/gcTelemetry.d.ts.map +1 -1
  36. package/dist/gc/gcTelemetry.js +6 -6
  37. package/dist/gc/gcTelemetry.js.map +1 -1
  38. package/dist/legacy.d.ts +1 -1
  39. package/dist/metadata.d.ts +7 -1
  40. package/dist/metadata.d.ts.map +1 -1
  41. package/dist/metadata.js +6 -0
  42. package/dist/metadata.js.map +1 -1
  43. package/dist/opLifecycle/batchManager.d.ts +8 -1
  44. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  45. package/dist/opLifecycle/batchManager.js +37 -16
  46. package/dist/opLifecycle/batchManager.js.map +1 -1
  47. package/dist/opLifecycle/definitions.d.ts +1 -1
  48. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  49. package/dist/opLifecycle/definitions.js.map +1 -1
  50. package/dist/opLifecycle/index.d.ts +2 -2
  51. package/dist/opLifecycle/index.d.ts.map +1 -1
  52. package/dist/opLifecycle/index.js +3 -1
  53. package/dist/opLifecycle/index.js.map +1 -1
  54. package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
  55. package/dist/opLifecycle/opCompressor.js +12 -8
  56. package/dist/opLifecycle/opCompressor.js.map +1 -1
  57. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  58. package/dist/opLifecycle/opGroupingManager.js +14 -11
  59. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  60. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  61. package/dist/opLifecycle/opSplitter.js +11 -6
  62. package/dist/opLifecycle/opSplitter.js.map +1 -1
  63. package/dist/opLifecycle/outbox.d.ts +22 -6
  64. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  65. package/dist/opLifecycle/outbox.js +43 -21
  66. package/dist/opLifecycle/outbox.js.map +1 -1
  67. package/dist/opLifecycle/remoteMessageProcessor.d.ts +10 -8
  68. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  69. package/dist/opLifecycle/remoteMessageProcessor.js +39 -15
  70. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  71. package/dist/packageVersion.d.ts +1 -1
  72. package/dist/packageVersion.d.ts.map +1 -1
  73. package/dist/packageVersion.js +1 -1
  74. package/dist/packageVersion.js.map +1 -1
  75. package/dist/pendingStateManager.d.ts +37 -13
  76. package/dist/pendingStateManager.d.ts.map +1 -1
  77. package/dist/pendingStateManager.js +95 -45
  78. package/dist/pendingStateManager.js.map +1 -1
  79. package/dist/public.d.ts +1 -1
  80. package/dist/scheduleManager.js +4 -0
  81. package/dist/scheduleManager.js.map +1 -1
  82. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  83. package/dist/summary/summarizerNode/summarizerNodeUtils.js +2 -0
  84. package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  85. package/dist/summary/summaryFormat.d.ts.map +1 -1
  86. package/dist/summary/summaryFormat.js +4 -1
  87. package/dist/summary/summaryFormat.js.map +1 -1
  88. package/internal.d.ts +1 -1
  89. package/legacy.d.ts +1 -1
  90. package/lib/blobManager/blobManager.d.ts +10 -0
  91. package/lib/blobManager/blobManager.d.ts.map +1 -1
  92. package/lib/blobManager/blobManager.js +19 -0
  93. package/lib/blobManager/blobManager.js.map +1 -1
  94. package/lib/channelCollection.d.ts +1 -1
  95. package/lib/channelCollection.d.ts.map +1 -1
  96. package/lib/channelCollection.js +40 -8
  97. package/lib/channelCollection.js.map +1 -1
  98. package/lib/containerRuntime.d.ts +14 -5
  99. package/lib/containerRuntime.d.ts.map +1 -1
  100. package/lib/containerRuntime.js +152 -100
  101. package/lib/containerRuntime.js.map +1 -1
  102. package/lib/dataStoreContext.d.ts +4 -0
  103. package/lib/dataStoreContext.d.ts.map +1 -1
  104. package/lib/dataStoreContext.js +10 -4
  105. package/lib/dataStoreContext.js.map +1 -1
  106. package/lib/gc/garbageCollection.d.ts +1 -1
  107. package/lib/gc/garbageCollection.d.ts.map +1 -1
  108. package/lib/gc/garbageCollection.js +14 -8
  109. package/lib/gc/garbageCollection.js.map +1 -1
  110. package/lib/gc/gcDefinitions.d.ts +4 -2
  111. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  112. package/lib/gc/gcDefinitions.js.map +1 -1
  113. package/lib/gc/gcHelpers.d.ts.map +1 -1
  114. package/lib/gc/gcHelpers.js +12 -0
  115. package/lib/gc/gcHelpers.js.map +1 -1
  116. package/lib/gc/gcTelemetry.d.ts +3 -2
  117. package/lib/gc/gcTelemetry.d.ts.map +1 -1
  118. package/lib/gc/gcTelemetry.js +6 -6
  119. package/lib/gc/gcTelemetry.js.map +1 -1
  120. package/lib/legacy.d.ts +1 -1
  121. package/lib/metadata.d.ts +7 -1
  122. package/lib/metadata.d.ts.map +1 -1
  123. package/lib/metadata.js +4 -1
  124. package/lib/metadata.js.map +1 -1
  125. package/lib/opLifecycle/batchManager.d.ts +8 -1
  126. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  127. package/lib/opLifecycle/batchManager.js +35 -15
  128. package/lib/opLifecycle/batchManager.js.map +1 -1
  129. package/lib/opLifecycle/definitions.d.ts +1 -1
  130. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  131. package/lib/opLifecycle/definitions.js.map +1 -1
  132. package/lib/opLifecycle/index.d.ts +2 -2
  133. package/lib/opLifecycle/index.d.ts.map +1 -1
  134. package/lib/opLifecycle/index.js +2 -2
  135. package/lib/opLifecycle/index.js.map +1 -1
  136. package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
  137. package/lib/opLifecycle/opCompressor.js +12 -8
  138. package/lib/opLifecycle/opCompressor.js.map +1 -1
  139. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  140. package/lib/opLifecycle/opGroupingManager.js +14 -11
  141. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  142. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  143. package/lib/opLifecycle/opSplitter.js +11 -6
  144. package/lib/opLifecycle/opSplitter.js.map +1 -1
  145. package/lib/opLifecycle/outbox.d.ts +22 -6
  146. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  147. package/lib/opLifecycle/outbox.js +44 -22
  148. package/lib/opLifecycle/outbox.js.map +1 -1
  149. package/lib/opLifecycle/remoteMessageProcessor.d.ts +10 -8
  150. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  151. package/lib/opLifecycle/remoteMessageProcessor.js +37 -14
  152. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  153. package/lib/packageVersion.d.ts +1 -1
  154. package/lib/packageVersion.d.ts.map +1 -1
  155. package/lib/packageVersion.js +1 -1
  156. package/lib/packageVersion.js.map +1 -1
  157. package/lib/pendingStateManager.d.ts +37 -13
  158. package/lib/pendingStateManager.d.ts.map +1 -1
  159. package/lib/pendingStateManager.js +95 -45
  160. package/lib/pendingStateManager.js.map +1 -1
  161. package/lib/public.d.ts +1 -1
  162. package/lib/scheduleManager.js +4 -0
  163. package/lib/scheduleManager.js.map +1 -1
  164. package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  165. package/lib/summary/summarizerNode/summarizerNodeUtils.js +2 -0
  166. package/lib/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  167. package/lib/summary/summaryFormat.d.ts.map +1 -1
  168. package/lib/summary/summaryFormat.js +4 -1
  169. package/lib/summary/summaryFormat.js.map +1 -1
  170. package/package.json +46 -31
  171. package/src/blobManager/blobManager.ts +19 -0
  172. package/src/channelCollection.ts +48 -11
  173. package/src/containerRuntime.ts +203 -133
  174. package/src/dataStoreContext.ts +22 -4
  175. package/src/gc/garbageCollection.ts +15 -10
  176. package/src/gc/gcDefinitions.ts +7 -2
  177. package/src/gc/gcHelpers.ts +18 -6
  178. package/src/gc/gcTelemetry.ts +20 -8
  179. package/src/metadata.ts +11 -1
  180. package/src/opLifecycle/README.md +0 -8
  181. package/src/opLifecycle/batchManager.ts +49 -16
  182. package/src/opLifecycle/definitions.ts +1 -1
  183. package/src/opLifecycle/index.ts +13 -2
  184. package/src/opLifecycle/opCompressor.ts +12 -8
  185. package/src/opLifecycle/opGroupingManager.ts +14 -11
  186. package/src/opLifecycle/opSplitter.ts +10 -6
  187. package/src/opLifecycle/outbox.ts +64 -26
  188. package/src/opLifecycle/remoteMessageProcessor.ts +56 -17
  189. package/src/packageVersion.ts +1 -1
  190. package/src/pendingStateManager.ts +173 -74
  191. package/src/scheduleManager.ts +6 -2
  192. package/src/summary/README.md +81 -0
  193. package/src/summary/summarizerNode/summarizerNodeUtils.ts +3 -1
  194. package/src/summary/summaryFormat.ts +3 -1
  195. package/src/summary/summaryFormats.md +69 -8
  196. package/tsconfig.json +0 -1
  197. package/src/summary/images/appTree.png +0 -0
  198. package/src/summary/images/protocolAndAppTree.png +0 -0
  199. package/src/summary/images/summaryTree.png +0 -0
@@ -61,6 +61,7 @@ import {
61
61
  MessageType,
62
62
  ISequencedDocumentMessage,
63
63
  ISignalMessage,
64
+ type ISummaryContext,
64
65
  } from "@fluidframework/driver-definitions/internal";
65
66
  import { readAndParse } from "@fluidframework/driver-utils/internal";
66
67
  import type { IIdCompressor } from "@fluidframework/id-compressor";
@@ -91,7 +92,6 @@ import {
91
92
  } from "@fluidframework/runtime-definitions/internal";
92
93
  import {
93
94
  GCDataBuilder,
94
- ReadAndParseBlob,
95
95
  RequestParser,
96
96
  TelemetryContext,
97
97
  addBlobToSummary,
@@ -169,7 +169,9 @@ import {
169
169
  } from "./messageTypes.js";
170
170
  import { IBatchMetadata, ISavedOpMetadata } from "./metadata.js";
171
171
  import {
172
+ BatchId,
172
173
  BatchMessage,
174
+ ensureContentsDeserialized,
173
175
  IBatch,
174
176
  IBatchCheckpoint,
175
177
  OpCompressor,
@@ -181,7 +183,7 @@ import {
181
183
  } from "./opLifecycle/index.js";
182
184
  import { pkgVersion } from "./packageVersion.js";
183
185
  import {
184
- IPendingBatchMessage,
186
+ PendingMessageResubmitData,
185
187
  IPendingLocalState,
186
188
  PendingStateManager,
187
189
  } from "./pendingStateManager.js";
@@ -668,7 +670,7 @@ export const makeLegacySendBatchFn =
668
670
  (batch: IBatch) => {
669
671
  // Default to negative one to match Container.submitBatch behavior
670
672
  let clientSequenceNumber: number = -1;
671
- for (const message of batch.content) {
673
+ for (const message of batch.messages) {
672
674
  clientSequenceNumber = submitFn(
673
675
  MessageType.Operation,
674
676
  // For back-compat (submitFn only works on deserialized content)
@@ -693,7 +695,7 @@ export const makeLegacySendBatchFn =
693
695
  type MessageWithContext = {
694
696
  local: boolean;
695
697
  savedOp?: boolean;
696
- batchStartCsn: number;
698
+ localOpMetadata?: unknown;
697
699
  } & (
698
700
  | {
699
701
  message: InboundSequencedContainerRuntimeMessage;
@@ -988,11 +990,7 @@ export class ContainerRuntime
988
990
  }
989
991
  };
990
992
 
991
- const disableCompression = mc.config.getBoolean(
992
- "Fluid.ContainerRuntime.CompressionDisabled",
993
- );
994
993
  const compressionLz4 =
995
- disableCompression !== true &&
996
994
  compressionOptions.minimumBatchSizeInBytes !== Infinity &&
997
995
  compressionOptions.compressionAlgorithm === "lz4";
998
996
 
@@ -1012,9 +1010,7 @@ export class ContainerRuntime
1012
1010
  },
1013
1011
  );
1014
1012
 
1015
- const featureGatesForTelemetry: Record<string, boolean | number | undefined> = {
1016
- disableCompression,
1017
- };
1013
+ const featureGatesForTelemetry: Record<string, boolean | number | undefined> = {};
1018
1014
 
1019
1015
  const runtime = new containerRuntimeCtor(
1020
1016
  context,
@@ -1360,6 +1356,13 @@ export class ContainerRuntime
1360
1356
  */
1361
1357
  private readonly loadedFromVersionId: string | undefined;
1362
1358
 
1359
+ private readonly isSnapshotInstanceOfISnapshot: boolean | undefined;
1360
+
1361
+ /**
1362
+ * The summary context of the last acked summary. The properties from this as used when uploading a summary.
1363
+ */
1364
+ private lastAckedSummaryContext: ISummaryContext | undefined;
1365
+
1363
1366
  /**
1364
1367
  * It a cache for holding mapping for loading groupIds with its snapshot from the service. Add expiry policy of 1 minute.
1365
1368
  * Starting with 1 min and based on recorded usage we can tweak it later on.
@@ -1512,9 +1515,6 @@ export class ContainerRuntime
1512
1515
  this.disableAttachReorder = this.mc.config.getBoolean(
1513
1516
  "Fluid.ContainerRuntime.disableAttachOpReorder",
1514
1517
  );
1515
- const disableChunking = this.mc.config.getBoolean(
1516
- "Fluid.ContainerRuntime.CompressionChunkingDisabled",
1517
- );
1518
1518
 
1519
1519
  const opGroupingManager = new OpGroupingManager(
1520
1520
  {
@@ -1531,7 +1531,7 @@ export class ContainerRuntime
1531
1531
  const opSplitter = new OpSplitter(
1532
1532
  chunks,
1533
1533
  this.submitBatchFn,
1534
- disableChunking === true ? Number.POSITIVE_INFINITY : runtimeOptions.chunkSizeInBytes,
1534
+ runtimeOptions.chunkSizeInBytes,
1535
1535
  runtimeOptions.maxBatchSizeInBytes,
1536
1536
  this.mc.logger,
1537
1537
  );
@@ -1549,10 +1549,6 @@ export class ContainerRuntime
1549
1549
  clientId: () => this.clientId,
1550
1550
  close: this.closeFn,
1551
1551
  connected: () => this.connected,
1552
- reSubmit: (message: IPendingBatchMessage) => {
1553
- this.reSubmit(message);
1554
- this.flush();
1555
- },
1556
1552
  reSubmitBatch: this.reSubmitBatch.bind(this),
1557
1553
  isActiveConnection: () => this.innerDeltaManager.active,
1558
1554
  isAttached: () => this.attachState !== AttachState.Detached,
@@ -1665,6 +1661,10 @@ export class ContainerRuntime
1665
1661
 
1666
1662
  const parentContext = wrapContext(this);
1667
1663
 
1664
+ if (snapshotWithContents !== undefined) {
1665
+ this.isSnapshotInstanceOfISnapshot = true;
1666
+ }
1667
+
1668
1668
  // Due to a mismatch between different layers in terms of
1669
1669
  // what is the interface of passing signals, we need the
1670
1670
  // downstream stores to wrap the signal.
@@ -1921,7 +1921,6 @@ export class ContainerRuntime
1921
1921
  sessionRuntimeSchema: JSON.stringify(this.sessionSchema),
1922
1922
  featureGates: JSON.stringify({
1923
1923
  ...featureGatesForTelemetry,
1924
- disableChunking,
1925
1924
  disableAttachReorder: this.disableAttachReorder,
1926
1925
  disablePartialFlush,
1927
1926
  closeSummarizerDelayOverride,
@@ -2167,9 +2166,13 @@ export class ContainerRuntime
2167
2166
  let childTree = snapshotTree;
2168
2167
  for (const part of pathParts) {
2169
2168
  if (hasIsolatedChannels) {
2170
- childTree = childTree?.trees[channelsTreeName];
2169
+ // TODO Why are we non null asserting here
2170
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2171
+ childTree = childTree.trees[channelsTreeName]!;
2171
2172
  }
2172
- childTree = childTree?.trees[part];
2173
+ // TODO Why are we non null asserting here
2174
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2175
+ childTree = childTree.trees[part]!;
2173
2176
  }
2174
2177
  return childTree;
2175
2178
  }
@@ -2221,7 +2224,9 @@ export class ContainerRuntime
2221
2224
  }
2222
2225
 
2223
2226
  if (id === blobManagerBasePath && requestParser.isLeaf(2)) {
2224
- const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
2227
+ // TODO why are we non null asserting here?
2228
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2229
+ const blob = await this.blobManager.getBlob(requestParser.pathParts[1]!);
2225
2230
  return blob
2226
2231
  ? {
2227
2232
  status: 200,
@@ -2612,40 +2617,55 @@ export class ContainerRuntime
2612
2617
  // or something different, like a system message.
2613
2618
  const modernRuntimeMessage = messageArg.type === MessageType.Operation;
2614
2619
 
2620
+ const savedOp = (messageArg.metadata as ISavedOpMetadata)?.savedOp;
2621
+
2622
+ // There is some ancient back-compat code that we'd like to instrument
2623
+ // to understand if/when it is hit.
2624
+ const logLegacyCase = (codePath: string) =>
2625
+ this.logger.sendTelemetryEvent({
2626
+ eventName: "LegacyMessageFormat",
2627
+ details: { codePath, type: messageArg.type },
2628
+ });
2629
+
2615
2630
  // Do shallow copy of message, as the processing flow will modify it.
2616
2631
  // There might be multiple container instances receiving the same message.
2617
2632
  // We do not need to make a deep copy. Each layer will just replace message.contents itself,
2618
2633
  // but will not modify the contents object (likely it will replace it on the message).
2619
2634
  const messageCopy = { ...messageArg };
2620
- const savedOp = (messageCopy.metadata as ISavedOpMetadata)?.savedOp;
2621
- const processResult = this.remoteMessageProcessor.process(messageCopy);
2622
- if (processResult === undefined) {
2623
- // This means the incoming message is an incomplete part of a message or batch
2624
- // and we need to process more messages before the rest of the system can understand it.
2625
- return;
2626
- }
2627
- for (const message of processResult.messages) {
2628
- const msg: MessageWithContext = modernRuntimeMessage
2629
- ? {
2630
- // Cast it since we expect it to be this based on modernRuntimeMessage computation above.
2631
- // There is nothing really ensuring that anytime original message.type is Operation that
2632
- // the result messages will be so. In the end modern bool being true only directs to
2633
- // throw error if ultimately unrecognized without compat details saying otherwise.
2634
- message: message as InboundSequencedContainerRuntimeMessage,
2635
- local,
2636
- modernRuntimeMessage,
2637
- batchStartCsn: processResult.batchStartCsn,
2638
- }
2639
- : // Unrecognized message will be ignored.
2640
- {
2641
- message,
2642
- local,
2643
- modernRuntimeMessage,
2644
- batchStartCsn: processResult.batchStartCsn,
2645
- };
2646
- msg.savedOp = savedOp;
2647
-
2648
- // ensure that we observe any re-entrancy, and if needed, rebase ops
2635
+ // We expect runtime messages to have JSON contents - deserialize it in place.
2636
+ ensureContentsDeserialized(messageCopy, modernRuntimeMessage, logLegacyCase);
2637
+ if (modernRuntimeMessage) {
2638
+ const processResult = this.remoteMessageProcessor.process(messageCopy, logLegacyCase);
2639
+ if (processResult === undefined) {
2640
+ // This means the incoming message is an incomplete part of a message or batch
2641
+ // and we need to process more messages before the rest of the system can understand it.
2642
+ return;
2643
+ }
2644
+ const batchStartCsn = processResult.batchStartCsn;
2645
+ const batch = processResult.messages;
2646
+ const messages: {
2647
+ message: InboundSequencedContainerRuntimeMessage;
2648
+ localOpMetadata: unknown;
2649
+ }[] = local
2650
+ ? this.pendingStateManager.processPendingLocalBatch(batch, batchStartCsn)
2651
+ : batch.map((message) => ({ message, localOpMetadata: undefined }));
2652
+ messages.forEach(({ message, localOpMetadata }) => {
2653
+ const msg: MessageWithContext = {
2654
+ message,
2655
+ local,
2656
+ modernRuntimeMessage,
2657
+ savedOp,
2658
+ localOpMetadata,
2659
+ };
2660
+ this.ensureNoDataModelChanges(() => this.processCore(msg));
2661
+ });
2662
+ } else {
2663
+ const msg: MessageWithContext = {
2664
+ message: messageCopy as InboundSequencedContainerRuntimeMessageOrSystemMessage,
2665
+ local,
2666
+ modernRuntimeMessage,
2667
+ savedOp,
2668
+ };
2649
2669
  this.ensureNoDataModelChanges(() => this.processCore(msg));
2650
2670
  }
2651
2671
  }
@@ -2656,7 +2676,7 @@ export class ContainerRuntime
2656
2676
  * Direct the message to the correct subsystem for processing, and implement other side effects
2657
2677
  */
2658
2678
  private processCore(messageWithContext: MessageWithContext) {
2659
- const { message, local } = messageWithContext;
2679
+ const { message, local, localOpMetadata } = messageWithContext;
2660
2680
 
2661
2681
  // Intercept to reduce minimum sequence number to the delta manager's minimum sequence number.
2662
2682
  // Sequence numbers are not guaranteed to follow any sort of order. Re-entrancy is one of those situations
@@ -2676,23 +2696,12 @@ export class ContainerRuntime
2676
2696
  this._processedClientSequenceNumber = message.clientSequenceNumber;
2677
2697
 
2678
2698
  try {
2679
- // See commit that added this assert for more details.
2680
- // These calls should be made for all but chunked ops:
2681
- // 1) this.pendingStateManager.processPendingLocalMessage() below
2682
- // 2) this.resetReconnectCount() below
2699
+ // RemoteMessageProcessor would have already reconstituted Chunked Ops into the original op type
2683
2700
  assert(
2684
2701
  message.type !== ContainerMessageType.ChunkedOp,
2685
2702
  0x93b /* we should never get here with chunked ops */,
2686
2703
  );
2687
2704
 
2688
- let localOpMetadata: unknown;
2689
- if (local && messageWithContext.modernRuntimeMessage) {
2690
- localOpMetadata = this.pendingStateManager.processPendingLocalMessage(
2691
- messageWithContext.message,
2692
- messageWithContext.batchStartCsn,
2693
- );
2694
- }
2695
-
2696
2705
  // If there are no more pending messages after processing a local message,
2697
2706
  // the document is no longer dirty.
2698
2707
  if (!this.hasPendingMessages()) {
@@ -2888,14 +2897,16 @@ export class ContainerRuntime
2888
2897
  /**
2889
2898
  * Flush the pending ops manually.
2890
2899
  * This method is expected to be called at the end of a batch.
2900
+ * @param resubmittingBatchId - If defined, indicates this is a resubmission of a batch
2901
+ * with the given Batch ID, which must be preserved
2891
2902
  */
2892
- private flush(): void {
2903
+ private flush(resubmittingBatchId?: BatchId): void {
2893
2904
  assert(
2894
2905
  this._orderSequentiallyCalls === 0,
2895
2906
  0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */,
2896
2907
  );
2897
2908
 
2898
- this.outbox.flush();
2909
+ this.outbox.flush(resubmittingBatchId);
2899
2910
  assert(this.outbox.isEmpty, 0x3cf /* reentrancy */);
2900
2911
  }
2901
2912
 
@@ -2909,7 +2920,7 @@ export class ContainerRuntime
2909
2920
  // Note: we are not touching any batches other than mainBatch here, for two reasons:
2910
2921
  // 1. It would not help, as other batches are flushed independently from main batch.
2911
2922
  // 2. There is no way to undo process of data store creation, blob creation, ID compressor ops, or other things tracked by other batches.
2912
- checkpoint = this.outbox.checkpoint().mainBatch;
2923
+ checkpoint = this.outbox.getBatchCheckpoints().mainBatch;
2913
2924
  }
2914
2925
  try {
2915
2926
  this._orderSequentiallyCalls++;
@@ -3052,7 +3063,7 @@ export class ContainerRuntime
3052
3063
  }
3053
3064
 
3054
3065
  /**
3055
- * Are we in the middle of batching ops together?
3066
+ * Typically ops are batched and later flushed together, but in some cases we want to flush immediately.
3056
3067
  */
3057
3068
  private currentlyBatching() {
3058
3069
  return this.flushMode !== FlushMode.Immediate || this._orderSequentiallyCalls !== 0;
@@ -3552,7 +3563,7 @@ export class ContainerRuntime
3552
3563
  summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
3553
3564
  const minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
3554
3565
  const message = `Summary @${summaryRefSeqNum}:${this.deltaManager.minimumSequenceNumber}`;
3555
- const lastAck = this.summaryCollection.latestAck;
3566
+ const lastAckedContext = this.lastAckedSummaryContext;
3556
3567
 
3557
3568
  const startSummaryResult = this.summarizerNode.startSummary(
3558
3569
  summaryRefSeqNum,
@@ -3619,10 +3630,10 @@ export class ContainerRuntime
3619
3630
  0x395 /* it's one and the same thing */,
3620
3631
  );
3621
3632
 
3622
- if (lastAck !== this.summaryCollection.latestAck) {
3633
+ if (lastAckedContext !== this.lastAckedSummaryContext) {
3623
3634
  return {
3624
3635
  continue: false,
3625
- error: `Last summary changed while summarizing. ${this.summaryCollection.latestAck} !== ${lastAck}`,
3636
+ error: `Last summary changed while summarizing. ${this.lastAckedSummaryContext} !== ${lastAckedContext}`,
3626
3637
  };
3627
3638
  }
3628
3639
  return { continue: true };
@@ -3694,7 +3705,9 @@ export class ContainerRuntime
3694
3705
  // Counting dataStores and handles
3695
3706
  // Because handles are unchanged dataStores in the current logic,
3696
3707
  // summarized dataStore count is total dataStore count minus handle count
3697
- const dataStoreTree = summaryTree.tree[channelsTreeName];
3708
+ // TODO why are we non null asserting here
3709
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3710
+ const dataStoreTree = summaryTree.tree[channelsTreeName]!;
3698
3711
 
3699
3712
  assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
3700
3713
  const handleCount = Object.values(dataStoreTree.tree).filter(
@@ -3730,18 +3743,11 @@ export class ContainerRuntime
3730
3743
  };
3731
3744
  }
3732
3745
 
3733
- const summaryContext =
3734
- lastAck === undefined
3735
- ? {
3736
- proposalHandle: undefined,
3737
- ackHandle: this.loadedFromVersionId,
3738
- referenceSequenceNumber: summaryRefSeqNum,
3739
- }
3740
- : {
3741
- proposalHandle: lastAck.summaryOp.contents.handle,
3742
- ackHandle: lastAck.summaryAck.contents.handle,
3743
- referenceSequenceNumber: summaryRefSeqNum,
3744
- };
3746
+ const summaryContext: ISummaryContext = {
3747
+ proposalHandle: this.lastAckedSummaryContext?.proposalHandle ?? undefined,
3748
+ ackHandle: this.lastAckedSummaryContext?.ackHandle ?? this.loadedFromVersionId,
3749
+ referenceSequenceNumber: summaryRefSeqNum,
3750
+ };
3745
3751
 
3746
3752
  let handle: string;
3747
3753
  try {
@@ -4032,7 +4038,9 @@ export class ContainerRuntime
4032
4038
  this.outbox.submit(message);
4033
4039
  }
4034
4040
 
4035
- if (!this.currentlyBatching()) {
4041
+ // Note: Technically, the system "always" batches - if this case is true we'll just have a single-message batch.
4042
+ const flushImmediatelyOnSubmit = !this.currentlyBatching();
4043
+ if (flushImmediatelyOnSubmit) {
4036
4044
  this.flush();
4037
4045
  } else {
4038
4046
  this.scheduleFlush();
@@ -4113,16 +4121,22 @@ export class ContainerRuntime
4113
4121
  }
4114
4122
  }
4115
4123
 
4116
- private reSubmitBatch(batch: IPendingBatchMessage[]) {
4124
+ private reSubmitBatch(batch: PendingMessageResubmitData[], batchId: BatchId) {
4117
4125
  this.orderSequentially(() => {
4118
4126
  for (const message of batch) {
4119
4127
  this.reSubmit(message);
4120
4128
  }
4121
4129
  });
4122
- this.flush();
4130
+
4131
+ // Only include Batch ID if "Offline Load" feature is enabled
4132
+ // It's only needed to identify batches across container forks arising from misuse of offline load.
4133
+ const includeBatchId =
4134
+ this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ?? false;
4135
+
4136
+ this.flush(includeBatchId ? batchId : undefined);
4123
4137
  }
4124
4138
 
4125
- private reSubmit(message: IPendingBatchMessage) {
4139
+ private reSubmit(message: PendingMessageResubmitData) {
4126
4140
  // Need to parse from string for back-compat
4127
4141
  const containerRuntimeMessage = this.parseLocalOpContent(message.content);
4128
4142
  this.reSubmitCore(containerRuntimeMessage, message.localOpMetadata, message.opMetadata);
@@ -4225,84 +4239,140 @@ export class ContainerRuntime
4225
4239
  const { proposalHandle, ackHandle, summaryRefSeq, summaryLogger } = options;
4226
4240
  // proposalHandle is always passed from RunningSummarizer.
4227
4241
  assert(proposalHandle !== undefined, 0x766 /* proposalHandle should be available */);
4228
- const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
4229
4242
  const result = await this.summarizerNode.refreshLatestSummary(
4230
4243
  proposalHandle,
4231
4244
  summaryRefSeq,
4232
4245
  );
4233
4246
 
4247
+ /* eslint-disable jsdoc/check-indentation */
4234
4248
  /**
4235
- * When refreshing a summary ack, this check indicates a new ack of a summary that is newer than the
4236
- * current summary that is tracked, but this summarizer runtime did not produce/track that summary. Thus
4237
- * it needs to refresh its state. Today refresh is done by fetching the latest snapshot to update the cache
4238
- * and then close as the current main client is likely to be re-elected as the parent summarizer again.
4249
+ * If the snapshot corresponding to the ack is not tracked by this client, it was submitted by another client.
4250
+ * Take action as per the following scenarios:
4251
+ * 1. If that snapshot is older than the one tracked by this client, ignore the ack because only the latest
4252
+ * snapshot is tracked.
4253
+ * 2. If that snapshot is newer, attempt to fetch the latest snapshot and do one of the following:
4254
+ * 2.1. If the fetched snapshot is same or newer than the one for which ack was received, close this client.
4255
+ * The next summarizer client will likely start from this snapshot and get out of this state. Fetching
4256
+ * the snapshot updates the cache for this client so if it's re-elected as summarizer, this will prevent
4257
+ * any thrashing.
4258
+ * 2.2. If the fetched snapshot is older than the one for which ack was received, ignore the ack. This can
4259
+ * happen in scenarios where the snapshot for the ack was lost in storage (in scenarios like DB rollback,
4260
+ * etc.) but the summary ack is still there because it's tracked a different service. In such cases,
4261
+ * ignoring the ack is the correct thing to do because the latest snapshot in storage is not the one for
4262
+ * the ack but is still the one tracked by this client. If we were to close the summarizer like in the
4263
+ * previous scenario, it will result in this document stuck in this state in a loop.
4239
4264
  */
4240
- if (!result.isSummaryTracked && result.isSummaryNewer) {
4241
- await this.fetchLatestSnapshotAndClose(
4242
- summaryLogger,
4243
- {
4244
- eventName: "RefreshLatestSummaryAckFetch",
4245
- ackHandle,
4246
- targetSequenceNumber: summaryRefSeq,
4247
- },
4248
- readAndParseBlob,
4249
- );
4265
+ /* eslint-enable jsdoc/check-indentation */
4266
+ if (!result.isSummaryTracked) {
4267
+ if (result.isSummaryNewer) {
4268
+ await this.fetchLatestSnapshotAndMaybeClose(summaryRefSeq, ackHandle, summaryLogger);
4269
+ }
4250
4270
  return;
4251
4271
  }
4252
4272
 
4253
4273
  // Notify the garbage collector so it can update its latest summary state.
4254
4274
  await this.garbageCollector.refreshLatestSummary(result);
4275
+
4276
+ // If we here, the ack was tracked by this client. Update the summary context of the last ack.
4277
+ this.lastAckedSummaryContext = {
4278
+ proposalHandle,
4279
+ ackHandle,
4280
+ referenceSequenceNumber: summaryRefSeq,
4281
+ };
4255
4282
  }
4256
4283
 
4257
4284
  /**
4258
- * Fetches the latest snapshot from storage and closes the container. This is done in cases where
4259
- * the last known snapshot is older than the latest one. This will ensure that the latest snapshot
4260
- * is downloaded and we don't end up loading snapshot from cache.
4285
+ * Fetches the latest snapshot from storage. If the fetched snapshot is same or newer than the one for which ack
4286
+ * was received, close this client. Fetching the snapshot will update the cache for this client so if it's
4287
+ * re-elected as summarizer, this will prevent any thrashing.
4288
+ * If the fetched snapshot is older than the one for which ack was received, ignore the ack and return. This can
4289
+ * happen in scenarios where the snapshot for the ack was lost in storage in scenarios like DB rollback, etc.
4261
4290
  */
4262
- private async fetchLatestSnapshotAndClose(
4291
+ private async fetchLatestSnapshotAndMaybeClose(
4292
+ targetRefSeq: number,
4293
+ targetAckHandle: string,
4263
4294
  logger: ITelemetryLoggerExt,
4264
- event: ITelemetryGenericEventExt,
4265
- readAndParseBlob: ReadAndParseBlob,
4266
4295
  ) {
4267
- await PerformanceEvent.timedExecAsync(
4296
+ const fetchedSnapshotRefSeq = await PerformanceEvent.timedExecAsync(
4268
4297
  logger,
4269
- event,
4298
+ { eventName: "RefreshLatestSummaryAckFetch" },
4270
4299
  async (perfEvent: {
4271
4300
  end: (arg0: {
4272
- getVersionDuration?: number | undefined;
4273
- getSnapshotDuration?: number | undefined;
4274
- snapshotRefSeq?: number | undefined;
4275
- snapshotVersion?: string | undefined;
4301
+ details: {
4302
+ getVersionDuration?: number | undefined;
4303
+ getSnapshotDuration?: number | undefined;
4304
+ snapshotRefSeq?: number | undefined;
4305
+ snapshotVersion?: string | undefined;
4306
+ newerSnapshotPresent?: boolean | undefined;
4307
+ targetRefSeq?: number | undefined;
4308
+ targetAckHandle?: string | undefined;
4309
+ };
4276
4310
  }) => void;
4277
4311
  }) => {
4278
- const stats: {
4312
+ const props: {
4279
4313
  getVersionDuration?: number;
4280
4314
  getSnapshotDuration?: number;
4281
4315
  snapshotRefSeq?: number;
4282
4316
  snapshotVersion?: string;
4283
- } = {};
4317
+ newerSnapshotPresent?: boolean | undefined;
4318
+ targetRefSeq?: number | undefined;
4319
+ targetAckHandle?: string | undefined;
4320
+ } = { targetRefSeq, targetAckHandle };
4284
4321
  const trace = Trace.start();
4285
4322
 
4286
- const versions = await this.storage.getVersions(
4287
- null,
4288
- 1,
4289
- "prefetchLatestSummaryBeforeClose",
4290
- FetchSource.noCache,
4291
- );
4292
- assert(!!versions && !!versions[0], 0x137 /* "Failed to get version from storage" */);
4293
- stats.getVersionDuration = trace.trace().duration;
4323
+ let snapshotTree: ISnapshotTree | null;
4324
+ const scenarioName = "RefreshLatestSummaryAckFetch";
4325
+ // If loader supplied us the ISnapshot when loading, the new getSnapshotApi is supported and feature gate is ON, then use the
4326
+ // new API, otherwise it will reduce the service performance because the service will need to recalculate the full snapshot
4327
+ // in case previously getSnapshotApi was used and now we use the getVersions API.
4328
+ if (
4329
+ this.isSnapshotInstanceOfISnapshot &&
4330
+ this.storage.getSnapshot !== undefined &&
4331
+ this.mc.config.getBoolean("Fluid.Container.UseLoadingGroupIdForSnapshotFetch2") ===
4332
+ true
4333
+ ) {
4334
+ const snapshot = await this.storage.getSnapshot({
4335
+ scenarioName,
4336
+ fetchSource: FetchSource.noCache,
4337
+ });
4338
+ const id = snapshot.snapshotTree.id;
4339
+ assert(id !== undefined, 0x9d0 /* id of the fetched snapshot should be defined */);
4340
+ props.snapshotVersion = id;
4341
+ snapshotTree = snapshot.snapshotTree;
4342
+ } else {
4343
+ const versions = await this.storage.getVersions(
4344
+ null,
4345
+ 1,
4346
+ scenarioName,
4347
+ FetchSource.noCache,
4348
+ );
4349
+ assert(
4350
+ !!versions && !!versions[0],
4351
+ 0x137 /* "Failed to get version from storage" */,
4352
+ );
4353
+ snapshotTree = await this.storage.getSnapshotTree(versions[0]);
4354
+ assert(!!snapshotTree, 0x138 /* "Failed to get snapshot from storage" */);
4355
+ props.snapshotVersion = versions[0].id;
4356
+ }
4294
4357
 
4295
- const maybeSnapshot = await this.storage.getSnapshotTree(versions[0]);
4296
- assert(!!maybeSnapshot, 0x138 /* "Failed to get snapshot from storage" */);
4297
- stats.getSnapshotDuration = trace.trace().duration;
4298
- const latestSnapshotRefSeq = await seqFromTree(maybeSnapshot, readAndParseBlob);
4299
- stats.snapshotRefSeq = latestSnapshotRefSeq;
4300
- stats.snapshotVersion = versions[0].id;
4358
+ props.getSnapshotDuration = trace.trace().duration;
4359
+ const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
4360
+ const snapshotRefSeq = await seqFromTree(snapshotTree, readAndParseBlob);
4361
+ props.snapshotRefSeq = snapshotRefSeq;
4362
+ props.newerSnapshotPresent = snapshotRefSeq >= targetRefSeq;
4301
4363
 
4302
- perfEvent.end(stats);
4364
+ perfEvent.end({ details: props });
4365
+ return snapshotRefSeq;
4303
4366
  },
4304
4367
  );
4305
4368
 
4369
+ // If the snapshot that was fetched is older than the target snapshot, return. The summarizer will not be closed
4370
+ // because the snapshot is likely deleted from storage and it so, closing the summarizer will result in the
4371
+ // document being stuck in this state.
4372
+ if (fetchedSnapshotRefSeq < targetRefSeq) {
4373
+ return;
4374
+ }
4375
+
4306
4376
  await delay(this.closeSummarizerDelayMs);
4307
4377
  this._summarizer?.stop("latestSummaryStateStale");
4308
4378
  this.disposeFn();
@@ -59,7 +59,6 @@ import {
59
59
  isSnapshotFetchRequiredForLoadingGroupId,
60
60
  } from "@fluidframework/runtime-utils/internal";
61
61
  import {
62
- DataCorruptionError,
63
62
  DataProcessingError,
64
63
  LoggingError,
65
64
  MonitoringContext,
@@ -305,6 +304,13 @@ export abstract class FluidDataStoreContext
305
304
  return this._isInMemoryRoot;
306
305
  }
307
306
 
307
+ /**
308
+ * Returns the count of pending messages that are stored until the data store is realized.
309
+ */
310
+ public get pendingCount(): number {
311
+ return this.pending?.length ?? 0;
312
+ }
313
+
308
314
  protected registry: IFluidDataStoreRegistry | undefined;
309
315
 
310
316
  protected detachedRuntimeCreation = false;
@@ -567,8 +573,10 @@ export abstract class FluidDataStoreContext
567
573
  // "verifyNotClosed" which logs tombstone errors. Throw error if tombstoned and throwing on load is configured.
568
574
  this.verifyNotClosed("process", false /* checkTombstone */, safeTelemetryProps);
569
575
  if (this.tombstoned && this.gcThrowOnTombstoneUsage) {
570
- throw new DataCorruptionError(
576
+ throw DataProcessingError.create(
571
577
  "Context is tombstoned! Call site [process]",
578
+ "process",
579
+ undefined /* sequencedMessage */,
572
580
  safeTelemetryProps,
573
581
  );
574
582
  }
@@ -961,7 +969,12 @@ export abstract class FluidDataStoreContext
961
969
  ) {
962
970
  if (this.deleted) {
963
971
  const messageString = `Context is deleted! Call site [${callSite}]`;
964
- const error = new DataCorruptionError(messageString, safeTelemetryProps);
972
+ const error = DataProcessingError.create(
973
+ messageString,
974
+ callSite,
975
+ undefined /* sequencedMessage */,
976
+ safeTelemetryProps,
977
+ );
965
978
  this.mc.logger.sendErrorEvent(
966
979
  {
967
980
  eventName: "GC_Deleted_DataStore_Changed",
@@ -979,7 +992,12 @@ export abstract class FluidDataStoreContext
979
992
 
980
993
  if (checkTombstone && this.tombstoned) {
981
994
  const messageString = `Context is tombstoned! Call site [${callSite}]`;
982
- const error = new DataCorruptionError(messageString, safeTelemetryProps);
995
+ const error = DataProcessingError.create(
996
+ messageString,
997
+ callSite,
998
+ undefined /* sequencedMessage */,
999
+ safeTelemetryProps,
1000
+ );
983
1001
 
984
1002
  sendGCUnexpectedUsageEvent(
985
1003
  this.mc,