@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
@@ -1,8 +1,8 @@
1
1
  import { AttachState, LoaderHeader, } from "@fluidframework/container-definitions";
2
2
  import { assert, Trace, TypedEventEmitter, unreachableCase, } from "@fluidframework/common-utils";
3
- import { ChildLogger, raiseConnectedEvent, PerformanceEvent, TaggedLoggerAdapter, loggerToMonitoringContext, } from "@fluidframework/telemetry-utils";
3
+ import { ChildLogger, raiseConnectedEvent, PerformanceEvent, TaggedLoggerAdapter, loggerToMonitoringContext, wrapError, } from "@fluidframework/telemetry-utils";
4
4
  import { DriverHeader, FetchSource, } from "@fluidframework/driver-definitions";
5
- import { readAndParse, isUnpackedRuntimeMessage } from "@fluidframework/driver-utils";
5
+ import { readAndParse } from "@fluidframework/driver-utils";
6
6
  import { DataCorruptionError, DataProcessingError, GenericError, UsageError, } from "@fluidframework/container-utils";
7
7
  import { MessageType, SummaryType, } from "@fluidframework/protocol-definitions";
8
8
  import { FlushMode, channelsTreeName, } from "@fluidframework/runtime-definitions";
@@ -14,7 +14,8 @@ import { FluidDataStoreRegistry } from "./dataStoreRegistry";
14
14
  import { Summarizer } from "./summarizer";
15
15
  import { SummaryManager } from "./summaryManager";
16
16
  import { ReportOpPerfTelemetry, } from "./connectionTelemetry";
17
- import { PendingStateManager } from "./pendingStateManager";
17
+ import { PendingStateManager, } from "./pendingStateManager";
18
+ import { BatchManager } from "./batchManager";
18
19
  import { pkgVersion } from "./packageVersion";
19
20
  import { BlobManager } from "./blobManager";
20
21
  import { DataStores, getSummaryForDatastores } from "./dataStores";
@@ -75,11 +76,10 @@ export var RuntimeHeaders;
75
76
  RuntimeHeaders["viaHandle"] = "viaHandle";
76
77
  })(RuntimeHeaders || (RuntimeHeaders = {}));
77
78
  const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
78
- // By default, we should reject any op larger than 768KB,
79
- // in order to account for some extra overhead from serialization
80
- // to not reach the 1MB limits in socket.io and Kafka.
81
- const defaultMaxOpSizeInBytes = 768000;
82
79
  const defaultFlushMode = FlushMode.TurnBased;
80
+ /**
81
+ * @deprecated - use ContainerRuntimeMessage instead
82
+ */
83
83
  export var RuntimeMessage;
84
84
  (function (RuntimeMessage) {
85
85
  RuntimeMessage["FluidDataStoreOp"] = "component";
@@ -90,12 +90,24 @@ export var RuntimeMessage;
90
90
  RuntimeMessage["Alias"] = "alias";
91
91
  RuntimeMessage["Operation"] = "op";
92
92
  })(RuntimeMessage || (RuntimeMessage = {}));
93
+ /**
94
+ * @deprecated - please use version in driver-utils
95
+ */
93
96
  export function isRuntimeMessage(message) {
94
97
  if (Object.values(RuntimeMessage).includes(message.type)) {
95
98
  return true;
96
99
  }
97
100
  return false;
98
101
  }
102
+ /**
103
+ * Unpacks runtime messages
104
+ *
105
+ * @remarks This API makes no promises regarding backward-compatability. This is internal API.
106
+ * @param message - message (as it observed in storage / service)
107
+ * @returns unpacked runtime message
108
+ *
109
+ * @internal
110
+ */
99
111
  export function unpackRuntimeMessage(message) {
100
112
  if (message.type === MessageType.Operation) {
101
113
  // legacy op format?
@@ -109,14 +121,15 @@ export function unpackRuntimeMessage(message) {
109
121
  message.type = innerContents.type;
110
122
  message.contents = innerContents.contents;
111
123
  }
112
- assert(isUnpackedRuntimeMessage(message), 0x122 /* "Message to unpack is not proper runtime message" */);
124
+ return true;
113
125
  }
114
126
  else {
115
127
  // Legacy format, but it's already "unpacked",
116
128
  // i.e. message.type is actually ContainerMessageType.
129
+ // Or it's non-runtime message.
117
130
  // Nothing to do in such case.
131
+ return false;
118
132
  }
119
- return message;
120
133
  }
121
134
  /**
122
135
  * Legacy ID for the built-in AgentScheduler. To minimize disruption while removing it, retaining this as a
@@ -157,7 +170,6 @@ export class ContainerRuntime extends TypedEventEmitter {
157
170
  this.summaryConfiguration = summaryConfiguration;
158
171
  this.defaultMaxConsecutiveReconnects = 7;
159
172
  this._orderSequentiallyCalls = 0;
160
- this.needsFlush = false;
161
173
  this.flushTrigger = false;
162
174
  this.savedOps = [];
163
175
  this.consecutiveReconnects = 0;
@@ -170,6 +182,12 @@ export class ContainerRuntime extends TypedEventEmitter {
170
182
  signalTimestamp: 0,
171
183
  trackingSignalSequenceNumber: undefined,
172
184
  };
185
+ // Provide lower soft limit - we want to have some number of ops to get efficiency in compression & bandwidth usage,
186
+ // but at the same time we want to send these ops sooner, to reduce overall latency of processing a batch.
187
+ // So there is some ballance here, that depends on compression algorithm and its efficiency working with smaller
188
+ // payloads. That number represents final (compressed) bits (once compression is implemented).
189
+ this.pendingAttachBatch = new BatchManager(64 * 1024);
190
+ this.pendingBatch = new BatchManager();
173
191
  this.summarizeOnDemand = (...args) => {
174
192
  if (this.clientDetails.type === summarizerClientType) {
175
193
  return this.summarizer.summarizeOnDemand(...args);
@@ -227,6 +245,8 @@ export class ContainerRuntime extends TypedEventEmitter {
227
245
  getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
228
246
  getLastSummaryTimestampMs: () => { var _a; return (_a = this.messageAtLastSummary) === null || _a === void 0 ? void 0 : _a.timestamp; },
229
247
  readAndParseBlob: async (id) => readAndParse(this.storage, id),
248
+ getContainerDiagnosticId: () => this.context.id,
249
+ activeConnection: () => this.deltaManager.active,
230
250
  });
231
251
  const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
232
252
  this.summarizerNode = createRootSummarizerNodeWithGC(ChildLogger.create(this.logger, "SummarizerNode"),
@@ -254,7 +274,7 @@ export class ContainerRuntime extends TypedEventEmitter {
254
274
  this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId, localId });
255
275
  }
256
276
  }, (blobPath) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"), this, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pendingAttachmentBlobs);
257
- this.scheduleManager = new ScheduleManager(context.deltaManager, this, ChildLogger.create(this.logger, "ScheduleManager"));
277
+ this.scheduleManager = new ScheduleManager(context.deltaManager, this, () => this.clientId, ChildLogger.create(this.logger, "ScheduleManager"));
258
278
  this.deltaSender = this.deltaManager;
259
279
  this.pendingStateManager = new PendingStateManager({
260
280
  applyStashedOp: this.applyStashedOp.bind(this),
@@ -264,7 +284,6 @@ export class ContainerRuntime extends TypedEventEmitter {
264
284
  flush: this.flush.bind(this),
265
285
  flushMode: () => this.flushMode,
266
286
  reSubmit: this.reSubmit.bind(this),
267
- rollback: this.rollback.bind(this),
268
287
  setFlushMode: (mode) => this.setFlushMode(mode),
269
288
  }, this._flushMode, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pending);
270
289
  this.context.quorum.on("removeMember", (clientId) => {
@@ -491,6 +510,9 @@ export class ContainerRuntime extends TypedEventEmitter {
491
510
  return (_a = this.summarizerClientElection) === null || _a === void 0 ? void 0 : _a.electedClientId;
492
511
  }
493
512
  get disposed() { return this._disposed; }
513
+ get emptyBatch() {
514
+ return this.pendingBatch.empty && this.pendingAttachBatch.empty;
515
+ }
494
516
  get summarizer() {
495
517
  assert(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
496
518
  return this._summarizer;
@@ -522,12 +544,9 @@ export class ContainerRuntime extends TypedEventEmitter {
522
544
  if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
523
545
  return true;
524
546
  }
525
- if (this.summaryConfiguration.state !== "disabled") {
526
- return this.summaryConfiguration.summarizerClientElection === true;
527
- }
528
- else {
529
- return false;
530
- }
547
+ return this.summaryConfiguration.state !== "disabled"
548
+ ? this.summaryConfiguration.summarizerClientElection === true
549
+ : false;
531
550
  }
532
551
  getMaxOpsSinceLastSummary() {
533
552
  // back-compat: maxOpsSinceLastSummary was moved from ISummaryRuntimeOptions
@@ -535,12 +554,9 @@ export class ContainerRuntime extends TypedEventEmitter {
535
554
  if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
536
555
  return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
537
556
  }
538
- if (this.summaryConfiguration.state !== "disabled") {
539
- return this.summaryConfiguration.maxOpsSinceLastSummary;
540
- }
541
- else {
542
- return 0;
543
- }
557
+ return this.summaryConfiguration.state !== "disabled"
558
+ ? this.summaryConfiguration.maxOpsSinceLastSummary
559
+ : 0;
544
560
  }
545
561
  getInitialSummarizerDelayMs() {
546
562
  // back-compat: initialSummarizerDelayMs was moved from ISummaryRuntimeOptions
@@ -548,12 +564,9 @@ export class ContainerRuntime extends TypedEventEmitter {
548
564
  if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
549
565
  return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
550
566
  }
551
- if (this.summaryConfiguration.state !== "disabled") {
552
- return this.summaryConfiguration.initialSummarizerDelayMs;
553
- }
554
- else {
555
- return 0;
556
- }
567
+ return this.summaryConfiguration.state !== "disabled"
568
+ ? this.summaryConfiguration.initialSummarizerDelayMs
569
+ : 0;
557
570
  }
558
571
  dispose(error) {
559
572
  var _a;
@@ -627,16 +640,12 @@ export class ContainerRuntime extends TypedEventEmitter {
627
640
  }
628
641
  if (id === BlobManager.basePath && requestParser.isLeaf(2)) {
629
642
  const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
630
- if (blob) {
631
- return {
643
+ return blob
644
+ ? {
632
645
  status: 200,
633
646
  mimeType: "fluid/object",
634
647
  value: blob,
635
- };
636
- }
637
- else {
638
- return create404Response(request);
639
- }
648
+ } : create404Response(request);
640
649
  }
641
650
  else if (requestParser.pathParts.length > 0) {
642
651
  const dataStore = await this.getDataStoreFromRequest(id, request);
@@ -736,7 +745,7 @@ export class ContainerRuntime extends TypedEventEmitter {
736
745
  // Feature disabled, we never stop reconnecting
737
746
  return true;
738
747
  }
739
- if (!this.pendingStateManager.hasPendingMessages()) {
748
+ if (!this.hasPendingMessages()) {
740
749
  // If there are no pending messages, we can always reconnect
741
750
  this.resetReconnectCount();
742
751
  return true;
@@ -841,13 +850,16 @@ export class ContainerRuntime extends TypedEventEmitter {
841
850
  this._perfSignalData.signalTimestamp = 0;
842
851
  this._perfSignalData.trackingSignalSequenceNumber = undefined;
843
852
  }
853
+ else {
854
+ assert(this.attachState === AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
855
+ }
844
856
  // Fail while disconnected
845
857
  if (reconnection) {
846
858
  this.consecutiveReconnects++;
847
859
  if (!this.shouldContinueReconnecting()) {
848
- this.closeFn(
849
- // pre-0.58 error message: MaxReconnectsWithNoProgress
850
- DataProcessingError.create("Runtime detected too many reconnects with no progress syncing local ops", "setConnectionState", undefined, {
860
+ this.closeFn(DataProcessingError.create(
861
+ // eslint-disable-next-line max-len
862
+ "Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)", "setConnectionState", undefined, {
851
863
  dataLoss: 1,
852
864
  attempts: this.consecutiveReconnects,
853
865
  pendingMessages: this.pendingStateManager.pendingMessagesCount,
@@ -859,46 +871,48 @@ export class ContainerRuntime extends TypedEventEmitter {
859
871
  this.replayPendingStates();
860
872
  }
861
873
  this.dataStores.setConnectionState(connected, clientId);
874
+ this.garbageCollector.setConnectionState(connected, clientId);
862
875
  raiseConnectedEvent(this.mc.logger, this, connected, clientId);
863
876
  }
864
877
  process(messageArg, local) {
865
878
  var _a;
866
879
  this.verifyNotClosed();
867
- // If it's not message for runtime, bail out right away.
868
- if (!isUnpackedRuntimeMessage(messageArg)) {
869
- return;
870
- }
871
- if ((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad) {
872
- this.savedOps.push(messageArg);
873
- }
874
880
  // Do shallow copy of message, as methods below will modify it.
875
881
  // There might be multiple container instances receiving same message
876
882
  // We do not need to make deep copy, as each layer will just replace message.content itself,
877
883
  // but would not modify contents details
878
884
  let message = Object.assign({}, messageArg);
885
+ // back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
886
+ // System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
887
+ // Old ops may contain empty string (I assume noops).
888
+ if (typeof message.contents === "string" && message.contents !== "") {
889
+ message.contents = JSON.parse(message.contents);
890
+ }
891
+ // Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
892
+ // This format was not shipped to production workflows.
893
+ const runtimeMessage = unpackRuntimeMessage(message);
894
+ if ((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad) {
895
+ this.savedOps.push(messageArg);
896
+ }
879
897
  // Surround the actual processing of the operation with messages to the schedule manager indicating
880
898
  // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
881
899
  // messages once a batch has been fully processed.
882
900
  this.scheduleManager.beforeOpProcessing(message);
883
901
  try {
884
- message = unpackRuntimeMessage(message);
885
902
  // Chunk processing must come first given that we will transform the message to the unchunked version
886
903
  // once all pieces are available
887
904
  message = this.processRemoteChunkedMessage(message);
888
905
  let localOpMetadata;
889
- if (local) {
890
- // Call the PendingStateManager to process local messages.
891
- // Do not process local chunked ops until all pieces are available.
892
- if (message.type !== ContainerMessageType.ChunkedOp) {
893
- localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
894
- }
906
+ if (local && runtimeMessage) {
907
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
895
908
  }
896
909
  // If there are no more pending messages after processing a local message,
897
910
  // the document is no longer dirty.
898
- if (!this.pendingStateManager.hasPendingMessages()) {
911
+ if (!this.hasPendingMessages()) {
899
912
  this.updateDocumentDirtyState(false);
900
913
  }
901
- switch (message.type) {
914
+ const type = message.type;
915
+ switch (type) {
902
916
  case ContainerMessageType.Attach:
903
917
  this.dataStores.processAttachMessage(message, local);
904
918
  break;
@@ -911,9 +925,16 @@ export class ContainerRuntime extends TypedEventEmitter {
911
925
  case ContainerMessageType.BlobAttach:
912
926
  this.blobManager.processBlobAttachOp(message, local);
913
927
  break;
928
+ case ContainerMessageType.ChunkedOp:
929
+ case ContainerMessageType.Rejoin:
930
+ break;
914
931
  default:
932
+ assert(!runtimeMessage, 0x3ce /* Runtime message of unknown type */);
933
+ }
934
+ // For back-compat, notify only about runtime messages for now.
935
+ if (runtimeMessage) {
936
+ this.emit("op", message, runtimeMessage);
915
937
  }
916
- this.emit("op", message);
917
938
  this.scheduleManager.afterOpProcessing(undefined, message);
918
939
  if (local) {
919
940
  // If we have processed a local op, this means that the container is
@@ -1007,25 +1028,57 @@ export class ContainerRuntime extends TypedEventEmitter {
1007
1028
  }
1008
1029
  flush() {
1009
1030
  assert(this._orderSequentiallyCalls === 0, 0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
1010
- if (!this.deltaSender) {
1011
- return;
1012
- }
1013
- // Let the PendingStateManager know that there was an attempt to flush messages.
1014
- // Note that this should happen before the `this.needsFlush` check below because in the scenario where we are
1015
- // not connected, `this.needsFlush` will be false but the PendingStateManager might have pending messages and
1016
- // hence needs to track this.
1017
- this.pendingStateManager.onFlush();
1018
- // If flush has already been called then exit early
1019
- if (!this.needsFlush) {
1020
- return;
1031
+ this.flushBatch(this.pendingAttachBatch.popBatch());
1032
+ this.flushBatch(this.pendingBatch.popBatch());
1033
+ assert(this.emptyBatch, 0x3cf /* reentrancy */);
1034
+ }
1035
+ flushBatch(batch) {
1036
+ const length = batch.length;
1037
+ if (length > 1) {
1038
+ batch[0].metadata = Object.assign(Object.assign({}, batch[0].metadata), { batch: true });
1039
+ batch[length - 1].metadata = Object.assign(Object.assign({}, batch[length - 1].metadata), { batch: false });
1040
+ // This assert fires for the following reason (there might be more cases like that):
1041
+ // AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
1042
+ // i.e. in the middle of op processing!
1043
+ // Sending ops while processing ops is not good idea - it's not defined when
1044
+ // referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
1045
+ // If we send ops in response to processing multiple ops, then we for sure hit this assert!
1046
+ // Tracked via ADO #1834
1047
+ // assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
1048
+ // "Batch should be generated synchronously, without processing ops in the middle!");
1021
1049
  }
1022
- this.needsFlush = false;
1050
+ let clientSequenceNumber = -1;
1023
1051
  // Did we disconnect in the middle of turn-based batch?
1024
1052
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
1025
- if (!this.canSendOps()) {
1026
- return;
1053
+ if (this.canSendOps()) {
1054
+ if (this.context.submitBatchFn !== undefined) {
1055
+ const batchToSend = [];
1056
+ for (const message of batch) {
1057
+ batchToSend.push({ contents: message.contents, metadata: message.metadata });
1058
+ }
1059
+ // returns clientSequenceNumber of last message in a batch
1060
+ clientSequenceNumber = this.context.submitBatchFn(batchToSend);
1061
+ }
1062
+ else {
1063
+ // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
1064
+ // version that has support for batches (submitBatchFn)
1065
+ for (const message of batch) {
1066
+ clientSequenceNumber = this.context.submitFn(MessageType.Operation, message.deserializedContent, true, // batch
1067
+ message.metadata);
1068
+ }
1069
+ this.deltaSender.flush();
1070
+ }
1071
+ // Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
1072
+ clientSequenceNumber -= batch.length - 1;
1073
+ assert(clientSequenceNumber >= 0, 0x3d0 /* clientSequenceNumber can't be negative */);
1074
+ }
1075
+ // Let the PendingStateManager know that a message was submitted.
1076
+ // In future, need to shift toward keeping batch as a whole!
1077
+ for (const message of batch) {
1078
+ this.pendingStateManager.onSubmitMessage(message.deserializedContent.type, clientSequenceNumber, message.referenceSequenceNumber, message.deserializedContent.contents, message.localOpMetadata, message.metadata);
1079
+ clientSequenceNumber++;
1027
1080
  }
1028
- return this.deltaSender.flush();
1081
+ this.pendingStateManager.onFlush();
1029
1082
  }
1030
1083
  orderSequentially(callback) {
1031
1084
  // If flush mode is already TurnBased we are either
@@ -1050,7 +1103,10 @@ export class ContainerRuntime extends TypedEventEmitter {
1050
1103
  trackOrderSequentiallyCalls(callback) {
1051
1104
  let checkpoint;
1052
1105
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
1053
- checkpoint = this.pendingStateManager.checkpoint();
1106
+ // Note: we are not touching this.pendingAttachBatch here, for two reasons:
1107
+ // 1. It would not help, as we flush attach ops as they become available.
1108
+ // 2. There is no way to undo process of data store creation.
1109
+ checkpoint = this.pendingBatch.checkpoint();
1054
1110
  }
1055
1111
  try {
1056
1112
  this._orderSequentiallyCalls++;
@@ -1059,7 +1115,16 @@ export class ContainerRuntime extends TypedEventEmitter {
1059
1115
  catch (error) {
1060
1116
  if (checkpoint) {
1061
1117
  // This will throw and close the container if rollback fails
1062
- checkpoint.rollback();
1118
+ try {
1119
+ checkpoint.rollback((message) => this.rollback(message.deserializedContent.type, message.deserializedContent.contents, message.localOpMetadata));
1120
+ }
1121
+ catch (err) {
1122
+ const error2 = wrapError(err, (message) => {
1123
+ return DataProcessingError.create(`RollbackError: ${message}`, "checkpointRollback", undefined);
1124
+ });
1125
+ this.closeFn(error2);
1126
+ throw error2;
1127
+ }
1063
1128
  }
1064
1129
  else {
1065
1130
  // pre-0.58 error message: orderSequentiallyCallbackException
@@ -1164,7 +1229,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1164
1229
  assert(this.attachState === AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
1165
1230
  this.emit("attached");
1166
1231
  }
1167
- if (attachState === AttachState.Attached && !this.pendingStateManager.hasPendingMessages()) {
1232
+ if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
1168
1233
  this.updateDocumentDirtyState(false);
1169
1234
  }
1170
1235
  this.dataStores.setAttachState(attachState);
@@ -1328,7 +1393,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1328
1393
  }
1329
1394
  /**
1330
1395
  * Runs garbage collection and updates the reference / used state of the nodes in the container.
1331
- * @returns the statistics of the garbage collection run.
1396
+ * @returns the statistics of the garbage collection run; undefined if GC did not run.
1332
1397
  */
1333
1398
  async collectGarbage(options) {
1334
1399
  return this.garbageCollector.collectGarbage(options);
@@ -1359,6 +1424,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1359
1424
  const summaryNumberLogger = ChildLogger.create(summaryLogger, undefined, {
1360
1425
  all: { summaryNumber },
1361
1426
  });
1427
+ assert(this.emptyBatch, 0x3d1 /* Can't trigger summary in the middle of a batch */);
1362
1428
  let latestSnapshotVersionId;
1363
1429
  if (refreshLatestAck) {
1364
1430
  const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(ChildLogger.create(summaryNumberLogger, undefined, { all: { safeSummary: true } }));
@@ -1433,7 +1499,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1433
1499
  const forcedFullTree = this.garbageCollector.summaryStateNeedsReset;
1434
1500
  try {
1435
1501
  summarizeResult = await this.summarize({
1436
- fullTree: fullTree || forcedFullTree,
1502
+ fullTree: fullTree !== null && fullTree !== void 0 ? fullTree : forcedFullTree,
1437
1503
  trackState: true,
1438
1504
  summaryLogger: summaryNumberLogger,
1439
1505
  runGC: this.garbageCollector.shouldRunGC,
@@ -1522,7 +1588,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1522
1588
  }
1523
1589
  let clientSequenceNumber;
1524
1590
  try {
1525
- clientSequenceNumber = this.submitSystemMessage(MessageType.Summarize, summaryMessage);
1591
+ clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
1526
1592
  }
1527
1593
  catch (error) {
1528
1594
  return Object.assign(Object.assign({ stage: "upload" }, uploadData), { error });
@@ -1570,7 +1636,17 @@ export class ContainerRuntime extends TypedEventEmitter {
1570
1636
  this.chunkMap.delete(clientId);
1571
1637
  }
1572
1638
  }
1639
+ hasPendingMessages() {
1640
+ return this.pendingStateManager.hasPendingMessages() || !this.emptyBatch;
1641
+ }
1573
1642
  updateDocumentDirtyState(dirty) {
1643
+ if (this.attachState !== AttachState.Attached) {
1644
+ assert(dirty, 0x3d2 /* Non-attached container is dirty */);
1645
+ }
1646
+ else {
1647
+ // Other way is not true = see this.isContainerMessageDirtyable()
1648
+ assert(!dirty || this.hasPendingMessages(), 0x3d3 /* if doc is dirty, there has to be pending ops */);
1649
+ }
1574
1650
  if (this.dirtyContainer === dirty) {
1575
1651
  return;
1576
1652
  }
@@ -1598,64 +1674,100 @@ export class ContainerRuntime extends TypedEventEmitter {
1598
1674
  this.verifyNotClosed();
1599
1675
  return this.blobManager.createBlob(blob);
1600
1676
  }
1601
- submit(type, content, localOpMetadata = undefined, opMetadata = undefined) {
1677
+ submit(type, contents, localOpMetadata = undefined, metadata = undefined) {
1602
1678
  this.verifyNotClosed();
1603
1679
  // There should be no ops in detached container state!
1604
1680
  assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
1605
- let clientSequenceNumber = -1;
1606
- let opMetadataInternal = opMetadata;
1607
- if (this.canSendOps()) {
1608
- const serializedContent = JSON.stringify(content);
1609
- // If in TurnBased flush mode we will trigger a flush at the next turn break
1610
- if (this.flushMode === FlushMode.TurnBased && !this.needsFlush) {
1611
- opMetadataInternal = Object.assign(Object.assign({}, opMetadata), { batch: true });
1612
- this.needsFlush = true;
1613
- // Use Promise.resolve().then() to queue a microtask to detect the end of the turn and force a flush.
1614
- if (!this.flushTrigger) {
1615
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1616
- Promise.resolve().then(() => {
1617
- this.flushTrigger = false;
1618
- this.flush();
1681
+ const deserializedContent = { type, contents };
1682
+ const serializedContent = JSON.stringify(deserializedContent);
1683
+ if (this.deltaManager.readOnlyInfo.readonly) {
1684
+ this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
1685
+ }
1686
+ const message = {
1687
+ contents: serializedContent,
1688
+ deserializedContent,
1689
+ metadata,
1690
+ localOpMetadata,
1691
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
1692
+ };
1693
+ try {
1694
+ // If this is attach message for new data store, and we are in a batch, send this op out of order
1695
+ // Is it safe:
1696
+ // Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
1697
+ // They become visible only when aliased, or handle to some sub-element of newly created datastore
1698
+ // is stored in some DDS, i.e. only after some other op.
1699
+ // Why:
1700
+ // Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
1701
+ // stores are created, causing issues like relay service throttling (too many ops) and catastrophic
1702
+ // failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
1703
+ // these issues.
1704
+ // Cons:
1705
+ // 1. With large batches, relay service may throttle clients. Clients may disconnect while throttled.
1706
+ // This change creates new possibility of a lot of newly created data stores never being referenced
1707
+ // because client died before it had a change to submit the rest of the ops. This will create more
1708
+ // garbage that needs to be collected leveraging GC (Garbage Collection) feature.
1709
+ // 2. Sending ops out of order means they are excluded from rollback functionality. This is not an issue
1710
+ // today as rollback can't undo creation of data store. To some extent not sending them is a bigger
1711
+ // issue than sending.
1712
+ // Please note that this does not change file format, so it can be disabled in the future if this
1713
+ // optimization no longer makes sense (for example, batch compression may make it less appealing).
1714
+ if (type === ContainerMessageType.Attach &&
1715
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
1716
+ if (!this.pendingAttachBatch.push(message)) {
1717
+ // BatchManager has two limits - soft limit & hard limit. Soft limit is only engaged
1718
+ // when queue is not empty.
1719
+ // Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
1720
+ this.flushBatch(this.pendingAttachBatch.popBatch());
1721
+ if (!this.pendingAttachBatch.push(message)) {
1722
+ throw new GenericError("BatchTooLarge",
1723
+ /* error */ undefined, {
1724
+ opSize: message.contents.length,
1725
+ count: this.pendingAttachBatch.length,
1726
+ limit: this.pendingAttachBatch.limit,
1727
+ });
1728
+ }
1729
+ }
1730
+ }
1731
+ else {
1732
+ if (!this.pendingBatch.push(message)) {
1733
+ throw new GenericError("BatchTooLarge",
1734
+ /* error */ undefined, {
1735
+ opSize: message.contents.length,
1736
+ count: this.pendingBatch.length,
1737
+ limit: this.pendingBatch.limit,
1619
1738
  });
1620
1739
  }
1621
1740
  }
1622
- if (!serializedContent || serializedContent.length <= defaultMaxOpSizeInBytes) {
1623
- clientSequenceNumber = this.submitRuntimeMessage(type, content, this._flushMode === FlushMode.TurnBased /* batch */, opMetadataInternal);
1741
+ if (this._flushMode !== FlushMode.TurnBased) {
1742
+ this.flush();
1624
1743
  }
1625
- else {
1626
- // If the content length is larger than the client configured message size
1627
- // instead of splitting the content, we will fail by explicitly closing the container
1628
- this.closeFn(new GenericError("OpTooLarge",
1629
- /* error */ undefined, {
1630
- length: serializedContent.length,
1631
- limit: defaultMaxOpSizeInBytes,
1632
- }));
1633
- clientSequenceNumber = -1;
1744
+ else if (!this.flushTrigger) {
1745
+ this.flushTrigger = true;
1746
+ // Queue a microtask to detect the end of the turn and force a flush.
1747
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1748
+ Promise.resolve().then(() => {
1749
+ this.flushTrigger = false;
1750
+ this.flush();
1751
+ });
1634
1752
  }
1635
1753
  }
1636
- // Let the PendingStateManager know that a message was submitted.
1637
- this.pendingStateManager.onSubmitMessage(type, clientSequenceNumber, this.deltaManager.lastSequenceNumber, content, localOpMetadata, opMetadataInternal);
1638
- if (this.isContainerMessageDirtyable(type, content)) {
1754
+ catch (error) {
1755
+ this.closeFn(error);
1756
+ throw error;
1757
+ }
1758
+ if (this.isContainerMessageDirtyable(type, contents)) {
1639
1759
  this.updateDocumentDirtyState(true);
1640
1760
  }
1641
1761
  }
1642
- submitSystemMessage(type, contents) {
1762
+ submitSummaryMessage(contents) {
1643
1763
  this.verifyNotClosed();
1644
1764
  assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
1645
1765
  // System message should not be sent in the middle of the batch.
1646
- // That said, we can preserve existing behavior by not flushing existing buffer.
1647
- // That might be not what caller hopes to get, but we can look deeper if telemetry tells us it's a problem.
1648
- const middleOfBatch = this.flushMode === FlushMode.TurnBased && this.needsFlush;
1649
- if (middleOfBatch) {
1650
- this.mc.logger.sendErrorEvent({ eventName: "submitSystemMessageError", type });
1651
- }
1652
- return this.context.submitFn(type, contents, middleOfBatch);
1653
- }
1654
- submitRuntimeMessage(type, contents, batch, appData) {
1655
- this.verifyNotClosed();
1656
- assert(this.connected, 0x259 /* "Container disconnected when trying to submit system message" */);
1657
- const payload = { type, contents };
1658
- return this.context.submitFn(MessageType.Operation, payload, batch, appData);
1766
+ assert(this.emptyBatch, 0x3d4 /* System op in the middle of a batch */);
1767
+ // back-compat: ADO #1385: Make this call unconditional in the future
1768
+ return this.context.submitSummaryFn !== undefined
1769
+ ? this.context.submitSummaryFn(contents)
1770
+ : this.context.submitFn(MessageType.Summarize, contents, false);
1659
1771
  }
1660
1772
  /**
1661
1773
  * Throw an error if the runtime is closed. Methods that are expected to potentially
@@ -1776,6 +1888,10 @@ export class ContainerRuntime extends TypedEventEmitter {
1776
1888
  if (!((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad)) {
1777
1889
  throw new UsageError("can't get state when offline load disabled");
1778
1890
  }
1891
+ // Flush pending batch.
1892
+ // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
1893
+ // to close current batch.
1894
+ this.flush();
1779
1895
  const previousPendingState = this.context.pendingLocalState;
1780
1896
  if (previousPendingState) {
1781
1897
  return {
@@ -1830,6 +1946,11 @@ export class ContainerRuntime extends TypedEventEmitter {
1830
1946
  // we may not have seen every sequence number (because of system ops) so apply everything once we
1831
1947
  // don't have any more saved ops
1832
1948
  await this.pendingStateManager.applyStashedOpsAt();
1949
+ // If it's not the case, we should take it into account when calculating dirty state.
1950
+ assert(this.context.attachState === AttachState.Attached, 0x3d5 /* this function is called for attached containers only */);
1951
+ if (!this.hasPendingMessages()) {
1952
+ this.updateDocumentDirtyState(false);
1953
+ }
1833
1954
  }
1834
1955
  validateSummaryHeuristicConfiguration(configuration) {
1835
1956
  // eslint-disable-next-line no-restricted-syntax