@fluidframework/container-runtime 2.30.0 → 2.31.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 (120) hide show
  1. package/CHANGELOG.md +588 -584
  2. package/dist/channelCollection.js +1 -1
  3. package/dist/channelCollection.js.map +1 -1
  4. package/dist/containerRuntime.d.ts +19 -4
  5. package/dist/containerRuntime.d.ts.map +1 -1
  6. package/dist/containerRuntime.js +100 -74
  7. package/dist/containerRuntime.js.map +1 -1
  8. package/dist/dataStoreContext.d.ts +6 -1
  9. package/dist/dataStoreContext.d.ts.map +1 -1
  10. package/dist/dataStoreContext.js +12 -1
  11. package/dist/dataStoreContext.js.map +1 -1
  12. package/dist/gc/gcTelemetry.js +2 -2
  13. package/dist/gc/gcTelemetry.js.map +1 -1
  14. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  15. package/dist/opLifecycle/batchManager.js +16 -5
  16. package/dist/opLifecycle/batchManager.js.map +1 -1
  17. package/dist/opLifecycle/outbox.d.ts +12 -3
  18. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  19. package/dist/opLifecycle/outbox.js +41 -21
  20. package/dist/opLifecycle/outbox.js.map +1 -1
  21. package/dist/packageVersion.d.ts +1 -1
  22. package/dist/packageVersion.js +1 -1
  23. package/dist/packageVersion.js.map +1 -1
  24. package/dist/pendingStateManager.d.ts +1 -0
  25. package/dist/pendingStateManager.d.ts.map +1 -1
  26. package/dist/pendingStateManager.js +12 -2
  27. package/dist/pendingStateManager.js.map +1 -1
  28. package/dist/runCounter.d.ts +11 -0
  29. package/dist/runCounter.d.ts.map +1 -0
  30. package/dist/runCounter.js +43 -0
  31. package/dist/runCounter.js.map +1 -0
  32. package/dist/runtimeLayerCompatState.d.ts +51 -0
  33. package/dist/runtimeLayerCompatState.d.ts.map +1 -0
  34. package/dist/runtimeLayerCompatState.js +123 -0
  35. package/dist/runtimeLayerCompatState.js.map +1 -0
  36. package/dist/summary/summarizerNode/summarizerNode.d.ts +2 -2
  37. package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  38. package/dist/summary/summarizerNode/summarizerNode.js +4 -4
  39. package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
  40. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts +1 -18
  41. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  42. package/dist/summary/summarizerNode/summarizerNodeUtils.js +0 -27
  43. package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  44. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts +2 -2
  45. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  46. package/dist/summary/summarizerNode/summarizerNodeWithGc.js +1 -2
  47. package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  48. package/lib/channelCollection.js +1 -1
  49. package/lib/channelCollection.js.map +1 -1
  50. package/lib/containerRuntime.d.ts +19 -4
  51. package/lib/containerRuntime.d.ts.map +1 -1
  52. package/lib/containerRuntime.js +100 -74
  53. package/lib/containerRuntime.js.map +1 -1
  54. package/lib/dataStoreContext.d.ts +6 -1
  55. package/lib/dataStoreContext.d.ts.map +1 -1
  56. package/lib/dataStoreContext.js +12 -1
  57. package/lib/dataStoreContext.js.map +1 -1
  58. package/lib/gc/gcTelemetry.js +2 -2
  59. package/lib/gc/gcTelemetry.js.map +1 -1
  60. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  61. package/lib/opLifecycle/batchManager.js +16 -5
  62. package/lib/opLifecycle/batchManager.js.map +1 -1
  63. package/lib/opLifecycle/outbox.d.ts +12 -3
  64. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  65. package/lib/opLifecycle/outbox.js +43 -23
  66. package/lib/opLifecycle/outbox.js.map +1 -1
  67. package/lib/packageVersion.d.ts +1 -1
  68. package/lib/packageVersion.js +1 -1
  69. package/lib/packageVersion.js.map +1 -1
  70. package/lib/pendingStateManager.d.ts +1 -0
  71. package/lib/pendingStateManager.d.ts.map +1 -1
  72. package/lib/pendingStateManager.js +12 -2
  73. package/lib/pendingStateManager.js.map +1 -1
  74. package/lib/runCounter.d.ts +11 -0
  75. package/lib/runCounter.d.ts.map +1 -0
  76. package/lib/runCounter.js +39 -0
  77. package/lib/runCounter.js.map +1 -0
  78. package/lib/runtimeLayerCompatState.d.ts +51 -0
  79. package/lib/runtimeLayerCompatState.d.ts.map +1 -0
  80. package/lib/runtimeLayerCompatState.js +118 -0
  81. package/lib/runtimeLayerCompatState.js.map +1 -0
  82. package/lib/summary/summarizerNode/summarizerNode.d.ts +2 -2
  83. package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  84. package/lib/summary/summarizerNode/summarizerNode.js +5 -5
  85. package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
  86. package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts +1 -18
  87. package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  88. package/lib/summary/summarizerNode/summarizerNodeUtils.js +1 -25
  89. package/lib/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  90. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts +2 -2
  91. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  92. package/lib/summary/summarizerNode/summarizerNodeWithGc.js +1 -2
  93. package/lib/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  94. package/lib/tsdoc-metadata.json +1 -1
  95. package/package.json +20 -30
  96. package/src/channelCollection.ts +1 -1
  97. package/src/containerRuntime.ts +148 -100
  98. package/src/dataStoreContext.ts +22 -1
  99. package/src/gc/{garbageCollection.md → README.md} +17 -19
  100. package/src/gc/gcTelemetry.ts +2 -2
  101. package/src/opLifecycle/batchManager.ts +20 -6
  102. package/src/opLifecycle/outbox.ts +64 -24
  103. package/src/packageVersion.ts +1 -1
  104. package/src/pendingStateManager.ts +18 -2
  105. package/src/runCounter.ts +25 -0
  106. package/src/runtimeLayerCompatState.ts +143 -0
  107. package/src/summary/summarizerNode/summarizerNode.ts +6 -5
  108. package/src/summary/summarizerNode/summarizerNodeUtils.ts +1 -27
  109. package/src/summary/summarizerNode/summarizerNodeWithGc.ts +2 -3
  110. package/tsconfig.json +1 -0
  111. package/dist/layerCompatState.d.ts +0 -19
  112. package/dist/layerCompatState.d.ts.map +0 -1
  113. package/dist/layerCompatState.js +0 -64
  114. package/dist/layerCompatState.js.map +0 -1
  115. package/lib/layerCompatState.d.ts +0 -19
  116. package/lib/layerCompatState.d.ts.map +0 -1
  117. package/lib/layerCompatState.js +0 -60
  118. package/lib/layerCompatState.js.map +0 -1
  119. package/prettier.config.cjs +0 -8
  120. package/src/layerCompatState.ts +0 -75
@@ -625,7 +625,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
625
625
  * in the snapshot.
626
626
  * So, return short ids only if explicitly enabled via feature flags. Else, return uuid();
627
627
  */
628
- if (this.mc.config.getBoolean("Fluid.Runtime.UseShortIds") === true) {
628
+ if (this.mc.config.getBoolean("Fluid.Runtime.IsShortIdEnabled") === true) {
629
629
  // We use three non-overlapping namespaces:
630
630
  // - detached state: even numbers
631
631
  // - attached state: odd numbers
@@ -168,7 +168,6 @@ import {
168
168
  type GarbageCollectionMessage,
169
169
  } from "./gc/index.js";
170
170
  import { InboundBatchAggregator } from "./inboundBatchAggregator.js";
171
- import { RuntimeCompatDetails, validateLoaderCompatibility } from "./layerCompatState.js";
172
171
  import {
173
172
  ContainerMessageType,
174
173
  type ContainerRuntimeDocumentSchemaMessage,
@@ -202,6 +201,11 @@ import {
202
201
  IPendingLocalState,
203
202
  PendingStateManager,
204
203
  } from "./pendingStateManager.js";
204
+ import { RunCounter } from "./runCounter.js";
205
+ import {
206
+ runtimeCompatDetailsForLoader,
207
+ validateLoaderCompatibility,
208
+ } from "./runtimeLayerCompatState.js";
205
209
  import { SignalTelemetryManager } from "./signalTelemetryProcessing.js";
206
210
  import {
207
211
  DocumentsSchemaController,
@@ -1175,7 +1179,7 @@ export class ContainerRuntime
1175
1179
 
1176
1180
  private readonly maxConsecutiveReconnects: number;
1177
1181
 
1178
- private _orderSequentiallyCalls: number = 0;
1182
+ private readonly batchRunner = new RunCounter();
1179
1183
  private readonly _flushMode: FlushMode;
1180
1184
  private readonly offlineEnabled: boolean;
1181
1185
  private flushTaskExists = false;
@@ -1190,23 +1194,16 @@ export class ContainerRuntime
1190
1194
  */
1191
1195
  private delayConnectClientId?: string;
1192
1196
 
1193
- private ensureNoDataModelChangesCalls = 0;
1197
+ private readonly dataModelChangeRunner = new RunCounter();
1194
1198
 
1195
1199
  /**
1196
1200
  * Invokes the given callback and expects that no ops are submitted
1197
1201
  * until execution finishes. If an op is submitted, an error will be raised.
1198
1202
  *
1199
- * Can be disabled by feature gate `Fluid.ContainerRuntime.DisableOpReentryCheck`
1200
- *
1201
1203
  * @param callback - the callback to be invoked
1202
1204
  */
1203
1205
  public ensureNoDataModelChanges<T>(callback: () => T): T {
1204
- this.ensureNoDataModelChangesCalls++;
1205
- try {
1206
- return callback();
1207
- } finally {
1208
- this.ensureNoDataModelChangesCalls--;
1209
- }
1206
+ return this.dataModelChangeRunner.run(callback);
1210
1207
  }
1211
1208
 
1212
1209
  public get connected(): boolean {
@@ -1309,10 +1306,21 @@ export class ContainerRuntime
1309
1306
  expiry: { policy: "absolute", durationMs: 60000 },
1310
1307
  });
1311
1308
 
1309
+ /**
1310
+ * The compatibility details of the Runtime layer that is exposed to the Loader layer
1311
+ * for validating Loader-Runtime compatibility.
1312
+ */
1312
1313
  public get ILayerCompatDetails(): ILayerCompatDetails {
1313
- return RuntimeCompatDetails;
1314
+ return runtimeCompatDetailsForLoader;
1314
1315
  }
1315
1316
 
1317
+ /**
1318
+ * If true, will skip Outbox flushing before processing an incoming message,
1319
+ * and instead the Outbox will check for a split batch on every submit.
1320
+ * This is a kill-bit switch for this simplification of logic, in case it causes unexpected issues.
1321
+ */
1322
+ private readonly disableFlushBeforeProcess: boolean;
1323
+
1316
1324
  /***/
1317
1325
  protected constructor(
1318
1326
  context: IContainerContext,
@@ -1374,8 +1382,12 @@ export class ContainerRuntime
1374
1382
  // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
1375
1383
  this.disposeFn = disposeFn ?? closeFn;
1376
1384
 
1377
- const maybeLoaderCompatDetails = context as FluidObject<ILayerCompatDetails>;
1378
- validateLoaderCompatibility(maybeLoaderCompatDetails.ILayerCompatDetails, this.disposeFn);
1385
+ // Validate that the Loader is compatible with this Runtime.
1386
+ const maybeloaderCompatDetailsForRuntime = context as FluidObject<ILayerCompatDetails>;
1387
+ validateLoaderCompatibility(
1388
+ maybeloaderCompatDetailsForRuntime.ILayerCompatDetails,
1389
+ this.disposeFn,
1390
+ );
1379
1391
 
1380
1392
  this.mc = createChildMonitoringContext({
1381
1393
  logger: this.baseLogger,
@@ -1429,6 +1441,25 @@ export class ContainerRuntime
1429
1441
  this.on("dirty", () => context.updateDirtyContainerState(true));
1430
1442
  this.on("saved", () => context.updateDirtyContainerState(false));
1431
1443
 
1444
+ // Telemetry for when the container is attached and subsequently saved for the first time.
1445
+ // These events are useful for investigating the validity of container "saved" eventing upon attach.
1446
+ // See this.setAttachState() and this.updateDocumentDirtyState() for more details on "attached" and "saved" events.
1447
+ this.once("attached", () => {
1448
+ this.mc.logger.sendTelemetryEvent({
1449
+ eventName: "Attached",
1450
+ details: {
1451
+ dirtyContainer: this.dirtyContainer,
1452
+ hasPendingMessages: this.hasPendingMessages(),
1453
+ },
1454
+ });
1455
+ });
1456
+ this.once("saved", () =>
1457
+ this.mc.logger.sendTelemetryEvent({
1458
+ eventName: "Saved",
1459
+ details: { attachState: this.attachState },
1460
+ }),
1461
+ );
1462
+
1432
1463
  // In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
1433
1464
  this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
1434
1465
 
@@ -1551,7 +1582,7 @@ export class ContainerRuntime
1551
1582
  // If the context has ILayerCompatDetails, it supports referenceSequenceNumbers since that features
1552
1583
  // predates ILayerCompatDetails.
1553
1584
  const referenceSequenceNumbersSupported =
1554
- maybeLoaderCompatDetails.ILayerCompatDetails === undefined
1585
+ maybeloaderCompatDetailsForRuntime.ILayerCompatDetails === undefined
1555
1586
  ? supportedFeatures?.get("referenceSequenceNumbers") === true
1556
1587
  : true;
1557
1588
  if (
@@ -1723,12 +1754,11 @@ export class ContainerRuntime
1723
1754
  createChildLogger({ logger: this.baseLogger, namespace: "InboundBatchAggregator" }),
1724
1755
  );
1725
1756
 
1726
- const disablePartialFlush = this.mc.config.getBoolean(
1727
- "Fluid.ContainerRuntime.DisablePartialFlush",
1728
- );
1729
-
1730
1757
  const legacySendBatchFn = makeLegacySendBatchFn(submitFn, this.innerDeltaManager);
1731
1758
 
1759
+ this.disableFlushBeforeProcess =
1760
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
1761
+
1732
1762
  this.outbox = new Outbox({
1733
1763
  shouldSend: () => this.canSendOps(),
1734
1764
  pendingStateManager: this.pendingStateManager,
@@ -1739,16 +1769,18 @@ export class ContainerRuntime
1739
1769
  config: {
1740
1770
  compressionOptions,
1741
1771
  maxBatchSizeInBytes: runtimeOptions.maxBatchSizeInBytes,
1742
- disablePartialFlush: disablePartialFlush === true,
1772
+ // If we disable flush before process, we must be ready to flush partial batches
1773
+ flushPartialBatches: this.disableFlushBeforeProcess,
1743
1774
  },
1744
1775
  logger: this.mc.logger,
1745
1776
  groupingManager: opGroupingManager,
1746
1777
  getCurrentSequenceNumbers: () => ({
1778
+ // Note: These sequence numbers only change when DeltaManager processes an incoming op
1747
1779
  referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
1748
1780
  clientSequenceNumber: this._processedClientSequenceNumber,
1749
1781
  }),
1750
1782
  reSubmit: this.reSubmit.bind(this),
1751
- opReentrancy: () => this.ensureNoDataModelChangesCalls > 0,
1783
+ opReentrancy: () => this.dataModelChangeRunner.running,
1752
1784
  closeContainer: this.closeFn,
1753
1785
  });
1754
1786
 
@@ -1918,8 +1950,8 @@ export class ContainerRuntime
1918
1950
  sessionRuntimeSchema: JSON.stringify(this.sessionSchema),
1919
1951
  featureGates: JSON.stringify({
1920
1952
  ...featureGatesForTelemetry,
1921
- disablePartialFlush,
1922
1953
  closeSummarizerDelayOverride,
1954
+ disableFlushBeforeProcess: this.disableFlushBeforeProcess,
1923
1955
  }),
1924
1956
  telemetryDocumentId: this.telemetryDocumentId,
1925
1957
  groupedBatchingEnabled: this.groupedBatchingEnabled,
@@ -2607,6 +2639,26 @@ export class ContainerRuntime
2607
2639
 
2608
2640
  this.verifyNotClosed();
2609
2641
 
2642
+ if (!this.disableFlushBeforeProcess) {
2643
+ // Reference Sequence Number may be about to change, and it must be consistent across a batch, so flush now
2644
+ this.outbox.flush();
2645
+ }
2646
+
2647
+ this.ensureNoDataModelChanges(() => {
2648
+ this.processInboundMessageOrBatch(messageCopy, local);
2649
+ });
2650
+ }
2651
+
2652
+ /**
2653
+ * Implementation of core logic for {@link ContainerRuntime.process}, once preconditions are established
2654
+ *
2655
+ * @param messageCopy - Shallow copy of the sequenced message. If it's a virtualized batch, we'll process
2656
+ * all messages in the batch here.
2657
+ */
2658
+ private processInboundMessageOrBatch(
2659
+ messageCopy: ISequencedDocumentMessage,
2660
+ local: boolean,
2661
+ ): void {
2610
2662
  // Whether or not the message appears to be a runtime message from an up-to-date client.
2611
2663
  // It may be a legacy runtime message (ie already unpacked and ContainerMessageType)
2612
2664
  // or something different, like a system message.
@@ -2768,9 +2820,7 @@ export class ContainerRuntime
2768
2820
  try {
2769
2821
  if (!runtimeBatch) {
2770
2822
  for (const { message } of messagesWithMetadata) {
2771
- this.ensureNoDataModelChanges(() => {
2772
- this.observeNonRuntimeMessage(message);
2773
- });
2823
+ this.observeNonRuntimeMessage(message);
2774
2824
  }
2775
2825
  return;
2776
2826
  }
@@ -2794,21 +2844,19 @@ export class ContainerRuntime
2794
2844
  if (!groupedBatch) {
2795
2845
  for (const { message, localOpMetadata } of messagesWithMetadata) {
2796
2846
  updateSequenceNumbers(message);
2797
- this.ensureNoDataModelChanges(() => {
2798
- this.validateAndProcessRuntimeMessages(
2799
- message as InboundSequencedContainerRuntimeMessage,
2800
- [
2801
- {
2802
- contents: message.contents,
2803
- localOpMetadata,
2804
- clientSequenceNumber: message.clientSequenceNumber,
2805
- },
2806
- ],
2807
- local,
2808
- savedOp,
2809
- );
2810
- this.emit("op", message, true /* runtimeMessage */);
2811
- });
2847
+ this.validateAndProcessRuntimeMessages(
2848
+ message as InboundSequencedContainerRuntimeMessage,
2849
+ [
2850
+ {
2851
+ contents: message.contents,
2852
+ localOpMetadata,
2853
+ clientSequenceNumber: message.clientSequenceNumber,
2854
+ },
2855
+ ],
2856
+ local,
2857
+ savedOp,
2858
+ );
2859
+ this.emit("op", message, true /* runtimeMessage */);
2812
2860
  }
2813
2861
  return;
2814
2862
  }
@@ -2817,17 +2865,14 @@ export class ContainerRuntime
2817
2865
  let previousMessage: InboundSequencedContainerRuntimeMessage | undefined;
2818
2866
 
2819
2867
  // Process the previous bunch of messages.
2820
- const sendBunchedMessages = (): void => {
2868
+ const processBunchedMessages = (): void => {
2821
2869
  assert(previousMessage !== undefined, 0xa67 /* previous message must exist */);
2822
- this.ensureNoDataModelChanges(() => {
2823
- this.validateAndProcessRuntimeMessages(
2824
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2825
- previousMessage!,
2826
- bunchedMessagesContent,
2827
- local,
2828
- savedOp,
2829
- );
2830
- });
2870
+ this.validateAndProcessRuntimeMessages(
2871
+ previousMessage,
2872
+ bunchedMessagesContent,
2873
+ local,
2874
+ savedOp,
2875
+ );
2831
2876
  bunchedMessagesContent = [];
2832
2877
  };
2833
2878
 
@@ -2839,7 +2884,7 @@ export class ContainerRuntime
2839
2884
  for (const { message, localOpMetadata } of messagesWithMetadata) {
2840
2885
  const currentMessage = updateSequenceNumbers(message);
2841
2886
  if (previousMessage && previousMessage.type !== currentMessage.type) {
2842
- sendBunchedMessages();
2887
+ processBunchedMessages();
2843
2888
  }
2844
2889
  previousMessage = currentMessage;
2845
2890
  bunchedMessagesContent.push({
@@ -2850,7 +2895,7 @@ export class ContainerRuntime
2850
2895
  }
2851
2896
 
2852
2897
  // Process the last bunch of messages.
2853
- sendBunchedMessages();
2898
+ processBunchedMessages();
2854
2899
 
2855
2900
  // Send the "op" events for the messages now that the ops have been processed.
2856
2901
  for (const { message } of messagesWithMetadata) {
@@ -3052,8 +3097,8 @@ export class ContainerRuntime
3052
3097
  */
3053
3098
  private flush(resubmittingBatchId?: BatchId): void {
3054
3099
  assert(
3055
- this._orderSequentiallyCalls === 0,
3056
- 0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */,
3100
+ !this.batchRunner.running,
3101
+ 0x24c /* "Cannot call `flush()` while manually accumulating a batch (e.g. under orderSequentially) */,
3057
3102
  );
3058
3103
 
3059
3104
  this.outbox.flush(resubmittingBatchId);
@@ -3065,57 +3110,60 @@ export class ContainerRuntime
3065
3110
  */
3066
3111
  public orderSequentially<T>(callback: () => T): T {
3067
3112
  let checkpoint: IBatchCheckpoint | undefined;
3068
- let result: T;
3113
+ const checkpointDirtyState = this.dirtyContainer;
3069
3114
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
3070
3115
  // Note: we are not touching any batches other than mainBatch here, for two reasons:
3071
3116
  // 1. It would not help, as other batches are flushed independently from main batch.
3072
3117
  // 2. There is no way to undo process of data store creation, blob creation, ID compressor ops, or other things tracked by other batches.
3073
3118
  checkpoint = this.outbox.getBatchCheckpoints().mainBatch;
3074
3119
  }
3075
- try {
3076
- this._orderSequentiallyCalls++;
3077
- result = callback();
3078
- } catch (error) {
3079
- if (checkpoint) {
3080
- // This will throw and close the container if rollback fails
3081
- try {
3082
- checkpoint.rollback((message: BatchMessage) =>
3083
- this.rollback(message.contents, message.localOpMetadata),
3120
+ const result = this.batchRunner.run(() => {
3121
+ try {
3122
+ return callback();
3123
+ } catch (error) {
3124
+ if (checkpoint) {
3125
+ // This will throw and close the container if rollback fails
3126
+ try {
3127
+ checkpoint.rollback((message: BatchMessage) =>
3128
+ this.rollback(message.contents, message.localOpMetadata),
3129
+ );
3130
+ // reset the dirty state after rollback to what it was before to keep it consistent
3131
+ if (this.dirtyContainer !== checkpointDirtyState) {
3132
+ this.updateDocumentDirtyState(checkpointDirtyState);
3133
+ }
3134
+ } catch (error_) {
3135
+ const error2 = wrapError(error_, (message) => {
3136
+ return DataProcessingError.create(
3137
+ `RollbackError: ${message}`,
3138
+ "checkpointRollback",
3139
+ undefined,
3140
+ ) as DataProcessingError;
3141
+ });
3142
+ this.closeFn(error2);
3143
+ throw error2;
3144
+ }
3145
+ } else {
3146
+ this.closeFn(
3147
+ wrapError(
3148
+ error,
3149
+ (errorMessage) =>
3150
+ new GenericError(
3151
+ `orderSequentially callback exception: ${errorMessage}`,
3152
+ error,
3153
+ {
3154
+ orderSequentiallyCalls: this.batchRunner.runs,
3155
+ },
3156
+ ),
3157
+ ),
3084
3158
  );
3085
- } catch (error_) {
3086
- const error2 = wrapError(error_, (message) => {
3087
- return DataProcessingError.create(
3088
- `RollbackError: ${message}`,
3089
- "checkpointRollback",
3090
- undefined,
3091
- ) as DataProcessingError;
3092
- });
3093
- this.closeFn(error2);
3094
- throw error2;
3095
3159
  }
3096
- } else {
3097
- this.closeFn(
3098
- wrapError(
3099
- error,
3100
- (errorMessage) =>
3101
- new GenericError(
3102
- `orderSequentially callback exception: ${errorMessage}`,
3103
- error,
3104
- {
3105
- orderSequentiallyCalls: this._orderSequentiallyCalls,
3106
- },
3107
- ),
3108
- ),
3109
- );
3110
- }
3111
3160
 
3112
- throw error; // throw the original error for the consumer of the runtime
3113
- } finally {
3114
- this._orderSequentiallyCalls--;
3115
- }
3161
+ throw error; // throw the original error for the consumer of the runtime
3162
+ }
3163
+ });
3116
3164
 
3117
3165
  // We don't flush on TurnBased since we expect all messages in the same JS turn to be part of the same batch
3118
- if (this.flushMode !== FlushMode.TurnBased && this._orderSequentiallyCalls === 0) {
3166
+ if (this.flushMode !== FlushMode.TurnBased && !this.batchRunner.running) {
3119
3167
  this.flush();
3120
3168
  }
3121
3169
  return result;
@@ -3196,7 +3244,7 @@ export class ContainerRuntime
3196
3244
  * Typically ops are batched and later flushed together, but in some cases we want to flush immediately.
3197
3245
  */
3198
3246
  private currentlyBatching(): boolean {
3199
- return this.flushMode !== FlushMode.Immediate || this._orderSequentiallyCalls !== 0;
3247
+ return this.flushMode !== FlushMode.Immediate || this.batchRunner.running;
3200
3248
  }
3201
3249
 
3202
3250
  private readonly _quorum: IQuorumClients;
@@ -4265,8 +4313,8 @@ export class ContainerRuntime
4265
4313
 
4266
4314
  default: {
4267
4315
  assert(
4268
- this._orderSequentiallyCalls > 0,
4269
- 0x587 /* Unreachable unless running under orderSequentially */,
4316
+ this.batchRunner.running,
4317
+ 0x587 /* Unreachable unless manually accumulating a batch */,
4270
4318
  );
4271
4319
  break;
4272
4320
  }
@@ -4306,7 +4354,7 @@ export class ContainerRuntime
4306
4354
  * for correlation to detect container forking.
4307
4355
  */
4308
4356
  private reSubmitBatch(batch: PendingMessageResubmitData[], batchId: BatchId): void {
4309
- this.orderSequentially(() => {
4357
+ this.batchRunner.run(() => {
4310
4358
  for (const message of batch) {
4311
4359
  this.reSubmit(message);
4312
4360
  }
@@ -4554,8 +4602,8 @@ export class ContainerRuntime
4554
4602
  public getPendingLocalState(props?: IGetPendingLocalStateProps): unknown {
4555
4603
  this.verifyNotClosed();
4556
4604
 
4557
- if (this._orderSequentiallyCalls !== 0) {
4558
- throw new UsageError("can't get state during orderSequentially");
4605
+ if (this.batchRunner.running) {
4606
+ throw new UsageError("can't get state while manually accumulating a batch");
4559
4607
  }
4560
4608
  this.imminentClosure ||= props?.notifyImminentClosure ?? false;
4561
4609
 
@@ -3,7 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { TypedEventEmitter } from "@fluid-internal/client-utils";
6
+ import { TypedEventEmitter, type ILayerCompatDetails } from "@fluid-internal/client-utils";
7
7
  import { AttachState, IAudience } from "@fluidframework/container-definitions";
8
8
  import { IDeltaManager } from "@fluidframework/container-definitions/internal";
9
9
  import {
@@ -77,6 +77,10 @@ import {
77
77
  tagCodeArtifacts,
78
78
  } from "@fluidframework/telemetry-utils/internal";
79
79
 
80
+ import {
81
+ runtimeCompatDetailsForDataStore,
82
+ validateDatastoreCompatibility,
83
+ } from "./runtimeLayerCompatState.js";
80
84
  import {
81
85
  // eslint-disable-next-line import/no-deprecated
82
86
  ReadFluidDataStoreAttributes,
@@ -278,6 +282,14 @@ export abstract class FluidDataStoreContext
278
282
  return this.registry;
279
283
  }
280
284
 
285
+ /**
286
+ * The compatibility details of the Runtime layer that is exposed to the DataStore layer
287
+ * for validating DataStore-Runtime compatibility.
288
+ */
289
+ public get ILayerCompatDetails(): ILayerCompatDetails {
290
+ return runtimeCompatDetailsForDataStore;
291
+ }
292
+
281
293
  private baseSnapshotSequenceNumber: number | undefined;
282
294
 
283
295
  /**
@@ -573,6 +585,7 @@ export abstract class FluidDataStoreContext
573
585
 
574
586
  const channel = await factory.instantiateDataStore(this, existing);
575
587
  assert(channel !== undefined, 0x140 /* "undefined channel on datastore context" */);
588
+
576
589
  await this.bindRuntime(channel, existing);
577
590
  // This data store may have been disposed before the channel is created during realization. If so,
578
591
  // dispose the channel now.
@@ -860,6 +873,13 @@ export abstract class FluidDataStoreContext
860
873
  }
861
874
 
862
875
  protected completeBindingRuntime(channel: IFluidDataStoreChannel): void {
876
+ // Validate that the DataStore is compatible with this Runtime.
877
+ const maybeDataStoreCompatDetails = channel as FluidObject<ILayerCompatDetails>;
878
+ validateDatastoreCompatibility(
879
+ maybeDataStoreCompatDetails.ILayerCompatDetails,
880
+ this.dispose.bind(this),
881
+ );
882
+
863
883
  // And now mark the runtime active
864
884
  this.loaded = true;
865
885
  this.channel = channel;
@@ -1005,6 +1025,7 @@ export abstract class FluidDataStoreContext
1005
1025
  callSite,
1006
1026
  undefined /* sequencedMessage */,
1007
1027
  safeTelemetryProps,
1028
+ 30 /* stackTraceLimit */,
1008
1029
  );
1009
1030
 
1010
1031
  this.mc.logger.sendTelemetryEvent(
@@ -1,28 +1,30 @@
1
1
  # Garbage Collection
2
2
 
3
- Garbage collection (GC) is the process by which Fluid Framework safely delete objects that are not used. The only responsibility of the users of Fluid Framework is to add and remove references to Fluid objects correctly.
3
+ Garbage collection (GC) is the process by which Fluid Framework safely deletes objects that are not used.
4
+ GC reduces the size of the Fluid file at rest, the in-memory content and the summary that is uploaded to / downloaded from the server.
5
+ It saves COGS on the server and it makes containers load faster as there is less data to download and process.
4
6
 
5
- ## Why have Garbage Collection?
7
+ All Fluid objects that are in use must be properly referenced so that they are not deleted by GC. Similarly, references to all unused Fluid objects should be removed so that they can be deleted by GC.
6
8
 
7
- GC reduces the size of the Fluid file at rest, the in-memory content and the summary that is uploaded to / downloaded from the server. It saves COGS on the server and it makes containers load faster as there is less data to download and process.
9
+ It is the responsibility of the users of Fluid Framework to correctly add and remove references to Fluid objects.
8
10
 
9
- ## What do I need to do?
11
+ ## Referencing / unreferencing Fluid objects
10
12
 
11
- All Fluid objects that are in use must be properly referenced so that they are not deleted by GC. Similarly, references to all unused Fluid objects should be removed so that they can be deleted by GC. It is the responsibility of the users of Fluid Framework to correctly add and remove references to Fluid objects.
13
+ Currently, the only Fluid objects that are eligible for GC are data stores and attachment blobs. The following sections describe how you can mark them as referenced or unreferenced.
12
14
 
13
- ## How do I reference / unreference Fluid objects?
14
-
15
- Currently, the only Fluid objects that are eligible for GC are data stores and attachment blobs. The following sections describe how you can mark them as referenced or unreferenced. These sections speak of a "referenced DDS" which refers to a DDS that is created by a referenced data store.
15
+ These sections speak of a "referenced DDS" which refers to a DDS that is created by a referenced data store.
16
16
 
17
17
  ### Data stores
18
18
 
19
19
  There are 2 ways to reference a data store:
20
20
 
21
- - Store the data stores's handle (see [IFluidHandle](../../../../../packages/common/core-interfaces/src/handles.ts) in a referenced DDS that supports handle in its data. For example, a data store's handle can be stored in a referenced `SharedMap` DDS.
21
+ - Store the data store's handle (see [IFluidHandle](../../../../../packages/common/core-interfaces/src/handles.ts)) in a referenced DDS that supports handle in its data.
22
+ For example, a data store's handle can be stored in a referenced `SharedMap` DDS.
22
23
 
23
- Note that storing a handle of any of a data store's DDS will also mark the data store as referenced.
24
+ Storing a handle of any of a data store's DDS will also mark the data store as referenced.
24
25
 
25
- - Alias the data store. Aliased data stores are rooted in the container, i.e., they are always referenced and cannot be unreferenced later. Aliased data stores can never be deleted so only do so if you want them to live forever.
26
+ - Alias the data store by calling [trySetAlias](../../../runtime-definitions/src/dataStoreContext.ts) on a data store during creation. Aliased data stores are rooted in the container, i.e., they are always referenced and cannot be unreferenced later.
27
+ Aliased data stores can never be deleted so only do so if you want them to live forever.
26
28
 
27
29
  Once there are no more referenced DDSes in the container containing a handle to a particular data store, that data store is unreferenced and is eligible for GC.
28
30
 
@@ -30,7 +32,7 @@ Once there are no more referenced DDSes in the container containing a handle to
30
32
 
31
33
  ### Attachment blobs
32
34
 
33
- The only way to reference an attachment blob is to store its IFluidHandle in a referenced DDS similar to data stores.
35
+ The only way to reference an attachment blob is to store its [IFluidHandle](../../../../../packages/common/core-interfaces/src/handles.ts) in a referenced DDS similar to data stores.
34
36
 
35
37
  Once there are no more referenced DDSes in the container containing a handle to a particular attachment blob, that attachment blob is unreferenced and is eligible for GC.
36
38
 
@@ -59,8 +61,9 @@ GC sweep phase runs in two stages:
59
61
 
60
62
  - The first stage is the "Tombstone" stage, where objects are marked as Tombstones, meaning GC believes they will
61
63
  never be referenced again and are safe to delete. They are not yet deleted at this point, but any attempt to
62
- load them will fail. This way, there's a chance to recover a Tombstoned object in case we detect it's still being used.
63
- - The second stage is the "Sweep" or "Delete" stage, where the objects are fully deleted.
64
+ load them will fail. Loading them will trigger "auto recovery" where the timestamp of when this object is unreferenced will be reset to now, thereby extending its lifetime.
65
+ This way, there's a chance to recover a Tombstoned object in case we detect it's still being used.
66
+ - The second stage is the "Sweep" stage, where the objects are fully deleted.
64
67
  This occurs after a configurable delay called the "Sweep Grace Period", to give time for application teams
65
68
  to monitor for Tombstone-related errors and react before delete occurs.
66
69
 
@@ -91,8 +94,3 @@ and then later update the passed-in GC Options to finalize the configuration in
91
94
  ### Enabling Sweep Phase
92
95
 
93
96
  To enable the Sweep Phase for new documents, you must set the `enableGCSweep` GC Option to true.
94
-
95
- ### More Advanced Configuration
96
-
97
- For additional behaviors that can be configured (e.g. for testing), please see these
98
- [Advanced Configuration](./gcEarlyAdoption.md#more-advanced-configurations) docs.
@@ -279,7 +279,7 @@ export class GCTelemetryTracker {
279
279
  const event = {
280
280
  eventName: `${state}Object_${usageType}`,
281
281
  ...tagCodeArtifacts({ pkg: packagePath?.join("/") }),
282
- stack: generateStack(),
282
+ stack: generateStack(30),
283
283
  id,
284
284
  fromId,
285
285
  headers: { ...headers },
@@ -314,7 +314,7 @@ export class GCTelemetryTracker {
314
314
  const event = {
315
315
  eventName: `GC_Tombstone_${nodeType}_${eventUsageName}`,
316
316
  ...tagCodeArtifacts({ pkg: packagePath?.join("/") }),
317
- stack: generateStack(),
317
+ stack: generateStack(30),
318
318
  id,
319
319
  fromId,
320
320
  headers: { ...headers },
@@ -4,6 +4,11 @@
4
4
  */
5
5
 
6
6
  import { assert } from "@fluidframework/core-utils/internal";
7
+ import {
8
+ LoggingError,
9
+ tagData,
10
+ TelemetryDataTag,
11
+ } from "@fluidframework/telemetry-utils/internal";
7
12
 
8
13
  import { ICompressionRuntimeOptions } from "../containerRuntime.js";
9
14
  import { asBatchMetadata, type IBatchMetadata } from "../metadata.js";
@@ -99,7 +104,8 @@ export class BatchManager {
99
104
  private get referenceSequenceNumber(): number | undefined {
100
105
  return this.pendingBatch.length === 0
101
106
  ? undefined
102
- : this.pendingBatch[this.pendingBatch.length - 1].referenceSequenceNumber;
107
+ : // NOTE: In case of reentrant ops, there could be multiple reference sequence numbers, but we will rebase before submitting.
108
+ this.pendingBatch[this.pendingBatch.length - 1].referenceSequenceNumber;
103
109
  }
104
110
 
105
111
  /**
@@ -166,17 +172,25 @@ export class BatchManager {
166
172
  * Capture the pending state at this point
167
173
  */
168
174
  public checkpoint(): IBatchCheckpoint {
175
+ const startSequenceNumber = this.clientSequenceNumber;
169
176
  const startPoint = this.pendingBatch.length;
170
177
  return {
171
178
  rollback: (process: (message: BatchMessage) => void) => {
172
- for (let i = this.pendingBatch.length; i > startPoint; ) {
173
- i--;
174
- const message = this.pendingBatch[i];
179
+ this.clientSequenceNumber = startSequenceNumber;
180
+ const rollbackOpsLifo = this.pendingBatch.splice(startPoint).reverse();
181
+ for (const message of rollbackOpsLifo) {
175
182
  this.batchContentSize -= message.contents?.length ?? 0;
176
183
  process(message);
177
184
  }
178
-
179
- this.pendingBatch.length = startPoint;
185
+ const count = this.pendingBatch.length - startPoint;
186
+ if (count !== 0) {
187
+ throw new LoggingError("Ops generated durning rollback", {
188
+ count,
189
+ ...tagData(TelemetryDataTag.UserData, {
190
+ ops: JSON.stringify(this.pendingBatch.slice(startPoint).map((b) => b.contents)),
191
+ }),
192
+ });
193
+ }
180
194
  },
181
195
  };
182
196
  }