@fluidframework/container-runtime 2.0.0-internal.1.1.3 → 2.0.0-internal.1.2.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 (137) hide show
  1. package/dist/batchManager.d.ts +37 -0
  2. package/dist/batchManager.d.ts.map +1 -0
  3. package/dist/batchManager.js +73 -0
  4. package/dist/batchManager.js.map +1 -0
  5. package/dist/batchTracker.d.ts +1 -2
  6. package/dist/batchTracker.d.ts.map +1 -1
  7. package/dist/batchTracker.js +1 -2
  8. package/dist/batchTracker.js.map +1 -1
  9. package/dist/containerRuntime.d.ts +52 -20
  10. package/dist/containerRuntime.d.ts.map +1 -1
  11. package/dist/containerRuntime.js +240 -119
  12. package/dist/containerRuntime.js.map +1 -1
  13. package/dist/dataStoreContext.d.ts +12 -6
  14. package/dist/dataStoreContext.d.ts.map +1 -1
  15. package/dist/dataStoreContext.js +16 -13
  16. package/dist/dataStoreContext.js.map +1 -1
  17. package/dist/dataStores.d.ts +6 -2
  18. package/dist/dataStores.d.ts.map +1 -1
  19. package/dist/dataStores.js +7 -9
  20. package/dist/dataStores.js.map +1 -1
  21. package/dist/deltaScheduler.d.ts +6 -4
  22. package/dist/deltaScheduler.d.ts.map +1 -1
  23. package/dist/deltaScheduler.js +6 -4
  24. package/dist/deltaScheduler.js.map +1 -1
  25. package/dist/garbageCollection.d.ts +41 -12
  26. package/dist/garbageCollection.d.ts.map +1 -1
  27. package/dist/garbageCollection.js +176 -98
  28. package/dist/garbageCollection.js.map +1 -1
  29. package/dist/gcSweepReadyUsageDetection.d.ts +53 -0
  30. package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -0
  31. package/dist/gcSweepReadyUsageDetection.js +135 -0
  32. package/dist/gcSweepReadyUsageDetection.js.map +1 -0
  33. package/dist/orderedClientElection.d.ts +28 -10
  34. package/dist/orderedClientElection.d.ts.map +1 -1
  35. package/dist/orderedClientElection.js +14 -4
  36. package/dist/orderedClientElection.js.map +1 -1
  37. package/dist/packageVersion.d.ts +1 -1
  38. package/dist/packageVersion.js +1 -1
  39. package/dist/packageVersion.js.map +1 -1
  40. package/dist/pendingStateManager.d.ts +0 -11
  41. package/dist/pendingStateManager.d.ts.map +1 -1
  42. package/dist/pendingStateManager.js +9 -44
  43. package/dist/pendingStateManager.js.map +1 -1
  44. package/dist/runningSummarizer.js +1 -1
  45. package/dist/runningSummarizer.js.map +1 -1
  46. package/dist/scheduleManager.d.ts +6 -3
  47. package/dist/scheduleManager.d.ts.map +1 -1
  48. package/dist/scheduleManager.js +22 -14
  49. package/dist/scheduleManager.js.map +1 -1
  50. package/dist/summarizerTypes.d.ts +16 -9
  51. package/dist/summarizerTypes.d.ts.map +1 -1
  52. package/dist/summarizerTypes.js +1 -1
  53. package/dist/summarizerTypes.js.map +1 -1
  54. package/dist/summaryCollection.d.ts +1 -0
  55. package/dist/summaryCollection.d.ts.map +1 -1
  56. package/dist/summaryCollection.js +29 -13
  57. package/dist/summaryCollection.js.map +1 -1
  58. package/dist/summaryManager.d.ts +2 -2
  59. package/dist/summaryManager.js +2 -2
  60. package/dist/summaryManager.js.map +1 -1
  61. package/lib/batchManager.d.ts +37 -0
  62. package/lib/batchManager.d.ts.map +1 -0
  63. package/lib/batchManager.js +69 -0
  64. package/lib/batchManager.js.map +1 -0
  65. package/lib/batchTracker.d.ts +1 -2
  66. package/lib/batchTracker.d.ts.map +1 -1
  67. package/lib/batchTracker.js +1 -2
  68. package/lib/batchTracker.js.map +1 -1
  69. package/lib/containerRuntime.d.ts +52 -20
  70. package/lib/containerRuntime.d.ts.map +1 -1
  71. package/lib/containerRuntime.js +243 -122
  72. package/lib/containerRuntime.js.map +1 -1
  73. package/lib/dataStoreContext.d.ts +12 -6
  74. package/lib/dataStoreContext.d.ts.map +1 -1
  75. package/lib/dataStoreContext.js +16 -13
  76. package/lib/dataStoreContext.js.map +1 -1
  77. package/lib/dataStores.d.ts +6 -2
  78. package/lib/dataStores.d.ts.map +1 -1
  79. package/lib/dataStores.js +7 -9
  80. package/lib/dataStores.js.map +1 -1
  81. package/lib/deltaScheduler.d.ts +6 -4
  82. package/lib/deltaScheduler.d.ts.map +1 -1
  83. package/lib/deltaScheduler.js +6 -4
  84. package/lib/deltaScheduler.js.map +1 -1
  85. package/lib/garbageCollection.d.ts +41 -12
  86. package/lib/garbageCollection.d.ts.map +1 -1
  87. package/lib/garbageCollection.js +175 -97
  88. package/lib/garbageCollection.js.map +1 -1
  89. package/lib/gcSweepReadyUsageDetection.d.ts +53 -0
  90. package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -0
  91. package/lib/gcSweepReadyUsageDetection.js +130 -0
  92. package/lib/gcSweepReadyUsageDetection.js.map +1 -0
  93. package/lib/orderedClientElection.d.ts +28 -10
  94. package/lib/orderedClientElection.d.ts.map +1 -1
  95. package/lib/orderedClientElection.js +14 -4
  96. package/lib/orderedClientElection.js.map +1 -1
  97. package/lib/packageVersion.d.ts +1 -1
  98. package/lib/packageVersion.js +1 -1
  99. package/lib/packageVersion.js.map +1 -1
  100. package/lib/pendingStateManager.d.ts +0 -11
  101. package/lib/pendingStateManager.d.ts.map +1 -1
  102. package/lib/pendingStateManager.js +9 -44
  103. package/lib/pendingStateManager.js.map +1 -1
  104. package/lib/runningSummarizer.js +1 -1
  105. package/lib/runningSummarizer.js.map +1 -1
  106. package/lib/scheduleManager.d.ts +6 -3
  107. package/lib/scheduleManager.d.ts.map +1 -1
  108. package/lib/scheduleManager.js +24 -16
  109. package/lib/scheduleManager.js.map +1 -1
  110. package/lib/summarizerTypes.d.ts +16 -9
  111. package/lib/summarizerTypes.d.ts.map +1 -1
  112. package/lib/summarizerTypes.js +1 -1
  113. package/lib/summarizerTypes.js.map +1 -1
  114. package/lib/summaryCollection.d.ts +1 -0
  115. package/lib/summaryCollection.d.ts.map +1 -1
  116. package/lib/summaryCollection.js +29 -13
  117. package/lib/summaryCollection.js.map +1 -1
  118. package/lib/summaryManager.d.ts +2 -2
  119. package/lib/summaryManager.js +2 -2
  120. package/lib/summaryManager.js.map +1 -1
  121. package/package.json +21 -18
  122. package/src/batchManager.ts +91 -0
  123. package/src/batchTracker.ts +1 -2
  124. package/src/containerRuntime.ts +336 -185
  125. package/src/dataStoreContext.ts +18 -14
  126. package/src/dataStores.ts +7 -8
  127. package/src/deltaScheduler.ts +6 -4
  128. package/src/garbageCollection.ts +224 -134
  129. package/src/gcSweepReadyUsageDetection.ts +147 -0
  130. package/src/orderedClientElection.ts +31 -10
  131. package/src/packageVersion.ts +1 -1
  132. package/src/pendingStateManager.ts +9 -57
  133. package/src/runningSummarizer.ts +1 -1
  134. package/src/scheduleManager.ts +32 -12
  135. package/src/summarizerTypes.ts +17 -9
  136. package/src/summaryCollection.ts +31 -16
  137. package/src/summaryManager.ts +2 -2
@@ -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,9 +201,10 @@ export interface ContainerRuntimeMessage {
195
201
  contents: any;
196
202
  type: ContainerMessageType;
197
203
  }
204
+
198
205
  export interface ISummaryBaseConfiguration {
199
206
  /**
200
- * Delay before first attempt to spawn summarizing container.
207
+ * Delay before first attempt to spawn summarizing container.
201
208
  */
202
209
  initialSummarizerDelayMs: number;
203
210
 
@@ -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,15 @@ export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
517
538
  return false;
518
539
  }
519
540
 
541
+ /**
542
+ * Unpacks runtime messages
543
+ *
544
+ * @remarks This API makes no promises regarding backward-compatability. This is internal API.
545
+ * @param message - message (as it observed in storage / service)
546
+ * @returns unpacked runtime message
547
+ *
548
+ * @internal
549
+ */
520
550
  export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
521
551
  if (message.type === MessageType.Operation) {
522
552
  // legacy op format?
@@ -529,13 +559,14 @@ export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
529
559
  message.type = innerContents.type;
530
560
  message.contents = innerContents.contents;
531
561
  }
532
- assert(isUnpackedRuntimeMessage(message), 0x122 /* "Message to unpack is not proper runtime message" */);
562
+ return true;
533
563
  } else {
534
564
  // Legacy format, but it's already "unpacked",
535
565
  // i.e. message.type is actually ContainerMessageType.
566
+ // Or it's non-runtime message.
536
567
  // Nothing to do in such case.
568
+ return false;
537
569
  }
538
- return message;
539
570
  }
540
571
 
541
572
  /**
@@ -774,7 +805,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
774
805
 
775
806
  private _orderSequentiallyCalls: number = 0;
776
807
  private _flushMode: FlushMode;
777
- private needsFlush = false;
778
808
  private flushTrigger = false;
779
809
 
780
810
  private _connected: boolean;
@@ -823,6 +853,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
823
853
  private readonly scheduleManager: ScheduleManager;
824
854
  private readonly blobManager: BlobManager;
825
855
  private readonly pendingStateManager: PendingStateManager;
856
+
857
+ // Provide lower soft limit - we want to have some number of ops to get efficiency in compression & bandwidth usage,
858
+ // but at the same time we want to send these ops sooner, to reduce overall latency of processing a batch.
859
+ // So there is some ballance here, that depends on compression algorithm and its efficiency working with smaller
860
+ // payloads. That number represents final (compressed) bits (once compression is implemented).
861
+ private readonly pendingAttachBatch = new BatchManager(64 * 1024);
862
+ private readonly pendingBatch = new BatchManager();
863
+
826
864
  private readonly garbageCollector: IGarbageCollector;
827
865
 
828
866
  // Local copy of incomplete received chunks.
@@ -833,6 +871,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
833
871
  /** The last message processed at the time of the last summary. */
834
872
  private messageAtLastSummary: ISummaryMetadataMessage | undefined;
835
873
 
874
+ private get emptyBatch() {
875
+ return this.pendingBatch.empty && this.pendingAttachBatch.empty;
876
+ }
877
+
836
878
  private get summarizer(): Summarizer {
837
879
  assert(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
838
880
  return this._summarizer;
@@ -868,11 +910,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
868
910
  if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
869
911
  return true;
870
912
  }
871
- if (this.summaryConfiguration.state !== "disabled") {
872
- return this.summaryConfiguration.summarizerClientElection === true;
873
- } else {
874
- return false;
875
- }
913
+ return this.summaryConfiguration.state !== "disabled"
914
+ ? this.summaryConfiguration.summarizerClientElection === true
915
+ : false;
876
916
  }
877
917
  private readonly maxOpsSinceLastSummary: number;
878
918
  private getMaxOpsSinceLastSummary(): number {
@@ -881,11 +921,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
881
921
  if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
882
922
  return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
883
923
  }
884
- if (this.summaryConfiguration.state !== "disabled") {
885
- return this.summaryConfiguration.maxOpsSinceLastSummary;
886
- } else {
887
- return 0;
888
- }
924
+ return this.summaryConfiguration.state !== "disabled"
925
+ ? this.summaryConfiguration.maxOpsSinceLastSummary
926
+ : 0;
889
927
  }
890
928
 
891
929
  private readonly initialSummarizerDelayMs: number;
@@ -895,11 +933,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
895
933
  if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
896
934
  return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
897
935
  }
898
- if (this.summaryConfiguration.state !== "disabled") {
899
- return this.summaryConfiguration.initialSummarizerDelayMs;
900
- } else {
901
- return 0;
902
- }
936
+ return this.summaryConfiguration.state !== "disabled"
937
+ ? this.summaryConfiguration.initialSummarizerDelayMs
938
+ : 0;
903
939
  }
904
940
 
905
941
  private readonly createContainerMetadata: ICreateContainerMetadata;
@@ -970,6 +1006,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
970
1006
  getNodePackagePath: async (nodePath: string) => this.getGCNodePackagePath(nodePath),
971
1007
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
972
1008
  readAndParseBlob: async <T>(id: string) => readAndParse<T>(this.storage, id),
1009
+ getContainerDiagnosticId: () => this.context.id,
1010
+ activeConnection: () => this.deltaManager.active,
973
1011
  });
974
1012
 
975
1013
  const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
@@ -1044,6 +1082,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1044
1082
  this.scheduleManager = new ScheduleManager(
1045
1083
  context.deltaManager,
1046
1084
  this,
1085
+ () => this.clientId,
1047
1086
  ChildLogger.create(this.logger, "ScheduleManager"),
1048
1087
  );
1049
1088
 
@@ -1058,7 +1097,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1058
1097
  flush: this.flush.bind(this),
1059
1098
  flushMode: () => this.flushMode,
1060
1099
  reSubmit: this.reSubmit.bind(this),
1061
- rollback: this.rollback.bind(this),
1062
1100
  setFlushMode: (mode) => this.setFlushMode(mode),
1063
1101
  },
1064
1102
  this._flushMode,
@@ -1293,15 +1331,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1293
1331
 
1294
1332
  if (id === BlobManager.basePath && requestParser.isLeaf(2)) {
1295
1333
  const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
1296
- if (blob) {
1297
- return {
1334
+ return blob
1335
+ ? {
1298
1336
  status: 200,
1299
1337
  mimeType: "fluid/object",
1300
1338
  value: blob,
1301
- };
1302
- } else {
1303
- return create404Response(request);
1304
- }
1339
+ } : create404Response(request);
1305
1340
  } else if (requestParser.pathParts.length > 0) {
1306
1341
  const dataStore = await this.getDataStoreFromRequest(id, request);
1307
1342
  const subRequest = requestParser.createSubRequest(1);
@@ -1427,7 +1462,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1427
1462
  return true;
1428
1463
  }
1429
1464
 
1430
- if (!this.pendingStateManager.hasPendingMessages()) {
1465
+ if (!this.hasPendingMessages()) {
1431
1466
  // If there are no pending messages, we can always reconnect
1432
1467
  this.resetReconnectCount();
1433
1468
  return true;
@@ -1546,6 +1581,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1546
1581
  this._perfSignalData.signalsLost = 0;
1547
1582
  this._perfSignalData.signalTimestamp = 0;
1548
1583
  this._perfSignalData.trackingSignalSequenceNumber = undefined;
1584
+ } else {
1585
+ assert(this.attachState === AttachState.Attached,
1586
+ 0x3cd /* Connection is possible only if container exists in storage */);
1549
1587
  }
1550
1588
 
1551
1589
  // Fail while disconnected
@@ -1554,9 +1592,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1554
1592
 
1555
1593
  if (!this.shouldContinueReconnecting()) {
1556
1594
  this.closeFn(
1557
- // pre-0.58 error message: MaxReconnectsWithNoProgress
1558
1595
  DataProcessingError.create(
1559
- "Runtime detected too many reconnects with no progress syncing local ops",
1596
+ // eslint-disable-next-line max-len
1597
+ "Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)",
1560
1598
  "setConnectionState",
1561
1599
  undefined,
1562
1600
  {
@@ -1573,6 +1611,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1573
1611
  }
1574
1612
 
1575
1613
  this.dataStores.setConnectionState(connected, clientId);
1614
+ this.garbageCollector.setConnectionState(connected, clientId);
1576
1615
 
1577
1616
  raiseConnectedEvent(this.mc.logger, this, connected, clientId);
1578
1617
  }
@@ -1580,49 +1619,50 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1580
1619
  public process(messageArg: ISequencedDocumentMessage, local: boolean) {
1581
1620
  this.verifyNotClosed();
1582
1621
 
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
1622
  // Do shallow copy of message, as methods below will modify it.
1593
1623
  // There might be multiple container instances receiving same message
1594
1624
  // We do not need to make deep copy, as each layer will just replace message.content itself,
1595
1625
  // but would not modify contents details
1596
1626
  let message = { ...messageArg };
1597
1627
 
1628
+ // back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
1629
+ // System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
1630
+ // Old ops may contain empty string (I assume noops).
1631
+ if (typeof message.contents === "string" && message.contents !== "") {
1632
+ message.contents = JSON.parse(message.contents);
1633
+ }
1634
+
1635
+ // Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
1636
+ // This format was not shipped to production workflows.
1637
+ const runtimeMessage = unpackRuntimeMessage(message);
1638
+
1639
+ if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
1640
+ this.savedOps.push(messageArg);
1641
+ }
1642
+
1598
1643
  // Surround the actual processing of the operation with messages to the schedule manager indicating
1599
1644
  // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
1600
1645
  // messages once a batch has been fully processed.
1601
1646
  this.scheduleManager.beforeOpProcessing(message);
1602
1647
 
1603
1648
  try {
1604
- message = unpackRuntimeMessage(message);
1605
-
1606
1649
  // Chunk processing must come first given that we will transform the message to the unchunked version
1607
1650
  // once all pieces are available
1608
1651
  message = this.processRemoteChunkedMessage(message);
1609
1652
 
1610
1653
  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
- }
1654
+ if (local && runtimeMessage) {
1655
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1617
1656
  }
1618
1657
 
1619
1658
  // If there are no more pending messages after processing a local message,
1620
1659
  // the document is no longer dirty.
1621
- if (!this.pendingStateManager.hasPendingMessages()) {
1660
+ if (!this.hasPendingMessages()) {
1622
1661
  this.updateDocumentDirtyState(false);
1623
1662
  }
1624
1663
 
1625
- switch (message.type) {
1664
+ const type = message.type as ContainerMessageType;
1665
+ switch (type) {
1626
1666
  case ContainerMessageType.Attach:
1627
1667
  this.dataStores.processAttachMessage(message, local);
1628
1668
  break;
@@ -1635,10 +1675,18 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1635
1675
  case ContainerMessageType.BlobAttach:
1636
1676
  this.blobManager.processBlobAttachOp(message, local);
1637
1677
  break;
1678
+ case ContainerMessageType.ChunkedOp:
1679
+ case ContainerMessageType.Rejoin:
1680
+ break;
1638
1681
  default:
1682
+ assert(!runtimeMessage, 0x3ce /* Runtime message of unknown type */);
1683
+ }
1684
+
1685
+ // For back-compat, notify only about runtime messages for now.
1686
+ if (runtimeMessage) {
1687
+ this.emit("op", message, runtimeMessage);
1639
1688
  }
1640
1689
 
1641
- this.emit("op", message);
1642
1690
  this.scheduleManager.afterOpProcessing(undefined, message);
1643
1691
 
1644
1692
  if (local) {
@@ -1752,30 +1800,76 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1752
1800
  assert(this._orderSequentiallyCalls === 0,
1753
1801
  0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
1754
1802
 
1755
- if (!this.deltaSender) {
1756
- return;
1757
- }
1803
+ this.flushBatch(this.pendingAttachBatch.popBatch());
1804
+ this.flushBatch(this.pendingBatch.popBatch());
1758
1805
 
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();
1806
+ assert(this.emptyBatch, 0x3cf /* reentrancy */);
1807
+ }
1764
1808
 
1765
- // If flush has already been called then exit early
1766
- if (!this.needsFlush) {
1767
- return;
1809
+ protected flushBatch(batch: BatchMessage[]): void {
1810
+ const length = batch.length;
1811
+
1812
+ if (length > 1) {
1813
+ batch[0].metadata = { ...batch[0].metadata, batch: true };
1814
+ batch[length - 1].metadata = { ...batch[length - 1].metadata, batch: false };
1815
+
1816
+ // This assert fires for the following reason (there might be more cases like that):
1817
+ // AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
1818
+ // i.e. in the middle of op processing!
1819
+ // Sending ops while processing ops is not good idea - it's not defined when
1820
+ // referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
1821
+ // If we send ops in response to processing multiple ops, then we for sure hit this assert!
1822
+ // Tracked via ADO #1834
1823
+ // assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
1824
+ // "Batch should be generated synchronously, without processing ops in the middle!");
1768
1825
  }
1769
1826
 
1770
- this.needsFlush = false;
1827
+ let clientSequenceNumber: number = -1;
1771
1828
 
1772
1829
  // Did we disconnect in the middle of turn-based batch?
1773
1830
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
1774
- if (!this.canSendOps()) {
1775
- return;
1831
+ if (this.canSendOps()) {
1832
+ if (this.context.submitBatchFn !== undefined) {
1833
+ const batchToSend: IBatchMessage[] = [];
1834
+ for (const message of batch) {
1835
+ batchToSend.push({ contents: message.contents, metadata: message.metadata });
1836
+ }
1837
+ // returns clientSequenceNumber of last message in a batch
1838
+ clientSequenceNumber = this.context.submitBatchFn(batchToSend);
1839
+ } else {
1840
+ // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
1841
+ // version that has support for batches (submitBatchFn)
1842
+ for (const message of batch) {
1843
+ clientSequenceNumber = this.context.submitFn(
1844
+ MessageType.Operation,
1845
+ message.deserializedContent,
1846
+ true, // batch
1847
+ message.metadata);
1848
+ }
1849
+
1850
+ this.deltaSender.flush();
1851
+ }
1852
+
1853
+ // Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
1854
+ clientSequenceNumber -= batch.length - 1;
1855
+ assert(clientSequenceNumber >= 0, 0x3d0 /* clientSequenceNumber can't be negative */);
1776
1856
  }
1777
1857
 
1778
- return this.deltaSender.flush();
1858
+ // Let the PendingStateManager know that a message was submitted.
1859
+ // In future, need to shift toward keeping batch as a whole!
1860
+ for (const message of batch) {
1861
+ this.pendingStateManager.onSubmitMessage(
1862
+ message.deserializedContent.type,
1863
+ clientSequenceNumber,
1864
+ message.referenceSequenceNumber,
1865
+ message.deserializedContent.contents,
1866
+ message.localOpMetadata,
1867
+ message.metadata,
1868
+ );
1869
+ clientSequenceNumber++;
1870
+ }
1871
+
1872
+ this.pendingStateManager.onFlush();
1779
1873
  }
1780
1874
 
1781
1875
  public orderSequentially(callback: () => void): void {
@@ -1801,9 +1895,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1801
1895
  }
1802
1896
 
1803
1897
  private trackOrderSequentiallyCalls(callback: () => void): void {
1804
- let checkpoint: { rollback: () => void; } | undefined;
1898
+ let checkpoint: { rollback: (action: (message: BatchMessage) => void) => void; } | undefined;
1805
1899
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
1806
- checkpoint = this.pendingStateManager.checkpoint();
1900
+ // Note: we are not touching this.pendingAttachBatch here, for two reasons:
1901
+ // 1. It would not help, as we flush attach ops as they become available.
1902
+ // 2. There is no way to undo process of data store creation.
1903
+ checkpoint = this.pendingBatch.checkpoint();
1807
1904
  }
1808
1905
 
1809
1906
  try {
@@ -1812,7 +1909,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1812
1909
  } catch (error) {
1813
1910
  if (checkpoint) {
1814
1911
  // This will throw and close the container if rollback fails
1815
- checkpoint.rollback();
1912
+ try {
1913
+ checkpoint.rollback((message: BatchMessage) =>
1914
+ this.rollback(
1915
+ message.deserializedContent.type,
1916
+ message.deserializedContent.contents,
1917
+ message.localOpMetadata));
1918
+ } catch (err) {
1919
+ const error2 = wrapError(err, (message) => {
1920
+ return DataProcessingError.create(
1921
+ `RollbackError: ${message}`,
1922
+ "checkpointRollback",
1923
+ undefined) as DataProcessingError;
1924
+ });
1925
+ this.closeFn(error2);
1926
+ throw error2;
1927
+ }
1816
1928
  } else {
1817
1929
  // pre-0.58 error message: orderSequentiallyCallbackException
1818
1930
  this.closeFn(new GenericError("orderSequentially callback exception", error));
@@ -1948,7 +2060,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1948
2060
  this.emit("attached");
1949
2061
  }
1950
2062
 
1951
- if (attachState === AttachState.Attached && !this.pendingStateManager.hasPendingMessages()) {
2063
+ if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
1952
2064
  this.updateDocumentDirtyState(false);
1953
2065
  }
1954
2066
  this.dataStores.setAttachState(attachState);
@@ -2170,7 +2282,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2170
2282
 
2171
2283
  /**
2172
2284
  * Runs garbage collection and updates the reference / used state of the nodes in the container.
2173
- * @returns the statistics of the garbage collection run.
2285
+ * @returns the statistics of the garbage collection run; undefined if GC did not run.
2174
2286
  */
2175
2287
  public async collectGarbage(
2176
2288
  options: {
@@ -2181,7 +2293,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2181
2293
  /** True to generate full GC data */
2182
2294
  fullGC?: boolean;
2183
2295
  },
2184
- ): Promise<IGCStats> {
2296
+ ): Promise<IGCStats | undefined> {
2185
2297
  return this.garbageCollector.collectGarbage(options);
2186
2298
  }
2187
2299
 
@@ -2216,6 +2328,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2216
2328
  },
2217
2329
  );
2218
2330
 
2331
+ assert(this.emptyBatch, 0x3d1 /* Can't trigger summary in the middle of a batch */);
2332
+
2219
2333
  let latestSnapshotVersionId: string | undefined;
2220
2334
  if (refreshLatestAck) {
2221
2335
  const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(
@@ -2305,7 +2419,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2305
2419
  const forcedFullTree = this.garbageCollector.summaryStateNeedsReset;
2306
2420
  try {
2307
2421
  summarizeResult = await this.summarize({
2308
- fullTree: fullTree || forcedFullTree,
2422
+ fullTree: fullTree ?? forcedFullTree,
2309
2423
  trackState: true,
2310
2424
  summaryLogger: summaryNumberLogger,
2311
2425
  runGC: this.garbageCollector.shouldRunGC,
@@ -2413,7 +2527,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2413
2527
 
2414
2528
  let clientSequenceNumber: number;
2415
2529
  try {
2416
- clientSequenceNumber = this.submitSystemMessage(MessageType.Summarize, summaryMessage);
2530
+ clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
2417
2531
  } catch (error) {
2418
2532
  return { stage: "upload", ...uploadData, error };
2419
2533
  }
@@ -2472,7 +2586,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2472
2586
  }
2473
2587
  }
2474
2588
 
2589
+ private hasPendingMessages() {
2590
+ return this.pendingStateManager.hasPendingMessages() || !this.emptyBatch;
2591
+ }
2592
+
2475
2593
  private updateDocumentDirtyState(dirty: boolean) {
2594
+ if (this.attachState !== AttachState.Attached) {
2595
+ assert(dirty, 0x3d2 /* Non-attached container is dirty */);
2596
+ } else {
2597
+ // Other way is not true = see this.isContainerMessageDirtyable()
2598
+ assert(!dirty || this.hasPendingMessages(),
2599
+ 0x3d3 /* if doc is dirty, there has to be pending ops */);
2600
+ }
2601
+
2476
2602
  if (this.dirtyContainer === dirty) {
2477
2603
  return;
2478
2604
  }
@@ -2511,104 +2637,117 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2511
2637
 
2512
2638
  private submit(
2513
2639
  type: ContainerMessageType,
2514
- content: any,
2640
+ contents: any,
2515
2641
  localOpMetadata: unknown = undefined,
2516
- opMetadata: Record<string, unknown> | undefined = undefined,
2642
+ metadata: Record<string, unknown> | undefined = undefined,
2517
2643
  ): void {
2518
2644
  this.verifyNotClosed();
2519
2645
 
2520
2646
  // There should be no ops in detached container state!
2521
2647
  assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
2522
2648
 
2523
- let clientSequenceNumber: number = -1;
2524
- let opMetadataInternal = opMetadata;
2649
+ const deserializedContent: ContainerRuntimeMessage = { type, contents };
2650
+ const serializedContent = JSON.stringify(deserializedContent);
2525
2651
 
2526
- if (this.canSendOps()) {
2527
- const serializedContent = JSON.stringify(content);
2652
+ if (this.deltaManager.readOnlyInfo.readonly) {
2653
+ this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
2654
+ }
2528
2655
 
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;
2536
-
2537
- // Use Promise.resolve().then() to queue a microtask to detect the end of the turn and force a flush.
2538
- if (!this.flushTrigger) {
2539
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
2540
- Promise.resolve().then(() => {
2541
- this.flushTrigger = false;
2542
- this.flush();
2543
- });
2656
+ const message: BatchMessage = {
2657
+ contents: serializedContent,
2658
+ deserializedContent,
2659
+ metadata,
2660
+ localOpMetadata,
2661
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
2662
+ };
2663
+
2664
+ try {
2665
+ // If this is attach message for new data store, and we are in a batch, send this op out of order
2666
+ // Is it safe:
2667
+ // Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
2668
+ // They become visible only when aliased, or handle to some sub-element of newly created datastore
2669
+ // is stored in some DDS, i.e. only after some other op.
2670
+ // Why:
2671
+ // Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
2672
+ // stores are created, causing issues like relay service throttling (too many ops) and catastrophic
2673
+ // failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
2674
+ // these issues.
2675
+ // Cons:
2676
+ // 1. With large batches, relay service may throttle clients. Clients may disconnect while throttled.
2677
+ // This change creates new possibility of a lot of newly created data stores never being referenced
2678
+ // because client died before it had a change to submit the rest of the ops. This will create more
2679
+ // garbage that needs to be collected leveraging GC (Garbage Collection) feature.
2680
+ // 2. Sending ops out of order means they are excluded from rollback functionality. This is not an issue
2681
+ // today as rollback can't undo creation of data store. To some extent not sending them is a bigger
2682
+ // issue than sending.
2683
+ // Please note that this does not change file format, so it can be disabled in the future if this
2684
+ // optimization no longer makes sense (for example, batch compression may make it less appealing).
2685
+ if (type === ContainerMessageType.Attach &&
2686
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
2687
+ if (!this.pendingAttachBatch.push(message)) {
2688
+ // BatchManager has two limits - soft limit & hard limit. Soft limit is only engaged
2689
+ // when queue is not empty.
2690
+ // Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
2691
+ this.flushBatch(this.pendingAttachBatch.popBatch());
2692
+ if (!this.pendingAttachBatch.push(message)) {
2693
+ throw new GenericError(
2694
+ "BatchTooLarge",
2695
+ /* error */ undefined,
2696
+ {
2697
+ opSize: message.contents.length,
2698
+ count: this.pendingAttachBatch.length,
2699
+ limit: this.pendingAttachBatch.limit,
2700
+ });
2701
+ }
2702
+ }
2703
+ } else {
2704
+ if (!this.pendingBatch.push(message)) {
2705
+ throw new GenericError(
2706
+ "BatchTooLarge",
2707
+ /* error */ undefined,
2708
+ {
2709
+ opSize: message.contents.length,
2710
+ count: this.pendingBatch.length,
2711
+ limit: this.pendingBatch.limit,
2712
+ });
2544
2713
  }
2545
2714
  }
2546
2715
 
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;
2716
+ if (this._flushMode !== FlushMode.TurnBased) {
2717
+ this.flush();
2718
+ } else if (!this.flushTrigger) {
2719
+ this.flushTrigger = true;
2720
+ // Queue a microtask to detect the end of the turn and force a flush.
2721
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
2722
+ Promise.resolve().then(() => {
2723
+ this.flushTrigger = false;
2724
+ this.flush();
2725
+ });
2561
2726
  }
2727
+ } catch (error) {
2728
+ this.closeFn(error as GenericError);
2729
+ throw error;
2562
2730
  }
2563
2731
 
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)) {
2732
+ if (this.isContainerMessageDirtyable(type, contents)) {
2574
2733
  this.updateDocumentDirtyState(true);
2575
2734
  }
2576
2735
  }
2577
2736
 
2578
- private submitSystemMessage(
2579
- type: MessageType,
2580
- contents: any) {
2737
+ private submitSummaryMessage(contents: ISummaryContent) {
2581
2738
  this.verifyNotClosed();
2582
2739
  assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
2583
2740
 
2584
2741
  // 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
- }
2742
+ assert(this.emptyBatch, 0x3d4 /* System op in the middle of a batch */);
2591
2743
 
2592
- return this.context.submitFn(
2593
- type,
2594
- contents,
2595
- middleOfBatch);
2596
- }
2597
-
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);
2744
+ // back-compat: ADO #1385: Make this call unconditional in the future
2745
+ return this.context.submitSummaryFn !== undefined
2746
+ ? this.context.submitSummaryFn(contents)
2747
+ : this.context.submitFn(
2748
+ MessageType.Summarize,
2749
+ contents,
2750
+ false);
2612
2751
  }
2613
2752
 
2614
2753
  /**
@@ -2684,25 +2823,25 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2684
2823
  // It should only be done by the summarizerNode, if required.
2685
2824
  const snapshotTreeFetcher = async () => {
2686
2825
  const fetchResult = await this.fetchSnapshotFromStorage(
2687
- ackHandle,
2688
- summaryLogger,
2689
- {
2690
- eventName: "RefreshLatestSummaryGetSnapshot",
2691
- ackHandle,
2692
- summaryRefSeq,
2693
- fetchLatest: false,
2694
- });
2695
- return fetchResult.snapshotTree;
2696
- };
2697
- const result = await this.summarizerNode.refreshLatestSummary(
2698
- proposalHandle,
2699
- summaryRefSeq,
2700
- snapshotTreeFetcher,
2701
- readAndParseBlob,
2702
- summaryLogger,
2703
- );
2704
-
2705
- // Notify the garbage collector so it can update its latest summary state.
2826
+ ackHandle,
2827
+ summaryLogger,
2828
+ {
2829
+ eventName: "RefreshLatestSummaryGetSnapshot",
2830
+ ackHandle,
2831
+ summaryRefSeq,
2832
+ fetchLatest: false,
2833
+ });
2834
+ return fetchResult.snapshotTree;
2835
+ };
2836
+ const result = await this.summarizerNode.refreshLatestSummary(
2837
+ proposalHandle,
2838
+ summaryRefSeq,
2839
+ snapshotTreeFetcher,
2840
+ readAndParseBlob,
2841
+ summaryLogger,
2842
+ );
2843
+
2844
+ // Notify the garbage collector so it can update its latest summary state.
2706
2845
  await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
2707
2846
  }
2708
2847
 
@@ -2789,6 +2928,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2789
2928
  throw new UsageError("can't get state when offline load disabled");
2790
2929
  }
2791
2930
 
2931
+ // Flush pending batch.
2932
+ // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
2933
+ // to close current batch.
2934
+ this.flush();
2935
+
2792
2936
  const previousPendingState = this.context.pendingLocalState as IPendingRuntimeState | undefined;
2793
2937
  if (previousPendingState) {
2794
2938
  return {
@@ -2878,6 +3022,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2878
3022
  // we may not have seen every sequence number (because of system ops) so apply everything once we
2879
3023
  // don't have any more saved ops
2880
3024
  await this.pendingStateManager.applyStashedOpsAt();
3025
+
3026
+ // If it's not the case, we should take it into account when calculating dirty state.
3027
+ assert(this.context.attachState === AttachState.Attached,
3028
+ 0x3d5 /* this function is called for attached containers only */);
3029
+ if (!this.hasPendingMessages()) {
3030
+ this.updateDocumentDirtyState(false);
3031
+ }
2881
3032
  }
2882
3033
 
2883
3034
  private validateSummaryHeuristicConfiguration(configuration: ISummaryConfigurationHeuristics) {