@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
@@ -18,6 +18,7 @@ const summarizer_1 = require("./summarizer");
18
18
  const summaryManager_1 = require("./summaryManager");
19
19
  const connectionTelemetry_1 = require("./connectionTelemetry");
20
20
  const pendingStateManager_1 = require("./pendingStateManager");
21
+ const batchManager_1 = require("./batchManager");
21
22
  const packageVersion_1 = require("./packageVersion");
22
23
  const blobManager_1 = require("./blobManager");
23
24
  const dataStores_1 = require("./dataStores");
@@ -78,11 +79,10 @@ var RuntimeHeaders;
78
79
  RuntimeHeaders["viaHandle"] = "viaHandle";
79
80
  })(RuntimeHeaders = exports.RuntimeHeaders || (exports.RuntimeHeaders = {}));
80
81
  const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
81
- // By default, we should reject any op larger than 768KB,
82
- // in order to account for some extra overhead from serialization
83
- // to not reach the 1MB limits in socket.io and Kafka.
84
- const defaultMaxOpSizeInBytes = 768000;
85
82
  const defaultFlushMode = runtime_definitions_1.FlushMode.TurnBased;
83
+ /**
84
+ * @deprecated - use ContainerRuntimeMessage instead
85
+ */
86
86
  var RuntimeMessage;
87
87
  (function (RuntimeMessage) {
88
88
  RuntimeMessage["FluidDataStoreOp"] = "component";
@@ -93,6 +93,9 @@ var RuntimeMessage;
93
93
  RuntimeMessage["Alias"] = "alias";
94
94
  RuntimeMessage["Operation"] = "op";
95
95
  })(RuntimeMessage = exports.RuntimeMessage || (exports.RuntimeMessage = {}));
96
+ /**
97
+ * @deprecated - please use version in driver-utils
98
+ */
96
99
  function isRuntimeMessage(message) {
97
100
  if (Object.values(RuntimeMessage).includes(message.type)) {
98
101
  return true;
@@ -100,6 +103,15 @@ function isRuntimeMessage(message) {
100
103
  return false;
101
104
  }
102
105
  exports.isRuntimeMessage = isRuntimeMessage;
106
+ /**
107
+ * Unpacks runtime messages
108
+ *
109
+ * @remarks This API makes no promises regarding backward-compatability. This is internal API.
110
+ * @param message - message (as it observed in storage / service)
111
+ * @returns unpacked runtime message
112
+ *
113
+ * @internal
114
+ */
103
115
  function unpackRuntimeMessage(message) {
104
116
  if (message.type === protocol_definitions_1.MessageType.Operation) {
105
117
  // legacy op format?
@@ -113,14 +125,15 @@ function unpackRuntimeMessage(message) {
113
125
  message.type = innerContents.type;
114
126
  message.contents = innerContents.contents;
115
127
  }
116
- (0, common_utils_1.assert)((0, driver_utils_1.isUnpackedRuntimeMessage)(message), 0x122 /* "Message to unpack is not proper runtime message" */);
128
+ return true;
117
129
  }
118
130
  else {
119
131
  // Legacy format, but it's already "unpacked",
120
132
  // i.e. message.type is actually ContainerMessageType.
133
+ // Or it's non-runtime message.
121
134
  // Nothing to do in such case.
135
+ return false;
122
136
  }
123
- return message;
124
137
  }
125
138
  exports.unpackRuntimeMessage = unpackRuntimeMessage;
126
139
  /**
@@ -163,7 +176,6 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
163
176
  this.summaryConfiguration = summaryConfiguration;
164
177
  this.defaultMaxConsecutiveReconnects = 7;
165
178
  this._orderSequentiallyCalls = 0;
166
- this.needsFlush = false;
167
179
  this.flushTrigger = false;
168
180
  this.savedOps = [];
169
181
  this.consecutiveReconnects = 0;
@@ -176,6 +188,12 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
176
188
  signalTimestamp: 0,
177
189
  trackingSignalSequenceNumber: undefined,
178
190
  };
191
+ // Provide lower soft limit - we want to have some number of ops to get efficiency in compression & bandwidth usage,
192
+ // but at the same time we want to send these ops sooner, to reduce overall latency of processing a batch.
193
+ // So there is some ballance here, that depends on compression algorithm and its efficiency working with smaller
194
+ // payloads. That number represents final (compressed) bits (once compression is implemented).
195
+ this.pendingAttachBatch = new batchManager_1.BatchManager(64 * 1024);
196
+ this.pendingBatch = new batchManager_1.BatchManager();
179
197
  this.summarizeOnDemand = (...args) => {
180
198
  if (this.clientDetails.type === summarizerClientElection_1.summarizerClientType) {
181
199
  return this.summarizer.summarizeOnDemand(...args);
@@ -233,6 +251,8 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
233
251
  getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
234
252
  getLastSummaryTimestampMs: () => { var _a; return (_a = this.messageAtLastSummary) === null || _a === void 0 ? void 0 : _a.timestamp; },
235
253
  readAndParseBlob: async (id) => (0, driver_utils_1.readAndParse)(this.storage, id),
254
+ getContainerDiagnosticId: () => this.context.id,
255
+ activeConnection: () => this.deltaManager.active,
236
256
  });
237
257
  const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
238
258
  this.summarizerNode = (0, runtime_utils_1.createRootSummarizerNodeWithGC)(telemetry_utils_1.ChildLogger.create(this.logger, "SummarizerNode"),
@@ -260,7 +280,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
260
280
  this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId, localId });
261
281
  }
262
282
  }, (blobPath) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"), this, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pendingAttachmentBlobs);
263
- this.scheduleManager = new scheduleManager_1.ScheduleManager(context.deltaManager, this, telemetry_utils_1.ChildLogger.create(this.logger, "ScheduleManager"));
283
+ this.scheduleManager = new scheduleManager_1.ScheduleManager(context.deltaManager, this, () => this.clientId, telemetry_utils_1.ChildLogger.create(this.logger, "ScheduleManager"));
264
284
  this.deltaSender = this.deltaManager;
265
285
  this.pendingStateManager = new pendingStateManager_1.PendingStateManager({
266
286
  applyStashedOp: this.applyStashedOp.bind(this),
@@ -270,7 +290,6 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
270
290
  flush: this.flush.bind(this),
271
291
  flushMode: () => this.flushMode,
272
292
  reSubmit: this.reSubmit.bind(this),
273
- rollback: this.rollback.bind(this),
274
293
  setFlushMode: (mode) => this.setFlushMode(mode),
275
294
  }, this._flushMode, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pending);
276
295
  this.context.quorum.on("removeMember", (clientId) => {
@@ -497,6 +516,9 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
497
516
  return (_a = this.summarizerClientElection) === null || _a === void 0 ? void 0 : _a.electedClientId;
498
517
  }
499
518
  get disposed() { return this._disposed; }
519
+ get emptyBatch() {
520
+ return this.pendingBatch.empty && this.pendingAttachBatch.empty;
521
+ }
500
522
  get summarizer() {
501
523
  (0, common_utils_1.assert)(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
502
524
  return this._summarizer;
@@ -528,12 +550,9 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
528
550
  if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
529
551
  return true;
530
552
  }
531
- if (this.summaryConfiguration.state !== "disabled") {
532
- return this.summaryConfiguration.summarizerClientElection === true;
533
- }
534
- else {
535
- return false;
536
- }
553
+ return this.summaryConfiguration.state !== "disabled"
554
+ ? this.summaryConfiguration.summarizerClientElection === true
555
+ : false;
537
556
  }
538
557
  getMaxOpsSinceLastSummary() {
539
558
  // back-compat: maxOpsSinceLastSummary was moved from ISummaryRuntimeOptions
@@ -541,12 +560,9 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
541
560
  if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
542
561
  return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
543
562
  }
544
- if (this.summaryConfiguration.state !== "disabled") {
545
- return this.summaryConfiguration.maxOpsSinceLastSummary;
546
- }
547
- else {
548
- return 0;
549
- }
563
+ return this.summaryConfiguration.state !== "disabled"
564
+ ? this.summaryConfiguration.maxOpsSinceLastSummary
565
+ : 0;
550
566
  }
551
567
  getInitialSummarizerDelayMs() {
552
568
  // back-compat: initialSummarizerDelayMs was moved from ISummaryRuntimeOptions
@@ -554,12 +570,9 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
554
570
  if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
555
571
  return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
556
572
  }
557
- if (this.summaryConfiguration.state !== "disabled") {
558
- return this.summaryConfiguration.initialSummarizerDelayMs;
559
- }
560
- else {
561
- return 0;
562
- }
573
+ return this.summaryConfiguration.state !== "disabled"
574
+ ? this.summaryConfiguration.initialSummarizerDelayMs
575
+ : 0;
563
576
  }
564
577
  dispose(error) {
565
578
  var _a;
@@ -633,16 +646,12 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
633
646
  }
634
647
  if (id === blobManager_1.BlobManager.basePath && requestParser.isLeaf(2)) {
635
648
  const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
636
- if (blob) {
637
- return {
649
+ return blob
650
+ ? {
638
651
  status: 200,
639
652
  mimeType: "fluid/object",
640
653
  value: blob,
641
- };
642
- }
643
- else {
644
- return (0, runtime_utils_1.create404Response)(request);
645
- }
654
+ } : (0, runtime_utils_1.create404Response)(request);
646
655
  }
647
656
  else if (requestParser.pathParts.length > 0) {
648
657
  const dataStore = await this.getDataStoreFromRequest(id, request);
@@ -742,7 +751,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
742
751
  // Feature disabled, we never stop reconnecting
743
752
  return true;
744
753
  }
745
- if (!this.pendingStateManager.hasPendingMessages()) {
754
+ if (!this.hasPendingMessages()) {
746
755
  // If there are no pending messages, we can always reconnect
747
756
  this.resetReconnectCount();
748
757
  return true;
@@ -847,13 +856,16 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
847
856
  this._perfSignalData.signalTimestamp = 0;
848
857
  this._perfSignalData.trackingSignalSequenceNumber = undefined;
849
858
  }
859
+ else {
860
+ (0, common_utils_1.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
861
+ }
850
862
  // Fail while disconnected
851
863
  if (reconnection) {
852
864
  this.consecutiveReconnects++;
853
865
  if (!this.shouldContinueReconnecting()) {
854
- this.closeFn(
855
- // pre-0.58 error message: MaxReconnectsWithNoProgress
856
- container_utils_1.DataProcessingError.create("Runtime detected too many reconnects with no progress syncing local ops", "setConnectionState", undefined, {
866
+ this.closeFn(container_utils_1.DataProcessingError.create(
867
+ // eslint-disable-next-line max-len
868
+ "Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)", "setConnectionState", undefined, {
857
869
  dataLoss: 1,
858
870
  attempts: this.consecutiveReconnects,
859
871
  pendingMessages: this.pendingStateManager.pendingMessagesCount,
@@ -865,46 +877,48 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
865
877
  this.replayPendingStates();
866
878
  }
867
879
  this.dataStores.setConnectionState(connected, clientId);
880
+ this.garbageCollector.setConnectionState(connected, clientId);
868
881
  (0, telemetry_utils_1.raiseConnectedEvent)(this.mc.logger, this, connected, clientId);
869
882
  }
870
883
  process(messageArg, local) {
871
884
  var _a;
872
885
  this.verifyNotClosed();
873
- // If it's not message for runtime, bail out right away.
874
- if (!(0, driver_utils_1.isUnpackedRuntimeMessage)(messageArg)) {
875
- return;
876
- }
877
- if ((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad) {
878
- this.savedOps.push(messageArg);
879
- }
880
886
  // Do shallow copy of message, as methods below will modify it.
881
887
  // There might be multiple container instances receiving same message
882
888
  // We do not need to make deep copy, as each layer will just replace message.content itself,
883
889
  // but would not modify contents details
884
890
  let message = Object.assign({}, messageArg);
891
+ // back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
892
+ // System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
893
+ // Old ops may contain empty string (I assume noops).
894
+ if (typeof message.contents === "string" && message.contents !== "") {
895
+ message.contents = JSON.parse(message.contents);
896
+ }
897
+ // Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
898
+ // This format was not shipped to production workflows.
899
+ const runtimeMessage = unpackRuntimeMessage(message);
900
+ if ((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad) {
901
+ this.savedOps.push(messageArg);
902
+ }
885
903
  // Surround the actual processing of the operation with messages to the schedule manager indicating
886
904
  // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
887
905
  // messages once a batch has been fully processed.
888
906
  this.scheduleManager.beforeOpProcessing(message);
889
907
  try {
890
- message = unpackRuntimeMessage(message);
891
908
  // Chunk processing must come first given that we will transform the message to the unchunked version
892
909
  // once all pieces are available
893
910
  message = this.processRemoteChunkedMessage(message);
894
911
  let localOpMetadata;
895
- if (local) {
896
- // Call the PendingStateManager to process local messages.
897
- // Do not process local chunked ops until all pieces are available.
898
- if (message.type !== ContainerMessageType.ChunkedOp) {
899
- localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
900
- }
912
+ if (local && runtimeMessage) {
913
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
901
914
  }
902
915
  // If there are no more pending messages after processing a local message,
903
916
  // the document is no longer dirty.
904
- if (!this.pendingStateManager.hasPendingMessages()) {
917
+ if (!this.hasPendingMessages()) {
905
918
  this.updateDocumentDirtyState(false);
906
919
  }
907
- switch (message.type) {
920
+ const type = message.type;
921
+ switch (type) {
908
922
  case ContainerMessageType.Attach:
909
923
  this.dataStores.processAttachMessage(message, local);
910
924
  break;
@@ -917,9 +931,16 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
917
931
  case ContainerMessageType.BlobAttach:
918
932
  this.blobManager.processBlobAttachOp(message, local);
919
933
  break;
934
+ case ContainerMessageType.ChunkedOp:
935
+ case ContainerMessageType.Rejoin:
936
+ break;
920
937
  default:
938
+ (0, common_utils_1.assert)(!runtimeMessage, 0x3ce /* Runtime message of unknown type */);
939
+ }
940
+ // For back-compat, notify only about runtime messages for now.
941
+ if (runtimeMessage) {
942
+ this.emit("op", message, runtimeMessage);
921
943
  }
922
- this.emit("op", message);
923
944
  this.scheduleManager.afterOpProcessing(undefined, message);
924
945
  if (local) {
925
946
  // If we have processed a local op, this means that the container is
@@ -1013,25 +1034,57 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1013
1034
  }
1014
1035
  flush() {
1015
1036
  (0, common_utils_1.assert)(this._orderSequentiallyCalls === 0, 0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
1016
- if (!this.deltaSender) {
1017
- return;
1018
- }
1019
- // Let the PendingStateManager know that there was an attempt to flush messages.
1020
- // Note that this should happen before the `this.needsFlush` check below because in the scenario where we are
1021
- // not connected, `this.needsFlush` will be false but the PendingStateManager might have pending messages and
1022
- // hence needs to track this.
1023
- this.pendingStateManager.onFlush();
1024
- // If flush has already been called then exit early
1025
- if (!this.needsFlush) {
1026
- return;
1037
+ this.flushBatch(this.pendingAttachBatch.popBatch());
1038
+ this.flushBatch(this.pendingBatch.popBatch());
1039
+ (0, common_utils_1.assert)(this.emptyBatch, 0x3cf /* reentrancy */);
1040
+ }
1041
+ flushBatch(batch) {
1042
+ const length = batch.length;
1043
+ if (length > 1) {
1044
+ batch[0].metadata = Object.assign(Object.assign({}, batch[0].metadata), { batch: true });
1045
+ batch[length - 1].metadata = Object.assign(Object.assign({}, batch[length - 1].metadata), { batch: false });
1046
+ // This assert fires for the following reason (there might be more cases like that):
1047
+ // AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
1048
+ // i.e. in the middle of op processing!
1049
+ // Sending ops while processing ops is not good idea - it's not defined when
1050
+ // referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
1051
+ // If we send ops in response to processing multiple ops, then we for sure hit this assert!
1052
+ // Tracked via ADO #1834
1053
+ // assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
1054
+ // "Batch should be generated synchronously, without processing ops in the middle!");
1027
1055
  }
1028
- this.needsFlush = false;
1056
+ let clientSequenceNumber = -1;
1029
1057
  // Did we disconnect in the middle of turn-based batch?
1030
1058
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
1031
- if (!this.canSendOps()) {
1032
- return;
1059
+ if (this.canSendOps()) {
1060
+ if (this.context.submitBatchFn !== undefined) {
1061
+ const batchToSend = [];
1062
+ for (const message of batch) {
1063
+ batchToSend.push({ contents: message.contents, metadata: message.metadata });
1064
+ }
1065
+ // returns clientSequenceNumber of last message in a batch
1066
+ clientSequenceNumber = this.context.submitBatchFn(batchToSend);
1067
+ }
1068
+ else {
1069
+ // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
1070
+ // version that has support for batches (submitBatchFn)
1071
+ for (const message of batch) {
1072
+ clientSequenceNumber = this.context.submitFn(protocol_definitions_1.MessageType.Operation, message.deserializedContent, true, // batch
1073
+ message.metadata);
1074
+ }
1075
+ this.deltaSender.flush();
1076
+ }
1077
+ // Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
1078
+ clientSequenceNumber -= batch.length - 1;
1079
+ (0, common_utils_1.assert)(clientSequenceNumber >= 0, 0x3d0 /* clientSequenceNumber can't be negative */);
1080
+ }
1081
+ // Let the PendingStateManager know that a message was submitted.
1082
+ // In future, need to shift toward keeping batch as a whole!
1083
+ for (const message of batch) {
1084
+ this.pendingStateManager.onSubmitMessage(message.deserializedContent.type, clientSequenceNumber, message.referenceSequenceNumber, message.deserializedContent.contents, message.localOpMetadata, message.metadata);
1085
+ clientSequenceNumber++;
1033
1086
  }
1034
- return this.deltaSender.flush();
1087
+ this.pendingStateManager.onFlush();
1035
1088
  }
1036
1089
  orderSequentially(callback) {
1037
1090
  // If flush mode is already TurnBased we are either
@@ -1056,7 +1109,10 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1056
1109
  trackOrderSequentiallyCalls(callback) {
1057
1110
  let checkpoint;
1058
1111
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
1059
- checkpoint = this.pendingStateManager.checkpoint();
1112
+ // Note: we are not touching this.pendingAttachBatch here, for two reasons:
1113
+ // 1. It would not help, as we flush attach ops as they become available.
1114
+ // 2. There is no way to undo process of data store creation.
1115
+ checkpoint = this.pendingBatch.checkpoint();
1060
1116
  }
1061
1117
  try {
1062
1118
  this._orderSequentiallyCalls++;
@@ -1065,7 +1121,16 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1065
1121
  catch (error) {
1066
1122
  if (checkpoint) {
1067
1123
  // This will throw and close the container if rollback fails
1068
- checkpoint.rollback();
1124
+ try {
1125
+ checkpoint.rollback((message) => this.rollback(message.deserializedContent.type, message.deserializedContent.contents, message.localOpMetadata));
1126
+ }
1127
+ catch (err) {
1128
+ const error2 = (0, telemetry_utils_1.wrapError)(err, (message) => {
1129
+ return container_utils_1.DataProcessingError.create(`RollbackError: ${message}`, "checkpointRollback", undefined);
1130
+ });
1131
+ this.closeFn(error2);
1132
+ throw error2;
1133
+ }
1069
1134
  }
1070
1135
  else {
1071
1136
  // pre-0.58 error message: orderSequentiallyCallbackException
@@ -1170,7 +1235,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1170
1235
  (0, common_utils_1.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
1171
1236
  this.emit("attached");
1172
1237
  }
1173
- if (attachState === container_definitions_1.AttachState.Attached && !this.pendingStateManager.hasPendingMessages()) {
1238
+ if (attachState === container_definitions_1.AttachState.Attached && !this.hasPendingMessages()) {
1174
1239
  this.updateDocumentDirtyState(false);
1175
1240
  }
1176
1241
  this.dataStores.setAttachState(attachState);
@@ -1334,7 +1399,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1334
1399
  }
1335
1400
  /**
1336
1401
  * Runs garbage collection and updates the reference / used state of the nodes in the container.
1337
- * @returns the statistics of the garbage collection run.
1402
+ * @returns the statistics of the garbage collection run; undefined if GC did not run.
1338
1403
  */
1339
1404
  async collectGarbage(options) {
1340
1405
  return this.garbageCollector.collectGarbage(options);
@@ -1365,6 +1430,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1365
1430
  const summaryNumberLogger = telemetry_utils_1.ChildLogger.create(summaryLogger, undefined, {
1366
1431
  all: { summaryNumber },
1367
1432
  });
1433
+ (0, common_utils_1.assert)(this.emptyBatch, 0x3d1 /* Can't trigger summary in the middle of a batch */);
1368
1434
  let latestSnapshotVersionId;
1369
1435
  if (refreshLatestAck) {
1370
1436
  const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(telemetry_utils_1.ChildLogger.create(summaryNumberLogger, undefined, { all: { safeSummary: true } }));
@@ -1439,7 +1505,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1439
1505
  const forcedFullTree = this.garbageCollector.summaryStateNeedsReset;
1440
1506
  try {
1441
1507
  summarizeResult = await this.summarize({
1442
- fullTree: fullTree || forcedFullTree,
1508
+ fullTree: fullTree !== null && fullTree !== void 0 ? fullTree : forcedFullTree,
1443
1509
  trackState: true,
1444
1510
  summaryLogger: summaryNumberLogger,
1445
1511
  runGC: this.garbageCollector.shouldRunGC,
@@ -1528,7 +1594,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1528
1594
  }
1529
1595
  let clientSequenceNumber;
1530
1596
  try {
1531
- clientSequenceNumber = this.submitSystemMessage(protocol_definitions_1.MessageType.Summarize, summaryMessage);
1597
+ clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
1532
1598
  }
1533
1599
  catch (error) {
1534
1600
  return Object.assign(Object.assign({ stage: "upload" }, uploadData), { error });
@@ -1576,7 +1642,17 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1576
1642
  this.chunkMap.delete(clientId);
1577
1643
  }
1578
1644
  }
1645
+ hasPendingMessages() {
1646
+ return this.pendingStateManager.hasPendingMessages() || !this.emptyBatch;
1647
+ }
1579
1648
  updateDocumentDirtyState(dirty) {
1649
+ if (this.attachState !== container_definitions_1.AttachState.Attached) {
1650
+ (0, common_utils_1.assert)(dirty, 0x3d2 /* Non-attached container is dirty */);
1651
+ }
1652
+ else {
1653
+ // Other way is not true = see this.isContainerMessageDirtyable()
1654
+ (0, common_utils_1.assert)(!dirty || this.hasPendingMessages(), 0x3d3 /* if doc is dirty, there has to be pending ops */);
1655
+ }
1580
1656
  if (this.dirtyContainer === dirty) {
1581
1657
  return;
1582
1658
  }
@@ -1604,64 +1680,100 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1604
1680
  this.verifyNotClosed();
1605
1681
  return this.blobManager.createBlob(blob);
1606
1682
  }
1607
- submit(type, content, localOpMetadata = undefined, opMetadata = undefined) {
1683
+ submit(type, contents, localOpMetadata = undefined, metadata = undefined) {
1608
1684
  this.verifyNotClosed();
1609
1685
  // There should be no ops in detached container state!
1610
1686
  (0, common_utils_1.assert)(this.attachState !== container_definitions_1.AttachState.Detached, 0x132 /* "sending ops in detached container" */);
1611
- let clientSequenceNumber = -1;
1612
- let opMetadataInternal = opMetadata;
1613
- if (this.canSendOps()) {
1614
- const serializedContent = JSON.stringify(content);
1615
- // If in TurnBased flush mode we will trigger a flush at the next turn break
1616
- if (this.flushMode === runtime_definitions_1.FlushMode.TurnBased && !this.needsFlush) {
1617
- opMetadataInternal = Object.assign(Object.assign({}, opMetadata), { batch: true });
1618
- this.needsFlush = true;
1619
- // Use Promise.resolve().then() to queue a microtask to detect the end of the turn and force a flush.
1620
- if (!this.flushTrigger) {
1621
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1622
- Promise.resolve().then(() => {
1623
- this.flushTrigger = false;
1624
- this.flush();
1687
+ const deserializedContent = { type, contents };
1688
+ const serializedContent = JSON.stringify(deserializedContent);
1689
+ if (this.deltaManager.readOnlyInfo.readonly) {
1690
+ this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
1691
+ }
1692
+ const message = {
1693
+ contents: serializedContent,
1694
+ deserializedContent,
1695
+ metadata,
1696
+ localOpMetadata,
1697
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
1698
+ };
1699
+ try {
1700
+ // If this is attach message for new data store, and we are in a batch, send this op out of order
1701
+ // Is it safe:
1702
+ // Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
1703
+ // They become visible only when aliased, or handle to some sub-element of newly created datastore
1704
+ // is stored in some DDS, i.e. only after some other op.
1705
+ // Why:
1706
+ // Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
1707
+ // stores are created, causing issues like relay service throttling (too many ops) and catastrophic
1708
+ // failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
1709
+ // these issues.
1710
+ // Cons:
1711
+ // 1. With large batches, relay service may throttle clients. Clients may disconnect while throttled.
1712
+ // This change creates new possibility of a lot of newly created data stores never being referenced
1713
+ // because client died before it had a change to submit the rest of the ops. This will create more
1714
+ // garbage that needs to be collected leveraging GC (Garbage Collection) feature.
1715
+ // 2. Sending ops out of order means they are excluded from rollback functionality. This is not an issue
1716
+ // today as rollback can't undo creation of data store. To some extent not sending them is a bigger
1717
+ // issue than sending.
1718
+ // Please note that this does not change file format, so it can be disabled in the future if this
1719
+ // optimization no longer makes sense (for example, batch compression may make it less appealing).
1720
+ if (type === ContainerMessageType.Attach &&
1721
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
1722
+ if (!this.pendingAttachBatch.push(message)) {
1723
+ // BatchManager has two limits - soft limit & hard limit. Soft limit is only engaged
1724
+ // when queue is not empty.
1725
+ // Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
1726
+ this.flushBatch(this.pendingAttachBatch.popBatch());
1727
+ if (!this.pendingAttachBatch.push(message)) {
1728
+ throw new container_utils_1.GenericError("BatchTooLarge",
1729
+ /* error */ undefined, {
1730
+ opSize: message.contents.length,
1731
+ count: this.pendingAttachBatch.length,
1732
+ limit: this.pendingAttachBatch.limit,
1733
+ });
1734
+ }
1735
+ }
1736
+ }
1737
+ else {
1738
+ if (!this.pendingBatch.push(message)) {
1739
+ throw new container_utils_1.GenericError("BatchTooLarge",
1740
+ /* error */ undefined, {
1741
+ opSize: message.contents.length,
1742
+ count: this.pendingBatch.length,
1743
+ limit: this.pendingBatch.limit,
1625
1744
  });
1626
1745
  }
1627
1746
  }
1628
- if (!serializedContent || serializedContent.length <= defaultMaxOpSizeInBytes) {
1629
- clientSequenceNumber = this.submitRuntimeMessage(type, content, this._flushMode === runtime_definitions_1.FlushMode.TurnBased /* batch */, opMetadataInternal);
1747
+ if (this._flushMode !== runtime_definitions_1.FlushMode.TurnBased) {
1748
+ this.flush();
1630
1749
  }
1631
- else {
1632
- // If the content length is larger than the client configured message size
1633
- // instead of splitting the content, we will fail by explicitly closing the container
1634
- this.closeFn(new container_utils_1.GenericError("OpTooLarge",
1635
- /* error */ undefined, {
1636
- length: serializedContent.length,
1637
- limit: defaultMaxOpSizeInBytes,
1638
- }));
1639
- clientSequenceNumber = -1;
1750
+ else if (!this.flushTrigger) {
1751
+ this.flushTrigger = true;
1752
+ // Queue a microtask to detect the end of the turn and force a flush.
1753
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1754
+ Promise.resolve().then(() => {
1755
+ this.flushTrigger = false;
1756
+ this.flush();
1757
+ });
1640
1758
  }
1641
1759
  }
1642
- // Let the PendingStateManager know that a message was submitted.
1643
- this.pendingStateManager.onSubmitMessage(type, clientSequenceNumber, this.deltaManager.lastSequenceNumber, content, localOpMetadata, opMetadataInternal);
1644
- if (this.isContainerMessageDirtyable(type, content)) {
1760
+ catch (error) {
1761
+ this.closeFn(error);
1762
+ throw error;
1763
+ }
1764
+ if (this.isContainerMessageDirtyable(type, contents)) {
1645
1765
  this.updateDocumentDirtyState(true);
1646
1766
  }
1647
1767
  }
1648
- submitSystemMessage(type, contents) {
1768
+ submitSummaryMessage(contents) {
1649
1769
  this.verifyNotClosed();
1650
1770
  (0, common_utils_1.assert)(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
1651
1771
  // System message should not be sent in the middle of the batch.
1652
- // That said, we can preserve existing behavior by not flushing existing buffer.
1653
- // That might be not what caller hopes to get, but we can look deeper if telemetry tells us it's a problem.
1654
- const middleOfBatch = this.flushMode === runtime_definitions_1.FlushMode.TurnBased && this.needsFlush;
1655
- if (middleOfBatch) {
1656
- this.mc.logger.sendErrorEvent({ eventName: "submitSystemMessageError", type });
1657
- }
1658
- return this.context.submitFn(type, contents, middleOfBatch);
1659
- }
1660
- submitRuntimeMessage(type, contents, batch, appData) {
1661
- this.verifyNotClosed();
1662
- (0, common_utils_1.assert)(this.connected, 0x259 /* "Container disconnected when trying to submit system message" */);
1663
- const payload = { type, contents };
1664
- return this.context.submitFn(protocol_definitions_1.MessageType.Operation, payload, batch, appData);
1772
+ (0, common_utils_1.assert)(this.emptyBatch, 0x3d4 /* System op in the middle of a batch */);
1773
+ // back-compat: ADO #1385: Make this call unconditional in the future
1774
+ return this.context.submitSummaryFn !== undefined
1775
+ ? this.context.submitSummaryFn(contents)
1776
+ : this.context.submitFn(protocol_definitions_1.MessageType.Summarize, contents, false);
1665
1777
  }
1666
1778
  /**
1667
1779
  * Throw an error if the runtime is closed. Methods that are expected to potentially
@@ -1782,6 +1894,10 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1782
1894
  if (!((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad)) {
1783
1895
  throw new container_utils_1.UsageError("can't get state when offline load disabled");
1784
1896
  }
1897
+ // Flush pending batch.
1898
+ // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
1899
+ // to close current batch.
1900
+ this.flush();
1785
1901
  const previousPendingState = this.context.pendingLocalState;
1786
1902
  if (previousPendingState) {
1787
1903
  return {
@@ -1836,6 +1952,11 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1836
1952
  // we may not have seen every sequence number (because of system ops) so apply everything once we
1837
1953
  // don't have any more saved ops
1838
1954
  await this.pendingStateManager.applyStashedOpsAt();
1955
+ // If it's not the case, we should take it into account when calculating dirty state.
1956
+ (0, common_utils_1.assert)(this.context.attachState === container_definitions_1.AttachState.Attached, 0x3d5 /* this function is called for attached containers only */);
1957
+ if (!this.hasPendingMessages()) {
1958
+ this.updateDocumentDirtyState(false);
1959
+ }
1839
1960
  }
1840
1961
  validateSummaryHeuristicConfiguration(configuration) {
1841
1962
  // eslint-disable-next-line no-restricted-syntax