@fluidframework/container-runtime 2.91.0 → 2.93.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 (140) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/api-report/container-runtime.legacy.beta.api.md +2 -0
  4. package/container-runtime.test-files.tar +0 -0
  5. package/dist/containerCompatibility.d.ts +1 -1
  6. package/dist/containerCompatibility.d.ts.map +1 -1
  7. package/dist/containerCompatibility.js.map +1 -1
  8. package/dist/containerRuntime.d.ts +38 -11
  9. package/dist/containerRuntime.d.ts.map +1 -1
  10. package/dist/containerRuntime.js +118 -86
  11. package/dist/containerRuntime.js.map +1 -1
  12. package/dist/gc/garbageCollection.d.ts +1 -0
  13. package/dist/gc/garbageCollection.d.ts.map +1 -1
  14. package/dist/gc/garbageCollection.js +3 -8
  15. package/dist/gc/garbageCollection.js.map +1 -1
  16. package/dist/gc/gcDefinitions.d.ts +4 -0
  17. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  18. package/dist/gc/gcDefinitions.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/legacy.d.ts +1 -1
  24. package/dist/opLifecycle/batchManager.d.ts +3 -9
  25. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  26. package/dist/opLifecycle/batchManager.js +5 -3
  27. package/dist/opLifecycle/batchManager.js.map +1 -1
  28. package/dist/opLifecycle/index.d.ts +1 -1
  29. package/dist/opLifecycle/index.d.ts.map +1 -1
  30. package/dist/opLifecycle/index.js +2 -1
  31. package/dist/opLifecycle/index.js.map +1 -1
  32. package/dist/opLifecycle/opGroupingManager.d.ts +6 -0
  33. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  34. package/dist/opLifecycle/opGroupingManager.js +11 -2
  35. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  36. package/dist/opLifecycle/opSerialization.d.ts +3 -1
  37. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  38. package/dist/opLifecycle/opSerialization.js +11 -9
  39. package/dist/opLifecycle/opSerialization.js.map +1 -1
  40. package/dist/opLifecycle/outbox.d.ts +8 -11
  41. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  42. package/dist/opLifecycle/outbox.js +42 -66
  43. package/dist/opLifecycle/outbox.js.map +1 -1
  44. package/dist/packageVersion.d.ts +1 -1
  45. package/dist/packageVersion.js +1 -1
  46. package/dist/packageVersion.js.map +1 -1
  47. package/dist/pendingStateManager.d.ts +8 -9
  48. package/dist/pendingStateManager.d.ts.map +1 -1
  49. package/dist/pendingStateManager.js +24 -22
  50. package/dist/pendingStateManager.js.map +1 -1
  51. package/dist/public.d.ts +1 -1
  52. package/dist/runtimeLayerCompatState.d.ts +2 -2
  53. package/dist/summary/documentSchema.d.ts +9 -3
  54. package/dist/summary/documentSchema.d.ts.map +1 -1
  55. package/dist/summary/documentSchema.js +19 -3
  56. package/dist/summary/documentSchema.js.map +1 -1
  57. package/dist/summary/orderedClientElection.js +2 -2
  58. package/dist/summary/orderedClientElection.js.map +1 -1
  59. package/dist/summary/summaryManager.d.ts +1 -0
  60. package/dist/summary/summaryManager.d.ts.map +1 -1
  61. package/dist/summary/summaryManager.js +9 -0
  62. package/dist/summary/summaryManager.js.map +1 -1
  63. package/eslint.config.mts +1 -1
  64. package/internal.d.ts +1 -1
  65. package/legacy.d.ts +1 -1
  66. package/lib/containerCompatibility.d.ts +1 -1
  67. package/lib/containerCompatibility.d.ts.map +1 -1
  68. package/lib/containerCompatibility.js.map +1 -1
  69. package/lib/containerRuntime.d.ts +38 -11
  70. package/lib/containerRuntime.d.ts.map +1 -1
  71. package/lib/containerRuntime.js +118 -87
  72. package/lib/containerRuntime.js.map +1 -1
  73. package/lib/gc/garbageCollection.d.ts +1 -0
  74. package/lib/gc/garbageCollection.d.ts.map +1 -1
  75. package/lib/gc/garbageCollection.js +3 -8
  76. package/lib/gc/garbageCollection.js.map +1 -1
  77. package/lib/gc/gcDefinitions.d.ts +4 -0
  78. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  79. package/lib/gc/gcDefinitions.js.map +1 -1
  80. package/lib/index.d.ts +1 -1
  81. package/lib/index.d.ts.map +1 -1
  82. package/lib/index.js +1 -1
  83. package/lib/index.js.map +1 -1
  84. package/lib/legacy.d.ts +1 -1
  85. package/lib/opLifecycle/batchManager.d.ts +3 -9
  86. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  87. package/lib/opLifecycle/batchManager.js +5 -3
  88. package/lib/opLifecycle/batchManager.js.map +1 -1
  89. package/lib/opLifecycle/index.d.ts +1 -1
  90. package/lib/opLifecycle/index.d.ts.map +1 -1
  91. package/lib/opLifecycle/index.js +1 -1
  92. package/lib/opLifecycle/index.js.map +1 -1
  93. package/lib/opLifecycle/opGroupingManager.d.ts +6 -0
  94. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  95. package/lib/opLifecycle/opGroupingManager.js +10 -1
  96. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  97. package/lib/opLifecycle/opSerialization.d.ts +3 -1
  98. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  99. package/lib/opLifecycle/opSerialization.js +11 -9
  100. package/lib/opLifecycle/opSerialization.js.map +1 -1
  101. package/lib/opLifecycle/outbox.d.ts +8 -11
  102. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  103. package/lib/opLifecycle/outbox.js +43 -67
  104. package/lib/opLifecycle/outbox.js.map +1 -1
  105. package/lib/packageVersion.d.ts +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/pendingStateManager.d.ts +8 -9
  109. package/lib/pendingStateManager.d.ts.map +1 -1
  110. package/lib/pendingStateManager.js +24 -22
  111. package/lib/pendingStateManager.js.map +1 -1
  112. package/lib/public.d.ts +1 -1
  113. package/lib/runtimeLayerCompatState.d.ts +2 -2
  114. package/lib/summary/documentSchema.d.ts +9 -3
  115. package/lib/summary/documentSchema.d.ts.map +1 -1
  116. package/lib/summary/documentSchema.js +19 -3
  117. package/lib/summary/documentSchema.js.map +1 -1
  118. package/lib/summary/orderedClientElection.js +2 -2
  119. package/lib/summary/orderedClientElection.js.map +1 -1
  120. package/lib/summary/summaryManager.d.ts +1 -0
  121. package/lib/summary/summaryManager.d.ts.map +1 -1
  122. package/lib/summary/summaryManager.js +9 -0
  123. package/lib/summary/summaryManager.js.map +1 -1
  124. package/lib/tsdoc-metadata.json +1 -1
  125. package/package.json +27 -28
  126. package/src/containerCompatibility.ts +2 -0
  127. package/src/containerRuntime.ts +163 -106
  128. package/src/gc/garbageCollection.ts +4 -9
  129. package/src/gc/gcDefinitions.ts +4 -0
  130. package/src/index.ts +1 -0
  131. package/src/opLifecycle/batchManager.ts +6 -13
  132. package/src/opLifecycle/index.ts +1 -0
  133. package/src/opLifecycle/opGroupingManager.ts +11 -1
  134. package/src/opLifecycle/opSerialization.ts +14 -12
  135. package/src/opLifecycle/outbox.ts +53 -86
  136. package/src/packageVersion.ts +1 -1
  137. package/src/pendingStateManager.ts +31 -33
  138. package/src/summary/documentSchema.ts +25 -2
  139. package/src/summary/orderedClientElection.ts +2 -2
  140. package/src/summary/summaryManager.ts +11 -0
@@ -95,6 +95,7 @@ import { FetchSource, MessageType } from "@fluidframework/driver-definitions/int
95
95
  import { readAndParse } from "@fluidframework/driver-utils/internal";
96
96
  import type { IIdCompressor } from "@fluidframework/id-compressor";
97
97
  import type {
98
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
98
99
  IIdCompressorCore,
99
100
  IdCreationRange,
100
101
  SerializedIdCompressorWithNoSession,
@@ -130,6 +131,7 @@ import type {
130
131
  IContainerRuntimeBaseInternal,
131
132
  MinimumVersionForCollab,
132
133
  ContainerExtensionExpectations,
134
+ ContainerRuntimeBaseAlpha,
133
135
  } from "@fluidframework/runtime-definitions/internal";
134
136
  import {
135
137
  addBlobToSummary,
@@ -219,7 +221,6 @@ import {
219
221
  type IGCRuntimeOptions,
220
222
  type IGCStats,
221
223
  type IGarbageCollector,
222
- gcGenerationOptionName,
223
224
  type GarbageCollectionMessage,
224
225
  type IGarbageCollectionRuntime,
225
226
  } from "./gc/index.js";
@@ -243,6 +244,7 @@ import {
243
244
  DuplicateBatchDetector,
244
245
  ensureContentsDeserialized,
245
246
  type IBatchCheckpoint,
247
+ largeBatchThreshold,
246
248
  OpCompressor,
247
249
  OpDecompressor,
248
250
  OpGroupingManager,
@@ -258,6 +260,7 @@ import {
258
260
  type IPendingLocalState,
259
261
  PendingStateManager,
260
262
  type PendingBatchResubmitMetadata,
263
+ type IPendingMessage,
261
264
  } from "./pendingStateManager.js";
262
265
  import { BatchRunCounter, RunCounter } from "./runCounter.js";
263
266
  import {
@@ -475,6 +478,25 @@ export interface ContainerRuntimeOptions {
475
478
  * When enabled (`true`), createBlob will return a handle before the blob upload completes.
476
479
  */
477
480
  readonly createBlobPayloadPending: true | undefined;
481
+
482
+ /**
483
+ * Controls automatic batch flushing during staging mode.
484
+ * Normal turn-based/async flush scheduling is suppressed while in staging mode
485
+ * until the accumulated batch reaches this many ops, at which point the batch
486
+ * is flushed. Incoming ops always break the current batch regardless of this setting.
487
+ *
488
+ * Set to Infinity to only break batches on system events (incoming ops).
489
+ *
490
+ * @defaultValue `largeBatchThreshold` (currently 1000)
491
+ */
492
+ readonly stagingModeAutoFlushThreshold: number;
493
+
494
+ /**
495
+ * When this property is set to true, the runtime will never send DocumentSchemaChange ops
496
+ * and will throw an error if any incoming DocumentSchemaChange ops are received.
497
+ * This effectively freezes the document schema at whatever state it was in when the document was created.
498
+ */
499
+ readonly disableSchemaUpgrade: boolean;
478
500
  }
479
501
 
480
502
  /**
@@ -607,6 +629,16 @@ const defaultMaxBatchSizeInBytes = 700 * 1024;
607
629
 
608
630
  const defaultChunkSizeInBytes = 204800;
609
631
 
632
+ /**
633
+ * Default maximum ops per staging-mode batch before automatic flush scheduling resumes.
634
+ *
635
+ * Chosen based on production telemetry: copy-paste operations routinely produce batches
636
+ * of 1000+ ops (435K instances over 30 days), and receivers on modern Fluid versions
637
+ * handle them without issues. Uses {@link largeBatchThreshold} to stay aligned with
638
+ * the existing "large batch" telemetry threshold ({@link OpGroupingManager}).
639
+ */
640
+ const defaultStagingModeAutoFlushThreshold = largeBatchThreshold;
641
+
610
642
  /**
611
643
  * The default time to wait for pending ops to be processed during summarization
612
644
  */
@@ -804,6 +836,24 @@ export async function loadContainerRuntime(
804
836
  return ContainerRuntime.loadRuntime(params);
805
837
  }
806
838
 
839
+ /**
840
+ * Alpha variant of {@link loadContainerRuntime} that returns the runtime in an
841
+ * extendable object, allowing additional properties to be added in the future.
842
+ *
843
+ * @param params - An object which specifies all required and optional params necessary to instantiate a runtime.
844
+ * @returns An object containing the runtime.
845
+ *
846
+ * @legacy @alpha
847
+ */
848
+ export async function loadContainerRuntimeAlpha(params: LoadContainerRuntimeParams): Promise<{
849
+ runtime: IContainerRuntime & ContainerRuntimeBaseAlpha & IRuntime;
850
+ }> {
851
+ return ContainerRuntime.loadRuntime2({
852
+ ...params,
853
+ registry: new FluidDataStoreRegistry(params.registryEntries),
854
+ });
855
+ }
856
+
807
857
  const defaultMaxConsecutiveReconnects = 7;
808
858
 
809
859
  /**
@@ -879,14 +929,15 @@ export class ContainerRuntime
879
929
  return ContainerRuntime.loadRuntime2({
880
930
  ...params,
881
931
  registry: new FluidDataStoreRegistry(params.registryEntries),
882
- });
932
+ }).then((r) => r.runtime);
883
933
  }
884
934
 
885
935
  /**
886
- * Load the stores from a snapshot and returns the runtime.
936
+ * Load the stores from a snapshot and returns an object containing the runtime.
887
937
  * @remarks
888
938
  * Same as {@link ContainerRuntime.loadRuntime},
889
939
  * but with `registry` instead of `registryEntries` and more `runtimeOptions`.
940
+ * Returns `{ runtime }` to allow future extensions (e.g. staging mode controls).
890
941
  */
891
942
  public static async loadRuntime2(
892
943
  params: Omit<LoadContainerRuntimeParams, "registryEntries" | "runtimeOptions"> & {
@@ -905,7 +956,7 @@ export class ContainerRuntime
905
956
  */
906
957
  runtimeOptions?: IContainerRuntimeOptionsInternal;
907
958
  },
908
- ): Promise<ContainerRuntime> {
959
+ ): Promise<{ runtime: ContainerRuntime }> {
909
960
  const {
910
961
  context,
911
962
  registry,
@@ -961,6 +1012,8 @@ export class ContainerRuntime
961
1012
  loadSequenceNumberVerification: "close",
962
1013
  maxBatchSizeInBytes: defaultMaxBatchSizeInBytes,
963
1014
  chunkSizeInBytes: defaultChunkSizeInBytes,
1015
+ stagingModeAutoFlushThreshold: defaultStagingModeAutoFlushThreshold,
1016
+ disableSchemaUpgrade: false,
964
1017
  };
965
1018
 
966
1019
  const defaultConfigs = {
@@ -986,6 +1039,8 @@ export class ContainerRuntime
986
1039
  ? disabledCompressionConfig
987
1040
  : defaultConfigs.compressionOptions,
988
1041
  createBlobPayloadPending = defaultConfigs.createBlobPayloadPending,
1042
+ stagingModeAutoFlushThreshold = defaultConfigs.stagingModeAutoFlushThreshold,
1043
+ disableSchemaUpgrade = defaultConfigs.disableSchemaUpgrade,
989
1044
  }: IContainerRuntimeOptionsInternal = runtimeOptions;
990
1045
 
991
1046
  // If explicitSchemaControl is off, ensure that options which require explicitSchemaControl are not enabled.
@@ -1130,6 +1185,7 @@ export class ContainerRuntime
1130
1185
  idCompressorMode = desiredIdCompressorMode;
1131
1186
  }
1132
1187
 
1188
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
1133
1189
  const createIdCompressorFn = (): IIdCompressor & IIdCompressorCore => {
1134
1190
  /**
1135
1191
  * Because the IdCompressor emits so much telemetry, this function is used to sample
@@ -1184,6 +1240,7 @@ export class ContainerRuntime
1184
1240
  },
1185
1241
  { minVersionForCollab },
1186
1242
  logger,
1243
+ disableSchemaUpgrade,
1187
1244
  );
1188
1245
 
1189
1246
  // If the minVersionForCollab for this client is greater than the existing one, we should use that one going forward.
@@ -1214,6 +1271,8 @@ export class ContainerRuntime
1214
1271
  enableGroupedBatching,
1215
1272
  explicitSchemaControl,
1216
1273
  createBlobPayloadPending,
1274
+ stagingModeAutoFlushThreshold,
1275
+ disableSchemaUpgrade,
1217
1276
  };
1218
1277
 
1219
1278
  validateMinimumVersionForCollab(updatedMinVersionForCollab);
@@ -1249,7 +1308,7 @@ export class ContainerRuntime
1249
1308
  // or zero. This must be done before Container replays saved ops.
1250
1309
  await runtime.pendingStateManager.applyStashedOpsAt(runtimeSequenceNumber ?? 0);
1251
1310
 
1252
- return runtime;
1311
+ return { runtime };
1253
1312
  }
1254
1313
 
1255
1314
  public readonly options: Record<string | number, unknown>;
@@ -1319,6 +1378,7 @@ export class ContainerRuntime
1319
1378
  return this.documentsSchemaController.sessionSchema.runtime;
1320
1379
  }
1321
1380
 
1381
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
1322
1382
  private _idCompressor: (IIdCompressor & IIdCompressorCore) | undefined;
1323
1383
 
1324
1384
  // We accumulate Id compressor Ops while Id compressor is not loaded yet (only for "delayed" mode)
@@ -1334,6 +1394,7 @@ export class ContainerRuntime
1334
1394
  /**
1335
1395
  * {@inheritDoc @fluidframework/runtime-definitions#IContainerRuntimeBase.idCompressor}
1336
1396
  */
1397
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
1337
1398
  public get idCompressor(): (IIdCompressor & IIdCompressorCore) | undefined {
1338
1399
  // Expose ID Compressor only if it's On from the start.
1339
1400
  // If container uses delayed mode, then we can only expose generateDocumentUniqueId() and nothing else.
@@ -1392,6 +1453,7 @@ export class ContainerRuntime
1392
1453
 
1393
1454
  private readonly batchRunner = new BatchRunCounter();
1394
1455
  private readonly _flushMode: FlushMode;
1456
+ private readonly stagingModeAutoFlushThreshold: number;
1395
1457
  /**
1396
1458
  * BatchId tracking is needed whenever there's a possibility of a "forked Container",
1397
1459
  * where the same local state is pending in two different running Containers, each of
@@ -1533,13 +1595,6 @@ export class ContainerRuntime
1533
1595
  return runtimeCompatDetailsForLoader;
1534
1596
  }
1535
1597
 
1536
- /**
1537
- * If true, will skip Outbox flushing before processing an incoming message (and on DeltaManager "op" event for loader back-compat),
1538
- * and instead the Outbox will check for a split batch on every submit.
1539
- * This is a kill-bit switch for this simplification of logic, in case it causes unexpected issues.
1540
- */
1541
- private readonly skipSafetyFlushDuringProcessStack: boolean;
1542
-
1543
1598
  private readonly extensions = new Map<ContainerExtensionId, ExtensionEntry>();
1544
1599
 
1545
1600
  /***/
@@ -1560,6 +1615,7 @@ export class ContainerRuntime
1560
1615
 
1561
1616
  blobManagerLoadInfo: IBlobManagerLoadInfo,
1562
1617
  private readonly _storage: IContainerStorageService,
1618
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
1563
1619
  private readonly createIdCompressorFn: () => IIdCompressor & IIdCompressorCore,
1564
1620
 
1565
1621
  private readonly documentsSchemaController: DocumentsSchemaController,
@@ -1750,15 +1806,6 @@ export class ContainerRuntime
1750
1806
  this.getConnectionState() === ConnectionState.CatchingUp
1751
1807
  : undefined;
1752
1808
 
1753
- this.mc.logger.sendTelemetryEvent({
1754
- eventName: "GCFeatureMatrix",
1755
- metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
1756
- inputs: JSON.stringify({
1757
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1758
- gcOptions_gcGeneration: runtimeOptions.gcOptions[gcGenerationOptionName],
1759
- }),
1760
- });
1761
-
1762
1809
  this.telemetryDocumentId = metadata?.telemetryDocumentId ?? uuid();
1763
1810
 
1764
1811
  const opGroupingManager = new OpGroupingManager(
@@ -1840,6 +1887,10 @@ export class ContainerRuntime
1840
1887
  this.closeFn(error);
1841
1888
  throw error;
1842
1889
  }
1890
+ this.stagingModeAutoFlushThreshold =
1891
+ this.mc.config.getNumber("Fluid.ContainerRuntime.StagingModeAutoFlushThreshold") ??
1892
+ runtimeOptions.stagingModeAutoFlushThreshold ??
1893
+ defaultStagingModeAutoFlushThreshold;
1843
1894
  this.batchIdTrackingEnabled =
1844
1895
  this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") ??
1845
1896
  this.mc.config.getBoolean("Fluid.ContainerRuntime.enableBatchIdTracking") ??
@@ -1998,10 +2049,6 @@ export class ContainerRuntime
1998
2049
 
1999
2050
  const legacySendBatchFn = makeLegacySendBatchFn(submitFn, this.innerDeltaManager);
2000
2051
 
2001
- this.skipSafetyFlushDuringProcessStack =
2002
- // Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
2003
- this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
2004
-
2005
2052
  this.outbox = new Outbox({
2006
2053
  shouldSend: () => this.shouldSendOps(),
2007
2054
  pendingStateManager: this.pendingStateManager,
@@ -2012,8 +2059,6 @@ export class ContainerRuntime
2012
2059
  config: {
2013
2060
  compressionOptions,
2014
2061
  maxBatchSizeInBytes: runtimeOptions.maxBatchSizeInBytes,
2015
- // If we disable flush before process, we must be ready to flush partial batches
2016
- flushPartialBatches: this.skipSafetyFlushDuringProcessStack,
2017
2062
  },
2018
2063
  logger: this.mc.logger,
2019
2064
  groupingManager: opGroupingManager,
@@ -2024,6 +2069,24 @@ export class ContainerRuntime
2024
2069
  }),
2025
2070
  reSubmit: this.reSubmit.bind(this),
2026
2071
  opReentrancy: () => this.dataModelChangeRunner.running,
2072
+ generateIdAllocationOp: (): LocalBatchMessage | undefined => {
2073
+ if (this._idCompressor === undefined) {
2074
+ return undefined;
2075
+ }
2076
+ const idRange = this._idCompressor.takeNextCreationRange();
2077
+ if (idRange.ids === undefined) {
2078
+ return undefined;
2079
+ }
2080
+ const idAllocationMessage: ContainerRuntimeIdAllocationMessage = {
2081
+ type: ContainerMessageType.IdAllocation,
2082
+ contents: idRange,
2083
+ };
2084
+ return {
2085
+ runtimeOp: idAllocationMessage,
2086
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
2087
+ staged: false,
2088
+ };
2089
+ },
2027
2090
  });
2028
2091
 
2029
2092
  this._quorum = quorum;
@@ -2070,14 +2133,12 @@ export class ContainerRuntime
2070
2133
  this.lastEmittedDirty = this.computeCurrentDirtyState();
2071
2134
  context.updateDirtyContainerState(this.lastEmittedDirty);
2072
2135
 
2073
- if (!this.skipSafetyFlushDuringProcessStack) {
2074
- // Reference Sequence Number may have just changed, and it must be consistent across a batch,
2075
- // so we should flush now to clear the way for the next ops.
2076
- // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
2077
- // But we need this coverage for old loaders that don't call ContainerRuntime.process for non-runtime messages.
2078
- // (We have to call flush _before_ processing a runtime op, but after is ok for non-runtime op)
2079
- this.deltaManager.on("op", () => this.flush());
2080
- }
2136
+ // Reference Sequence Number may have just changed, and it must be consistent across a batch,
2137
+ // so we should flush now to clear the way for the next ops.
2138
+ // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
2139
+ // But we need this coverage for old loaders that don't call ContainerRuntime.process for non-runtime messages.
2140
+ // (We have to call flush _before_ processing a runtime op, but after is ok for non-runtime op)
2141
+ this.deltaManager.on("op", () => this.flush());
2081
2142
 
2082
2143
  // logging hardware telemetry
2083
2144
  this.baseLogger.send({
@@ -2093,7 +2154,9 @@ export class ContainerRuntime
2093
2154
  summaryNumber: loadSummaryNumber,
2094
2155
  summaryFormatVersion: metadata?.summaryFormatVersion,
2095
2156
  disableIsolatedChannels: metadata?.disableIsolatedChannels,
2157
+ // This is useful even for interactive clients since they track unreferenced nodes and log errors.
2096
2158
  gcVersion: metadata?.gcFeature,
2159
+ gcConfigs: this.garbageCollector.serializedConfigs,
2097
2160
  options: JSON.stringify(runtimeOptions),
2098
2161
  idCompressorModeMetadata: metadata?.documentSchema?.runtime?.idCompressorMode,
2099
2162
  idCompressorMode: this.sessionSchema.idCompressorMode,
@@ -2101,7 +2164,6 @@ export class ContainerRuntime
2101
2164
  featureGates: JSON.stringify({
2102
2165
  ...featureGatesForTelemetry,
2103
2166
  closeSummarizerDelayOverride,
2104
- disableFlushBeforeProcess: this.skipSafetyFlushDuringProcessStack,
2105
2167
  }),
2106
2168
  telemetryDocumentId: this.telemetryDocumentId,
2107
2169
  groupedBatchingEnabled: this.groupedBatchingEnabled,
@@ -2723,12 +2785,10 @@ export class ContainerRuntime
2723
2785
  this.emitDirtyDocumentEvent = false;
2724
2786
 
2725
2787
  try {
2726
- // Any ID Allocation ops that failed to submit after the pending state was queued need to have
2727
- // the corresponding ranges resubmitted (note this call replaces the typical resubmit flow).
2728
- // Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
2729
- // before staging mode so we can simply say staged: false.
2730
- this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
2731
- this.scheduleFlush();
2788
+ // Any ID Allocation ops that failed to submit need to have their ranges included
2789
+ // in the next allocation op. Reset the compressor's unfinalized range cursor so that the next
2790
+ // call to takeNextCreationRange (during replay) will include those unfinalized ranges.
2791
+ this._idCompressor?.resetUnfinalizedCreationRange();
2732
2792
 
2733
2793
  // replay the ops
2734
2794
  this.pendingStateManager.replayPendingStates();
@@ -3023,10 +3083,8 @@ export class ContainerRuntime
3023
3083
 
3024
3084
  this.verifyNotClosed();
3025
3085
 
3026
- if (!this.skipSafetyFlushDuringProcessStack) {
3027
- // Reference Sequence Number may be about to change, and it must be consistent across a batch, so flush now
3028
- this.flush();
3029
- }
3086
+ // Reference Sequence Number may be about to change, and it must be consistent across a batch, so flush now
3087
+ this.flush();
3030
3088
 
3031
3089
  this.ensureNoDataModelChanges(() => {
3032
3090
  this.processInboundMessageOrBatch(messageCopy, local);
@@ -3630,20 +3688,36 @@ export class ContainerRuntime
3630
3688
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
3631
3689
  this.flush();
3632
3690
 
3633
- const exitStagingMode = (discardOrCommit: () => void): void => {
3691
+ const exitStagingMode = (
3692
+ discardOrCommit: () => IPendingMessage["batchInfo"][],
3693
+ exitMethod: "commit" | "discard",
3694
+ ): void => {
3634
3695
  try {
3635
- // Final flush of any last staged changes
3636
- // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
3637
- this.outbox.flush();
3638
-
3639
- this.stageControls = undefined;
3696
+ PerformanceEvent.timedExec(
3697
+ this.mc.logger,
3698
+ {
3699
+ eventName: `ExitStagingMode_${exitMethod}`,
3700
+ },
3701
+ (event) => {
3702
+ // Final flush of any last staged changes
3703
+ // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
3704
+ this.outbox.flush();
3640
3705
 
3641
- // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
3642
- // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
3643
- this.submitIdAllocationOpIfNeeded({ staged: false });
3644
- discardOrCommit();
3706
+ this.stageControls = undefined;
3645
3707
 
3646
- this.channelCollection.notifyStagingMode(false);
3708
+ const batchInfos = discardOrCommit();
3709
+ event.reportProgress({
3710
+ details: {
3711
+ autoFlushThreshold: this.stagingModeAutoFlushThreshold,
3712
+ batches: batchInfos.length,
3713
+ batchesAtOrOverThreshold: batchInfos.filter(
3714
+ (b) => b.length >= this.stagingModeAutoFlushThreshold,
3715
+ ).length,
3716
+ },
3717
+ });
3718
+ this.channelCollection.notifyStagingMode(false);
3719
+ },
3720
+ );
3647
3721
  } catch (error) {
3648
3722
  const normalizedError = normalizeError(error);
3649
3723
  this.closeFn(normalizedError);
@@ -3655,21 +3729,24 @@ export class ContainerRuntime
3655
3729
  discardChanges: () =>
3656
3730
  exitStagingMode(() => {
3657
3731
  // Pop all staged batches from the PSM and roll them back in LIFO order
3658
- this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
3659
- this.rollbackStagedChange(runtimeOp, localOpMetadata);
3660
- });
3732
+ const batchInfos = this.pendingStateManager.popStagedBatches(
3733
+ ({ runtimeOp, localOpMetadata }) => {
3734
+ this.rollbackStagedChange(runtimeOp, localOpMetadata);
3735
+ },
3736
+ );
3661
3737
  this.updateDocumentDirtyState();
3662
- }),
3738
+ return batchInfos;
3739
+ }, "discard"),
3663
3740
  commitChanges: (options) => {
3664
3741
  const { squash } = { ...defaultStagingCommitOptions, ...options };
3665
3742
  exitStagingMode(() => {
3666
3743
  // Replay all staged batches in typical FIFO order.
3667
3744
  // We'll be out of staging mode so they'll be sent to the service finally.
3668
- this.pendingStateManager.replayPendingStates({
3745
+ return this.pendingStateManager.replayPendingStates({
3669
3746
  committingStagedBatches: true,
3670
3747
  squash,
3671
3748
  });
3672
- });
3749
+ }, "commit");
3673
3750
  },
3674
3751
  };
3675
3752
 
@@ -4210,7 +4287,6 @@ export class ContainerRuntime
4210
4287
  outboxLength: this.outbox.messageCount,
4211
4288
  mainBatchLength: this.outbox.mainBatchMessageCount,
4212
4289
  blobAttachBatchLength: this.outbox.blobAttachBatchMessageCount,
4213
- idAllocationBatchLength: this.outbox.idAllocationBatchMessageCount,
4214
4290
  },
4215
4291
  );
4216
4292
  }
@@ -4543,7 +4619,8 @@ export class ContainerRuntime
4543
4619
 
4544
4620
  /**
4545
4621
  * This helper is called during summarization. If the container is dirty, it will return a failed summarize result
4546
- * (IBaseSummarizeResult) unless this is the final summarize attempt and SkipFailingIncorrectSummary option is set.
4622
+ * (IBaseSummarizeResult) unless this is the final summarize attempt, in which case the summary is allowed to
4623
+ * proceed to make progress in documents where there are consistently pending ops in the summarizer.
4547
4624
  * @param logger - The logger to be used for sending telemetry.
4548
4625
  * @param referenceSequenceNumber - The reference sequence number of the summary attempt.
4549
4626
  * @param minimumSequenceNumber - The minimum sequence number of the summary attempt.
@@ -4562,13 +4639,9 @@ export class ContainerRuntime
4562
4639
  return;
4563
4640
  }
4564
4641
 
4565
- // If "SkipFailingIncorrectSummary" option is true, don't fail the summary in the last attempt.
4566
- // This is a fallback to make progress in documents where there are consistently pending ops in
4567
- // the summarizer.
4568
- if (
4569
- finalAttempt &&
4570
- this.mc.config.getBoolean("Fluid.Summarizer.SkipFailingIncorrectSummary") === true
4571
- ) {
4642
+ // Don't fail the summary in the last attempt. This is a fallback to make progress in
4643
+ // documents where there are consistently pending ops in the summarizer.
4644
+ if (finalAttempt) {
4572
4645
  const error = DataProcessingError.create(
4573
4646
  "Pending ops during summarization",
4574
4647
  "submitSummary",
@@ -4577,7 +4650,7 @@ export class ContainerRuntime
4577
4650
  );
4578
4651
  logger.sendErrorEvent(
4579
4652
  {
4580
- eventName: "SkipFailingIncorrectSummary",
4653
+ eventName: "PendingOpsDuringSummaryFinalAttempt",
4581
4654
  referenceSequenceNumber,
4582
4655
  minimumSequenceNumber,
4583
4656
  beforeGenerate: beforeSummaryGeneration,
@@ -4671,33 +4744,6 @@ export class ContainerRuntime
4671
4744
  return this.blobManager.lookupTemporaryBlobStorageId(localId);
4672
4745
  }
4673
4746
 
4674
- private submitIdAllocationOpIfNeeded({
4675
- resubmitOutstandingRanges = false,
4676
- staged,
4677
- }: {
4678
- resubmitOutstandingRanges?: boolean;
4679
- staged: boolean;
4680
- }): void {
4681
- if (this._idCompressor) {
4682
- const idRange = resubmitOutstandingRanges
4683
- ? this._idCompressor.takeUnfinalizedCreationRange()
4684
- : this._idCompressor.takeNextCreationRange();
4685
- // Don't include the idRange if there weren't any Ids allocated
4686
- if (idRange.ids !== undefined) {
4687
- const idAllocationMessage: ContainerRuntimeIdAllocationMessage = {
4688
- type: ContainerMessageType.IdAllocation,
4689
- contents: idRange,
4690
- };
4691
- const idAllocationBatchMessage: LocalBatchMessage = {
4692
- runtimeOp: idAllocationMessage,
4693
- referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
4694
- staged,
4695
- };
4696
- this.outbox.submitIdAllocation(idAllocationBatchMessage);
4697
- }
4698
- }
4699
- }
4700
-
4701
4747
  private submit(
4702
4748
  containerRuntimeMessage: LocalContainerRuntimeMessage,
4703
4749
  localOpMetadata: unknown = undefined,
@@ -4741,11 +4787,6 @@ export class ContainerRuntime
4741
4787
  0xbba /* Unexpected message type submitted in Staging Mode */,
4742
4788
  );
4743
4789
 
4744
- // Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
4745
- if (!staged) {
4746
- this.submitIdAllocationOpIfNeeded({ staged: false });
4747
- }
4748
-
4749
4790
  // Allow document schema controller to send a message if it needs to propose change in document schema.
4750
4791
  // If it needs to send a message, it will call provided callback with payload of such message and rely
4751
4792
  // on this callback to do actual sending.
@@ -4800,6 +4841,20 @@ export class ContainerRuntime
4800
4841
  }
4801
4842
 
4802
4843
  private scheduleFlush(): void {
4844
+ // During staging mode, suppress automatic flush scheduling until the main batch
4845
+ // reaches or exceeds the threshold.
4846
+ // Incoming ops still break the batch via direct this.flush() calls elsewhere
4847
+ // (deltaManager "op" handler, process(), connection changes, getPendingLocalState,
4848
+ // exitStagingMode). Those all bypass scheduleFlush(), so they're unaffected by this check.
4849
+ // Additionally, outbox.outboxSequenceNumberCoherencyCheck() (called on every submit) detects
4850
+ // sequence number changes and throws if unexpected changes are detected.
4851
+ if (
4852
+ this.inStagingMode &&
4853
+ this.outbox.mainBatchMessageCount < this.stagingModeAutoFlushThreshold
4854
+ ) {
4855
+ return;
4856
+ }
4857
+
4803
4858
  if (this.flushScheduled) {
4804
4859
  return;
4805
4860
  }
@@ -5195,7 +5250,9 @@ export class ContainerRuntime
5195
5250
  eventName: "getPendingLocalState",
5196
5251
  },
5197
5252
  (event) => {
5198
- const pending = this.pendingStateManager.getLocalState(props?.snapshotSequenceNumber);
5253
+ const { pending } = this.pendingStateManager.getLocalState(
5254
+ props?.snapshotSequenceNumber,
5255
+ );
5199
5256
  const sessionExpiryTimerStarted =
5200
5257
  props?.sessionExpiryTimerStarted ?? this.garbageCollector.sessionExpiryTimerStarted;
5201
5258
 
@@ -100,6 +100,10 @@ export class GarbageCollector implements IGarbageCollector {
100
100
 
101
101
  private readonly configs: IGarbageCollectorConfigs;
102
102
 
103
+ public get serializedConfigs(): string {
104
+ return JSON.stringify(this.configs);
105
+ }
106
+
103
107
  public get shouldRunGC(): boolean {
104
108
  return this.configs.gcAllowed;
105
109
  }
@@ -334,15 +338,6 @@ export class GarbageCollector implements IGarbageCollector {
334
338
 
335
339
  return { gcData: { gcNodes }, usedRoutes };
336
340
  });
337
-
338
- // Log all the GC options and the state determined by the garbage collector.
339
- // This is useful even for interactive clients since they track unreferenced nodes and log errors.
340
- this.mc.logger.sendTelemetryEvent({
341
- eventName: "GarbageCollectorLoaded",
342
- gcConfigs: JSON.stringify(this.configs),
343
- gcOptions: JSON.stringify(createParams.gcOptions),
344
- ...createParams.createContainerMetadata,
345
- });
346
341
  }
347
342
 
348
343
  /**
@@ -373,6 +373,10 @@ export interface IGarbageCollectionRuntime {
373
373
  * Defines the contract for the garbage collector.
374
374
  */
375
375
  export interface IGarbageCollector {
376
+ /**
377
+ * The GC configurations serialized as a JSON string for telemetry.
378
+ */
379
+ readonly serializedConfigs: string;
376
380
  /**
377
381
  * Tells the time at which session expiry timer started in a previous container.
378
382
  * This is only set when loading from a stashed container and will be equal to the
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export {
10
10
  type IContainerRuntimeOptions,
11
11
  type IContainerRuntimeOptionsInternal,
12
12
  loadContainerRuntime,
13
+ loadContainerRuntimeAlpha,
13
14
  type LoadContainerRuntimeParams,
14
15
  agentSchedulerId,
15
16
  ContainerRuntime,
@@ -20,17 +20,8 @@ import { serializeOp } from "./opSerialization.js";
20
20
  import type { BatchStartInfo } from "./remoteMessageProcessor.js";
21
21
 
22
22
  export interface IBatchManagerOptions {
23
+ readonly disableGroupedBatching: boolean;
23
24
  readonly compressionOptions?: ICompressionRuntimeOptions;
24
-
25
- /**
26
- * If true, the outbox is allowed to rebase the batch during flushing.
27
- */
28
- readonly canRebase: boolean;
29
-
30
- /**
31
- * If true, don't compare batchID of incoming batches to this. e.g. ID Allocation Batch IDs should be ignored
32
- */
33
- readonly ignoreBatchId?: boolean;
34
25
  }
35
26
 
36
27
  export interface BatchSequenceNumbers {
@@ -127,8 +118,9 @@ export class BatchManager {
127
118
 
128
119
  /**
129
120
  * Gets the pending batch and clears state for the next batch.
121
+ * The caller is responsible for calling {@link addBatchMetadata} after any modifications (e.g. prepending messages).
130
122
  */
131
- public popBatch(batchId?: BatchId): LocalBatch {
123
+ public popBatch(): LocalBatch {
132
124
  assert(this.pendingBatch[0] !== undefined, 0xb8a /* expected non-empty batch */);
133
125
  const batch: LocalBatch = {
134
126
  messages: this.pendingBatch,
@@ -141,7 +133,7 @@ export class BatchManager {
141
133
  this.clientSequenceNumber = undefined;
142
134
  this.hasReentrantOps = false;
143
135
 
144
- return addBatchMetadata(batch, batchId);
136
+ return batch;
145
137
  }
146
138
 
147
139
  /**
@@ -162,7 +154,8 @@ export class BatchManager {
162
154
  throw new LoggingError("Ops generated during rollback", {
163
155
  count,
164
156
  ...tagData(TelemetryDataTag.UserData, {
165
- ops: serializeOp(this.pendingBatch.slice(startPoint).map((b) => b.runtimeOp)),
157
+ ops: serializeOp(this.pendingBatch.slice(startPoint).map((b) => b.runtimeOp))
158
+ .content,
166
159
  }),
167
160
  });
168
161
  }
@@ -45,6 +45,7 @@ export {
45
45
  } from "./remoteMessageProcessor.js";
46
46
  export {
47
47
  type EmptyGroupedBatch,
48
+ largeBatchThreshold,
48
49
  OpGroupingManager,
49
50
  type OpGroupingManagerConfig,
50
51
  isGroupedBatch,