@fluidframework/container-runtime 2.0.0-internal.1.1.0 → 2.0.0-internal.1.2.0.93071

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 (70) hide show
  1. package/dist/batchManager.d.ts +32 -0
  2. package/dist/batchManager.d.ts.map +1 -0
  3. package/dist/batchManager.js +71 -0
  4. package/dist/batchManager.js.map +1 -0
  5. package/dist/containerRuntime.d.ts +44 -17
  6. package/dist/containerRuntime.d.ts.map +1 -1
  7. package/dist/containerRuntime.js +197 -95
  8. package/dist/containerRuntime.js.map +1 -1
  9. package/dist/dataStoreContext.d.ts +4 -4
  10. package/dist/dataStoreContext.js +5 -5
  11. package/dist/dataStoreContext.js.map +1 -1
  12. package/dist/packageVersion.d.ts +1 -1
  13. package/dist/packageVersion.d.ts.map +1 -1
  14. package/dist/packageVersion.js +1 -1
  15. package/dist/packageVersion.js.map +1 -1
  16. package/dist/pendingStateManager.d.ts +0 -11
  17. package/dist/pendingStateManager.d.ts.map +1 -1
  18. package/dist/pendingStateManager.js +6 -43
  19. package/dist/pendingStateManager.js.map +1 -1
  20. package/dist/runningSummarizer.js +1 -1
  21. package/dist/runningSummarizer.js.map +1 -1
  22. package/dist/scheduleManager.js +1 -1
  23. package/dist/scheduleManager.js.map +1 -1
  24. package/dist/summarizerTypes.d.ts +3 -3
  25. package/dist/summarizerTypes.js +1 -1
  26. package/dist/summarizerTypes.js.map +1 -1
  27. package/dist/summaryCollection.d.ts +1 -0
  28. package/dist/summaryCollection.d.ts.map +1 -1
  29. package/dist/summaryCollection.js +32 -13
  30. package/dist/summaryCollection.js.map +1 -1
  31. package/lib/batchManager.d.ts +32 -0
  32. package/lib/batchManager.d.ts.map +1 -0
  33. package/lib/batchManager.js +67 -0
  34. package/lib/batchManager.js.map +1 -0
  35. package/lib/containerRuntime.d.ts +44 -17
  36. package/lib/containerRuntime.d.ts.map +1 -1
  37. package/lib/containerRuntime.js +200 -98
  38. package/lib/containerRuntime.js.map +1 -1
  39. package/lib/dataStoreContext.d.ts +4 -4
  40. package/lib/dataStoreContext.js +5 -5
  41. package/lib/dataStoreContext.js.map +1 -1
  42. package/lib/packageVersion.d.ts +1 -1
  43. package/lib/packageVersion.d.ts.map +1 -1
  44. package/lib/packageVersion.js +1 -1
  45. package/lib/packageVersion.js.map +1 -1
  46. package/lib/pendingStateManager.d.ts +0 -11
  47. package/lib/pendingStateManager.d.ts.map +1 -1
  48. package/lib/pendingStateManager.js +6 -43
  49. package/lib/pendingStateManager.js.map +1 -1
  50. package/lib/runningSummarizer.js +1 -1
  51. package/lib/runningSummarizer.js.map +1 -1
  52. package/lib/scheduleManager.js +2 -2
  53. package/lib/scheduleManager.js.map +1 -1
  54. package/lib/summarizerTypes.d.ts +3 -3
  55. package/lib/summarizerTypes.js +1 -1
  56. package/lib/summarizerTypes.js.map +1 -1
  57. package/lib/summaryCollection.d.ts +1 -0
  58. package/lib/summaryCollection.d.ts.map +1 -1
  59. package/lib/summaryCollection.js +32 -13
  60. package/lib/summaryCollection.js.map +1 -1
  61. package/package.json +17 -17
  62. package/src/batchManager.ts +88 -0
  63. package/src/containerRuntime.ts +273 -156
  64. package/src/dataStoreContext.ts +7 -7
  65. package/src/packageVersion.ts +1 -1
  66. package/src/pendingStateManager.ts +6 -56
  67. package/src/runningSummarizer.ts +1 -1
  68. package/src/scheduleManager.ts +2 -2
  69. package/src/summarizerTypes.ts +3 -3
  70. package/src/summaryCollection.ts +33 -16
@@ -23,6 +23,7 @@ import {
23
23
  ILoaderOptions,
24
24
  LoaderHeader,
25
25
  ISnapshotTreeWithBlobContents,
26
+ IBatchMessage,
26
27
  } from "@fluidframework/container-definitions";
27
28
  import {
28
29
  IContainerRuntime,
@@ -41,6 +42,7 @@ import {
41
42
  TaggedLoggerAdapter,
42
43
  MonitoringContext,
43
44
  loggerToMonitoringContext,
45
+ wrapError,
44
46
  } from "@fluidframework/telemetry-utils";
45
47
  import {
46
48
  DriverHeader,
@@ -48,7 +50,7 @@ import {
48
50
  IDocumentStorageService,
49
51
  ISummaryContext,
50
52
  } from "@fluidframework/driver-definitions";
51
- import { readAndParse, isUnpackedRuntimeMessage } from "@fluidframework/driver-utils";
53
+ import { readAndParse } from "@fluidframework/driver-utils";
52
54
  import {
53
55
  DataCorruptionError,
54
56
  DataProcessingError,
@@ -113,7 +115,11 @@ import {
113
115
  ReportOpPerfTelemetry,
114
116
  IPerfSignalReport,
115
117
  } from "./connectionTelemetry";
116
- import { IPendingLocalState, PendingStateManager } from "./pendingStateManager";
118
+ import {
119
+ IPendingLocalState,
120
+ PendingStateManager,
121
+ } from "./pendingStateManager";
122
+ import { BatchManager, BatchMessage } from "./batchManager";
117
123
  import { pkgVersion } from "./packageVersion";
118
124
  import { BlobManager, IBlobManagerLoadInfo, IPendingBlobs } from "./blobManager";
119
125
  import { DataStores, getSummaryForDatastores } from "./dataStores";
@@ -195,6 +201,7 @@ export interface ContainerRuntimeMessage {
195
201
  contents: any;
196
202
  type: ContainerMessageType;
197
203
  }
204
+
198
205
  export interface ISummaryBaseConfiguration {
199
206
  /**
200
207
  * Delay before first attempt to spawn summarizing container.
@@ -223,7 +230,8 @@ export interface ISummaryBaseConfiguration {
223
230
  export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfiguration {
224
231
  state: "enabled";
225
232
  /**
226
- * @deprecated - please move all implementation to minIdleTime and maxIdleTime
233
+ * @deprecated Please move all implementations to {@link ISummaryConfigurationHeuristics.minIdleTime} and
234
+ * {@link ISummaryConfigurationHeuristics.maxIdleTime} instead.
227
235
  */
228
236
  idleTime: number;
229
237
  /**
@@ -368,33 +376,45 @@ export interface ISummaryRuntimeOptions {
368
376
  summaryConfigOverrides?: ISummaryConfiguration;
369
377
 
370
378
  /**
371
- * @deprecated - use `summaryConfigOverrides.initialSummarizerDelayMs` instead.
372
- * Delay before first attempt to spawn summarizing container.
373
- */
379
+ * Delay before first attempt to spawn summarizing container.
380
+ *
381
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
382
+ * {@link ISummaryBaseConfiguration.initialSummarizerDelayMs} instead.
383
+ */
374
384
  initialSummarizerDelayMs?: number;
375
385
 
376
386
  /**
377
- * @deprecated - use `summaryConfigOverrides.disableSummaries` instead.
378
387
  * Flag that disables summaries if it is set to true.
388
+ *
389
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
390
+ * {@link ISummaryConfigurationDisableSummarizer.state} instead.
379
391
  */
380
392
  disableSummaries?: boolean;
381
393
 
382
394
  /**
383
- * @deprecated - use `summaryConfigOverrides.maxOpsSinceLastSummary` instead.
384
- * Defaults to 7000 ops
395
+ * @defaultValue 7000 operations (ops)
396
+ *
397
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
398
+ * {@link ISummaryBaseConfiguration.maxOpsSinceLastSummary} instead.
385
399
  */
386
400
  maxOpsSinceLastSummary?: number;
387
401
 
388
402
  /**
389
- * @deprecated - use `summaryConfigOverrides.summarizerClientElection` instead.
390
- * Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
391
- * This defaults to false (disabled) and must be explicitly set to true to enable.
392
- */
403
+ * Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
404
+ *
405
+ * @defaultValue `false` (disabled) and must be explicitly set to true to enable.
406
+ *
407
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
408
+ * {@link ISummaryBaseConfiguration.summarizerClientElection} instead.
409
+ */
393
410
  summarizerClientElection?: boolean;
394
411
 
395
412
  /**
396
- * @deprecated - use `summaryConfigOverrides.state = "DisableHeuristics"` instead.
397
- * Options that control the running summarizer behavior. */
413
+ * Options that control the running summarizer behavior.
414
+ *
415
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
416
+ * `{@link ISummaryConfiguration.state} = "DisableHeuristics"` instead.
417
+ * */
398
418
  summarizerOptions?: Readonly<Partial<ISummarizerOptions>>;
399
419
  }
400
420
 
@@ -493,13 +513,11 @@ interface IPendingRuntimeState {
493
513
 
494
514
  const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
495
515
 
496
- // By default, we should reject any op larger than 768KB,
497
- // in order to account for some extra overhead from serialization
498
- // to not reach the 1MB limits in socket.io and Kafka.
499
- const defaultMaxOpSizeInBytes = 768000;
500
-
501
516
  const defaultFlushMode = FlushMode.TurnBased;
502
517
 
518
+ /**
519
+ * @deprecated - use ContainerRuntimeMessage instead
520
+ */
503
521
  export enum RuntimeMessage {
504
522
  FluidDataStoreOp = "component",
505
523
  Attach = "attach",
@@ -510,6 +528,9 @@ export enum RuntimeMessage {
510
528
  Operation = "op",
511
529
  }
512
530
 
531
+ /**
532
+ * @deprecated - please use version in driver-utils
533
+ */
513
534
  export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
514
535
  if ((Object.values(RuntimeMessage) as string[]).includes(message.type)) {
515
536
  return true;
@@ -517,6 +538,12 @@ export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
517
538
  return false;
518
539
  }
519
540
 
541
+ /**
542
+ * Unpacks runtime messages
543
+ * @internal - no promises RE back-compat - this is internal API.
544
+ * @param message - message (as it observed in storage / service)
545
+ * @returns unpacked runtime message
546
+ */
520
547
  export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
521
548
  if (message.type === MessageType.Operation) {
522
549
  // legacy op format?
@@ -529,13 +556,14 @@ export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
529
556
  message.type = innerContents.type;
530
557
  message.contents = innerContents.contents;
531
558
  }
532
- assert(isUnpackedRuntimeMessage(message), 0x122 /* "Message to unpack is not proper runtime message" */);
559
+ return true;
533
560
  } else {
534
561
  // Legacy format, but it's already "unpacked",
535
562
  // i.e. message.type is actually ContainerMessageType.
563
+ // Or it's non-runtime message.
536
564
  // Nothing to do in such case.
565
+ return false;
537
566
  }
538
- return message;
539
567
  }
540
568
 
541
569
  /**
@@ -774,7 +802,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
774
802
 
775
803
  private _orderSequentiallyCalls: number = 0;
776
804
  private _flushMode: FlushMode;
777
- private needsFlush = false;
778
805
  private flushTrigger = false;
779
806
 
780
807
  private _connected: boolean;
@@ -823,6 +850,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
823
850
  private readonly scheduleManager: ScheduleManager;
824
851
  private readonly blobManager: BlobManager;
825
852
  private readonly pendingStateManager: PendingStateManager;
853
+ private readonly batchManager = new BatchManager();
826
854
  private readonly garbageCollector: IGarbageCollector;
827
855
 
828
856
  // Local copy of incomplete received chunks.
@@ -1058,7 +1086,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1058
1086
  flush: this.flush.bind(this),
1059
1087
  flushMode: () => this.flushMode,
1060
1088
  reSubmit: this.reSubmit.bind(this),
1061
- rollback: this.rollback.bind(this),
1062
1089
  setFlushMode: (mode) => this.setFlushMode(mode),
1063
1090
  },
1064
1091
  this._flushMode,
@@ -1427,7 +1454,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1427
1454
  return true;
1428
1455
  }
1429
1456
 
1430
- if (!this.pendingStateManager.hasPendingMessages()) {
1457
+ if (!this.hasPendingMessages()) {
1431
1458
  // If there are no pending messages, we can always reconnect
1432
1459
  this.resetReconnectCount();
1433
1460
  return true;
@@ -1546,6 +1573,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1546
1573
  this._perfSignalData.signalsLost = 0;
1547
1574
  this._perfSignalData.signalTimestamp = 0;
1548
1575
  this._perfSignalData.trackingSignalSequenceNumber = undefined;
1576
+ } else {
1577
+ assert(this.attachState === AttachState.Attached,
1578
+ "Connection is possible only if container exists in storage");
1549
1579
  }
1550
1580
 
1551
1581
  // Fail while disconnected
@@ -1554,9 +1584,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1554
1584
 
1555
1585
  if (!this.shouldContinueReconnecting()) {
1556
1586
  this.closeFn(
1557
- // pre-0.58 error message: MaxReconnectsWithNoProgress
1558
1587
  DataProcessingError.create(
1559
- "Runtime detected too many reconnects with no progress syncing local ops",
1588
+ // eslint-disable-next-line max-len
1589
+ "Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)",
1560
1590
  "setConnectionState",
1561
1591
  undefined,
1562
1592
  {
@@ -1580,49 +1610,50 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1580
1610
  public process(messageArg: ISequencedDocumentMessage, local: boolean) {
1581
1611
  this.verifyNotClosed();
1582
1612
 
1583
- // If it's not message for runtime, bail out right away.
1584
- if (!isUnpackedRuntimeMessage(messageArg)) {
1585
- return;
1586
- }
1587
-
1588
- if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
1589
- this.savedOps.push(messageArg);
1590
- }
1591
-
1592
1613
  // Do shallow copy of message, as methods below will modify it.
1593
1614
  // There might be multiple container instances receiving same message
1594
1615
  // We do not need to make deep copy, as each layer will just replace message.content itself,
1595
1616
  // but would not modify contents details
1596
1617
  let message = { ...messageArg };
1597
1618
 
1619
+ // back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
1620
+ // System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
1621
+ // Old ops may contain empty string (I assume noops).
1622
+ if (typeof message.contents === "string" && message.contents !== "") {
1623
+ message.contents = JSON.parse(message.contents);
1624
+ }
1625
+
1626
+ // Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
1627
+ // This format was not shipped to production workflows.
1628
+ const runtimeMessage = unpackRuntimeMessage(message);
1629
+
1630
+ if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
1631
+ this.savedOps.push(messageArg);
1632
+ }
1633
+
1598
1634
  // Surround the actual processing of the operation with messages to the schedule manager indicating
1599
1635
  // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
1600
1636
  // messages once a batch has been fully processed.
1601
1637
  this.scheduleManager.beforeOpProcessing(message);
1602
1638
 
1603
1639
  try {
1604
- message = unpackRuntimeMessage(message);
1605
-
1606
1640
  // Chunk processing must come first given that we will transform the message to the unchunked version
1607
1641
  // once all pieces are available
1608
1642
  message = this.processRemoteChunkedMessage(message);
1609
1643
 
1610
1644
  let localOpMetadata: unknown;
1611
- if (local) {
1612
- // Call the PendingStateManager to process local messages.
1613
- // Do not process local chunked ops until all pieces are available.
1614
- if (message.type !== ContainerMessageType.ChunkedOp) {
1615
- localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1616
- }
1645
+ if (local && runtimeMessage) {
1646
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1617
1647
  }
1618
1648
 
1619
1649
  // If there are no more pending messages after processing a local message,
1620
1650
  // the document is no longer dirty.
1621
- if (!this.pendingStateManager.hasPendingMessages()) {
1651
+ if (!this.hasPendingMessages()) {
1622
1652
  this.updateDocumentDirtyState(false);
1623
1653
  }
1624
1654
 
1625
- switch (message.type) {
1655
+ const type = message.type as ContainerMessageType;
1656
+ switch (type) {
1626
1657
  case ContainerMessageType.Attach:
1627
1658
  this.dataStores.processAttachMessage(message, local);
1628
1659
  break;
@@ -1635,10 +1666,18 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1635
1666
  case ContainerMessageType.BlobAttach:
1636
1667
  this.blobManager.processBlobAttachOp(message, local);
1637
1668
  break;
1669
+ case ContainerMessageType.ChunkedOp:
1670
+ case ContainerMessageType.Rejoin:
1671
+ break;
1638
1672
  default:
1673
+ assert(!runtimeMessage, "Runtime message of unknown type");
1674
+ }
1675
+
1676
+ // For back-compat, notify only about runtime messages for now.
1677
+ if (runtimeMessage) {
1678
+ this.emit("op", message, runtimeMessage);
1639
1679
  }
1640
1680
 
1641
- this.emit("op", message);
1642
1681
  this.scheduleManager.afterOpProcessing(undefined, message);
1643
1682
 
1644
1683
  if (local) {
@@ -1752,30 +1791,76 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1752
1791
  assert(this._orderSequentiallyCalls === 0,
1753
1792
  0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
1754
1793
 
1755
- if (!this.deltaSender) {
1756
- return;
1757
- }
1794
+ const batch = this.batchManager.popBatch();
1795
+ this.flushBatch(batch);
1758
1796
 
1759
- // Let the PendingStateManager know that there was an attempt to flush messages.
1760
- // Note that this should happen before the `this.needsFlush` check below because in the scenario where we are
1761
- // not connected, `this.needsFlush` will be false but the PendingStateManager might have pending messages and
1762
- // hence needs to track this.
1763
- this.pendingStateManager.onFlush();
1797
+ assert(this.batchManager.empty, "reentrancy");
1798
+ }
1764
1799
 
1765
- // If flush has already been called then exit early
1766
- if (!this.needsFlush) {
1767
- return;
1800
+ protected flushBatch(batch: BatchMessage[]): void {
1801
+ const length = batch.length;
1802
+
1803
+ if (length > 1) {
1804
+ batch[0].metadata = { ...batch[0].metadata, batch: true };
1805
+ batch[length - 1].metadata = { ...batch[length - 1].metadata, batch: false };
1806
+
1807
+ // This assert fires for the following reason (there might be more cases like that):
1808
+ // AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
1809
+ // i.e. in the middle of op processing!
1810
+ // Sending ops while processing ops is not good idea - it's not defined when
1811
+ // referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
1812
+ // If we send ops in response to processing multiple ops, then we for sure hit this assert!
1813
+ // Tracked via ADO #1834
1814
+ // assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
1815
+ // "Batch should be generated synchronously, without processing ops in the middle!");
1768
1816
  }
1769
1817
 
1770
- this.needsFlush = false;
1818
+ let clientSequenceNumber: number = -1;
1771
1819
 
1772
1820
  // Did we disconnect in the middle of turn-based batch?
1773
1821
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
1774
- if (!this.canSendOps()) {
1775
- return;
1822
+ if (this.canSendOps()) {
1823
+ if (this.context.submitBatchFn !== undefined) {
1824
+ const batchToSend: IBatchMessage[] = [];
1825
+ for (const message of batch) {
1826
+ batchToSend.push({ contents: message.contents, metadata: message.metadata });
1827
+ }
1828
+ // returns clientSequenceNumber of last message in a batch
1829
+ clientSequenceNumber = this.context.submitBatchFn(batchToSend);
1830
+ } else {
1831
+ // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
1832
+ // version that has support for batches (submitBatchFn)
1833
+ for (const message of batch) {
1834
+ clientSequenceNumber = this.context.submitFn(
1835
+ MessageType.Operation,
1836
+ message.deserializedContent,
1837
+ true, // batch
1838
+ message.metadata);
1839
+ }
1840
+
1841
+ this.deltaSender.flush();
1842
+ }
1843
+
1844
+ // Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
1845
+ clientSequenceNumber -= batch.length - 1;
1846
+ assert(clientSequenceNumber >= 0, "clientSequenceNumber can't be negative");
1776
1847
  }
1777
1848
 
1778
- return this.deltaSender.flush();
1849
+ // Let the PendingStateManager know that a message was submitted.
1850
+ // In future, need to shift toward keeping batch as a whole!
1851
+ for (const message of batch) {
1852
+ this.pendingStateManager.onSubmitMessage(
1853
+ message.deserializedContent.type,
1854
+ clientSequenceNumber,
1855
+ message.referenceSequenceNumber,
1856
+ message.deserializedContent.contents,
1857
+ message.localOpMetadata,
1858
+ message.metadata,
1859
+ );
1860
+ clientSequenceNumber++;
1861
+ }
1862
+
1863
+ this.pendingStateManager.onFlush();
1779
1864
  }
1780
1865
 
1781
1866
  public orderSequentially(callback: () => void): void {
@@ -1801,9 +1886,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1801
1886
  }
1802
1887
 
1803
1888
  private trackOrderSequentiallyCalls(callback: () => void): void {
1804
- let checkpoint: { rollback: () => void; } | undefined;
1889
+ let checkpoint: { rollback: (action: (message: BatchMessage) => void) => void; } | undefined;
1805
1890
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
1806
- checkpoint = this.pendingStateManager.checkpoint();
1891
+ checkpoint = this.batchManager.checkpoint();
1807
1892
  }
1808
1893
 
1809
1894
  try {
@@ -1812,7 +1897,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1812
1897
  } catch (error) {
1813
1898
  if (checkpoint) {
1814
1899
  // This will throw and close the container if rollback fails
1815
- checkpoint.rollback();
1900
+ try {
1901
+ checkpoint.rollback((message: BatchMessage) =>
1902
+ this.rollback(
1903
+ message.deserializedContent.type,
1904
+ message.deserializedContent.contents,
1905
+ message.localOpMetadata));
1906
+ } catch (err) {
1907
+ const error2 = wrapError(err, (message) => {
1908
+ return DataProcessingError.create(
1909
+ `RollbackError: ${message}`,
1910
+ "checkpointRollback",
1911
+ undefined) as DataProcessingError;
1912
+ });
1913
+ this.closeFn(error2);
1914
+ throw error2;
1915
+ }
1816
1916
  } else {
1817
1917
  // pre-0.58 error message: orderSequentiallyCallbackException
1818
1918
  this.closeFn(new GenericError("orderSequentially callback exception", error));
@@ -1948,7 +2048,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1948
2048
  this.emit("attached");
1949
2049
  }
1950
2050
 
1951
- if (attachState === AttachState.Attached && !this.pendingStateManager.hasPendingMessages()) {
2051
+ if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
1952
2052
  this.updateDocumentDirtyState(false);
1953
2053
  }
1954
2054
  this.dataStores.setAttachState(attachState);
@@ -2216,6 +2316,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2216
2316
  },
2217
2317
  );
2218
2318
 
2319
+ assert(this.batchManager.empty, "Can't trigger summary in the middle of a batch");
2320
+
2219
2321
  let latestSnapshotVersionId: string | undefined;
2220
2322
  if (refreshLatestAck) {
2221
2323
  const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(
@@ -2413,7 +2515,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2413
2515
 
2414
2516
  let clientSequenceNumber: number;
2415
2517
  try {
2416
- clientSequenceNumber = this.submitSystemMessage(MessageType.Summarize, summaryMessage);
2518
+ clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
2417
2519
  } catch (error) {
2418
2520
  return { stage: "upload", ...uploadData, error };
2419
2521
  }
@@ -2472,7 +2574,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2472
2574
  }
2473
2575
  }
2474
2576
 
2577
+ private hasPendingMessages() {
2578
+ return this.pendingStateManager.hasPendingMessages() || !this.batchManager.empty;
2579
+ }
2580
+
2475
2581
  private updateDocumentDirtyState(dirty: boolean) {
2582
+ if (this.attachState !== AttachState.Attached) {
2583
+ assert(dirty, "Non-attached container is dirty");
2584
+ } else {
2585
+ // Other way is not true = see this.isContainerMessageDirtyable()
2586
+ assert(!dirty || this.hasPendingMessages(),
2587
+ "if doc is dirty, there has to be pending ops");
2588
+ }
2589
+
2476
2590
  if (this.dirtyContainer === dirty) {
2477
2591
  return;
2478
2592
  }
@@ -2511,31 +2625,58 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2511
2625
 
2512
2626
  private submit(
2513
2627
  type: ContainerMessageType,
2514
- content: any,
2628
+ contents: any,
2515
2629
  localOpMetadata: unknown = undefined,
2516
- opMetadata: Record<string, unknown> | undefined = undefined,
2630
+ metadata: Record<string, unknown> | undefined = undefined,
2517
2631
  ): void {
2518
2632
  this.verifyNotClosed();
2519
2633
 
2520
2634
  // There should be no ops in detached container state!
2521
2635
  assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
2522
2636
 
2523
- let clientSequenceNumber: number = -1;
2524
- let opMetadataInternal = opMetadata;
2637
+ const deserializedContent: ContainerRuntimeMessage = { type, contents };
2638
+ const serializedContent = JSON.stringify(deserializedContent);
2525
2639
 
2526
- if (this.canSendOps()) {
2527
- const serializedContent = JSON.stringify(content);
2640
+ if (this.deltaManager.readOnlyInfo.readonly) {
2641
+ this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
2642
+ }
2528
2643
 
2529
- // If in TurnBased flush mode we will trigger a flush at the next turn break
2530
- if (this.flushMode === FlushMode.TurnBased && !this.needsFlush) {
2531
- opMetadataInternal = {
2532
- ...opMetadata,
2533
- batch: true,
2534
- };
2535
- this.needsFlush = true;
2644
+ const message: BatchMessage = {
2645
+ contents: serializedContent,
2646
+ deserializedContent,
2647
+ metadata,
2648
+ localOpMetadata,
2649
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
2650
+ };
2536
2651
 
2537
- // Use Promise.resolve().then() to queue a microtask to detect the end of the turn and force a flush.
2538
- if (!this.flushTrigger) {
2652
+ try {
2653
+ // If this is attach message for new data store, and we are in a batch, send this op out of order
2654
+ // Is it safe:
2655
+ // Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
2656
+ // They become visible only when aliased, or handle to some sub-element of newly created datastore
2657
+ // is stored in some DDS, i.e. only after some other op.
2658
+ // Why:
2659
+ // Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
2660
+ // stores are created, causing issues like relay service throttling (too many ops) and catastrophic
2661
+ // failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
2662
+ // these issues.
2663
+ // Cons:
2664
+ // With large batches, relay service may throttle clients. Clients may disconnect while throttled.
2665
+ // This change creates new possibility of a lot of newly created data stores never being referenced
2666
+ // because client died before it had a change to submit the rest of the ops. This will create more
2667
+ // garbage that needs to be collected leveraging GC (Garbage Collection) feature.
2668
+ // Please note that this does not change file format, so it can be disabled in the future if this
2669
+ // optimization no longer makes sense (for example, batch compression may make it less appealing).
2670
+ if (this._flushMode === FlushMode.TurnBased && type === ContainerMessageType.Attach &&
2671
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
2672
+ this.flushBatch([message]);
2673
+ } else {
2674
+ this.batchManager.push(message);
2675
+ if (this._flushMode !== FlushMode.TurnBased) {
2676
+ this.flush();
2677
+ } else if (!this.flushTrigger) {
2678
+ this.flushTrigger = true;
2679
+ // Queue a microtask to detect the end of the turn and force a flush.
2539
2680
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
2540
2681
  Promise.resolve().then(() => {
2541
2682
  this.flushTrigger = false;
@@ -2543,72 +2684,32 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2543
2684
  });
2544
2685
  }
2545
2686
  }
2546
-
2547
- if (!serializedContent || serializedContent.length <= defaultMaxOpSizeInBytes) {
2548
- clientSequenceNumber = this.submitRuntimeMessage(type,
2549
- content, this._flushMode === FlushMode.TurnBased /* batch */, opMetadataInternal);
2550
- } else {
2551
- // If the content length is larger than the client configured message size
2552
- // instead of splitting the content, we will fail by explicitly closing the container
2553
- this.closeFn(new GenericError(
2554
- "OpTooLarge",
2555
- /* error */ undefined,
2556
- {
2557
- length: serializedContent.length,
2558
- limit: defaultMaxOpSizeInBytes,
2559
- }));
2560
- clientSequenceNumber = -1;
2561
- }
2687
+ } catch (error) {
2688
+ this.closeFn(error as GenericError);
2689
+ throw error;
2562
2690
  }
2563
2691
 
2564
- // Let the PendingStateManager know that a message was submitted.
2565
- this.pendingStateManager.onSubmitMessage(
2566
- type,
2567
- clientSequenceNumber,
2568
- this.deltaManager.lastSequenceNumber,
2569
- content,
2570
- localOpMetadata,
2571
- opMetadataInternal,
2572
- );
2573
- if (this.isContainerMessageDirtyable(type, content)) {
2692
+ if (this.isContainerMessageDirtyable(type, contents)) {
2574
2693
  this.updateDocumentDirtyState(true);
2575
2694
  }
2576
2695
  }
2577
2696
 
2578
- private submitSystemMessage(
2579
- type: MessageType,
2580
- contents: any) {
2697
+ private submitSummaryMessage(contents: ISummaryContent) {
2581
2698
  this.verifyNotClosed();
2582
2699
  assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
2583
2700
 
2584
2701
  // System message should not be sent in the middle of the batch.
2585
- // That said, we can preserve existing behavior by not flushing existing buffer.
2586
- // That might be not what caller hopes to get, but we can look deeper if telemetry tells us it's a problem.
2587
- const middleOfBatch = this.flushMode === FlushMode.TurnBased && this.needsFlush;
2588
- if (middleOfBatch) {
2589
- this.mc.logger.sendErrorEvent({ eventName: "submitSystemMessageError", type });
2590
- }
2591
-
2592
- return this.context.submitFn(
2593
- type,
2594
- contents,
2595
- middleOfBatch);
2596
- }
2702
+ assert(this.batchManager.empty, "System op in the middle of a batch");
2597
2703
 
2598
- private submitRuntimeMessage(
2599
- type: ContainerMessageType,
2600
- contents: any,
2601
- batch: boolean,
2602
- appData?: any,
2603
- ) {
2604
- this.verifyNotClosed();
2605
- assert(this.connected, 0x259 /* "Container disconnected when trying to submit system message" */);
2606
- const payload: ContainerRuntimeMessage = { type, contents };
2607
- return this.context.submitFn(
2608
- MessageType.Operation,
2609
- payload,
2610
- batch,
2611
- appData);
2704
+ // back-compat: ADO #1385: Make this call unconditional in the future
2705
+ if (this.context.submitSummaryFn !== undefined) {
2706
+ return this.context.submitSummaryFn(contents);
2707
+ } else {
2708
+ return this.context.submitFn(
2709
+ MessageType.Summarize,
2710
+ contents,
2711
+ false); // batch
2712
+ }
2612
2713
  }
2613
2714
 
2614
2715
  /**
@@ -2680,25 +2781,29 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2680
2781
  summaryLogger: ITelemetryLogger,
2681
2782
  ) {
2682
2783
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
2683
- const { snapshotTree } = await this.fetchSnapshotFromStorage(
2684
- ackHandle,
2685
- summaryLogger,
2686
- {
2687
- eventName: "RefreshLatestSummaryGetSnapshot",
2688
- ackHandle,
2689
- summaryRefSeq,
2690
- fetchLatest: false,
2691
- },
2692
- );
2693
- const result = await this.summarizerNode.refreshLatestSummary(
2694
- proposalHandle,
2695
- summaryRefSeq,
2696
- async () => snapshotTree,
2697
- readAndParseBlob,
2698
- summaryLogger,
2699
- );
2700
-
2701
- // Notify the garbage collector so it can update its latest summary state.
2784
+ // The call to fetch the snapshot is very expensive and not always needed.
2785
+ // It should only be done by the summarizerNode, if required.
2786
+ const snapshotTreeFetcher = async () => {
2787
+ const fetchResult = await this.fetchSnapshotFromStorage(
2788
+ ackHandle,
2789
+ summaryLogger,
2790
+ {
2791
+ eventName: "RefreshLatestSummaryGetSnapshot",
2792
+ ackHandle,
2793
+ summaryRefSeq,
2794
+ fetchLatest: false,
2795
+ });
2796
+ return fetchResult.snapshotTree;
2797
+ };
2798
+ const result = await this.summarizerNode.refreshLatestSummary(
2799
+ proposalHandle,
2800
+ summaryRefSeq,
2801
+ snapshotTreeFetcher,
2802
+ readAndParseBlob,
2803
+ summaryLogger,
2804
+ );
2805
+
2806
+ // Notify the garbage collector so it can update its latest summary state.
2702
2807
  await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
2703
2808
  }
2704
2809
 
@@ -2785,6 +2890,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2785
2890
  throw new UsageError("can't get state when offline load disabled");
2786
2891
  }
2787
2892
 
2893
+ // Flush pending batch.
2894
+ // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
2895
+ // to close current batch.
2896
+ this.flush();
2897
+
2788
2898
  const previousPendingState = this.context.pendingLocalState as IPendingRuntimeState | undefined;
2789
2899
  if (previousPendingState) {
2790
2900
  return {
@@ -2874,6 +2984,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2874
2984
  // we may not have seen every sequence number (because of system ops) so apply everything once we
2875
2985
  // don't have any more saved ops
2876
2986
  await this.pendingStateManager.applyStashedOpsAt();
2987
+
2988
+ // If it's not the case, we should take it into account when calculating dirty state.
2989
+ assert(this.context.attachState === AttachState.Attached,
2990
+ "this function is called for attached containers only");
2991
+ if (!this.hasPendingMessages()) {
2992
+ this.updateDocumentDirtyState(false);
2993
+ }
2877
2994
  }
2878
2995
 
2879
2996
  private validateSummaryHeuristicConfiguration(configuration: ISummaryConfigurationHeuristics) {