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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/batchManager.d.ts +32 -0
  2. package/dist/batchManager.d.ts.map +1 -0
  3. package/dist/batchManager.js +71 -0
  4. package/dist/batchManager.js.map +1 -0
  5. package/dist/containerRuntime.d.ts +44 -17
  6. package/dist/containerRuntime.d.ts.map +1 -1
  7. package/dist/containerRuntime.js +197 -95
  8. package/dist/containerRuntime.js.map +1 -1
  9. package/dist/dataStoreContext.d.ts +4 -4
  10. package/dist/dataStoreContext.js +5 -5
  11. package/dist/dataStoreContext.js.map +1 -1
  12. package/dist/packageVersion.d.ts +1 -1
  13. package/dist/packageVersion.d.ts.map +1 -1
  14. package/dist/packageVersion.js +1 -1
  15. package/dist/packageVersion.js.map +1 -1
  16. package/dist/pendingStateManager.d.ts +0 -11
  17. package/dist/pendingStateManager.d.ts.map +1 -1
  18. package/dist/pendingStateManager.js +6 -43
  19. package/dist/pendingStateManager.js.map +1 -1
  20. package/dist/runningSummarizer.js +1 -1
  21. package/dist/runningSummarizer.js.map +1 -1
  22. package/dist/scheduleManager.js +1 -1
  23. package/dist/scheduleManager.js.map +1 -1
  24. package/dist/summarizerTypes.d.ts +3 -3
  25. package/dist/summarizerTypes.js +1 -1
  26. package/dist/summarizerTypes.js.map +1 -1
  27. package/dist/summaryCollection.d.ts +1 -0
  28. package/dist/summaryCollection.d.ts.map +1 -1
  29. package/dist/summaryCollection.js +32 -13
  30. package/dist/summaryCollection.js.map +1 -1
  31. package/lib/batchManager.d.ts +32 -0
  32. package/lib/batchManager.d.ts.map +1 -0
  33. package/lib/batchManager.js +67 -0
  34. package/lib/batchManager.js.map +1 -0
  35. package/lib/containerRuntime.d.ts +44 -17
  36. package/lib/containerRuntime.d.ts.map +1 -1
  37. package/lib/containerRuntime.js +200 -98
  38. package/lib/containerRuntime.js.map +1 -1
  39. package/lib/dataStoreContext.d.ts +4 -4
  40. package/lib/dataStoreContext.js +5 -5
  41. package/lib/dataStoreContext.js.map +1 -1
  42. package/lib/packageVersion.d.ts +1 -1
  43. package/lib/packageVersion.d.ts.map +1 -1
  44. package/lib/packageVersion.js +1 -1
  45. package/lib/packageVersion.js.map +1 -1
  46. package/lib/pendingStateManager.d.ts +0 -11
  47. package/lib/pendingStateManager.d.ts.map +1 -1
  48. package/lib/pendingStateManager.js +6 -43
  49. package/lib/pendingStateManager.js.map +1 -1
  50. package/lib/runningSummarizer.js +1 -1
  51. package/lib/runningSummarizer.js.map +1 -1
  52. package/lib/scheduleManager.js +2 -2
  53. package/lib/scheduleManager.js.map +1 -1
  54. package/lib/summarizerTypes.d.ts +3 -3
  55. package/lib/summarizerTypes.js +1 -1
  56. package/lib/summarizerTypes.js.map +1 -1
  57. package/lib/summaryCollection.d.ts +1 -0
  58. package/lib/summaryCollection.d.ts.map +1 -1
  59. package/lib/summaryCollection.js +32 -13
  60. package/lib/summaryCollection.js.map +1 -1
  61. package/package.json +17 -17
  62. package/src/batchManager.ts +88 -0
  63. package/src/containerRuntime.ts +273 -156
  64. package/src/dataStoreContext.ts +7 -7
  65. package/src/packageVersion.ts +1 -1
  66. package/src/pendingStateManager.ts +6 -56
  67. package/src/runningSummarizer.ts +1 -1
  68. package/src/scheduleManager.ts +2 -2
  69. package/src/summarizerTypes.ts +3 -3
  70. package/src/summaryCollection.ts +33 -16
@@ -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,12 @@ function isRuntimeMessage(message) {
100
103
  return false;
101
104
  }
102
105
  exports.isRuntimeMessage = isRuntimeMessage;
106
+ /**
107
+ * Unpacks runtime messages
108
+ * @internal - no promises RE back-compat - this is internal API.
109
+ * @param message - message (as it observed in storage / service)
110
+ * @returns unpacked runtime message
111
+ */
103
112
  function unpackRuntimeMessage(message) {
104
113
  if (message.type === protocol_definitions_1.MessageType.Operation) {
105
114
  // legacy op format?
@@ -113,14 +122,15 @@ function unpackRuntimeMessage(message) {
113
122
  message.type = innerContents.type;
114
123
  message.contents = innerContents.contents;
115
124
  }
116
- (0, common_utils_1.assert)((0, driver_utils_1.isUnpackedRuntimeMessage)(message), 0x122 /* "Message to unpack is not proper runtime message" */);
125
+ return true;
117
126
  }
118
127
  else {
119
128
  // Legacy format, but it's already "unpacked",
120
129
  // i.e. message.type is actually ContainerMessageType.
130
+ // Or it's non-runtime message.
121
131
  // Nothing to do in such case.
132
+ return false;
122
133
  }
123
- return message;
124
134
  }
125
135
  exports.unpackRuntimeMessage = unpackRuntimeMessage;
126
136
  /**
@@ -163,7 +173,6 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
163
173
  this.summaryConfiguration = summaryConfiguration;
164
174
  this.defaultMaxConsecutiveReconnects = 7;
165
175
  this._orderSequentiallyCalls = 0;
166
- this.needsFlush = false;
167
176
  this.flushTrigger = false;
168
177
  this.savedOps = [];
169
178
  this.consecutiveReconnects = 0;
@@ -176,6 +185,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
176
185
  signalTimestamp: 0,
177
186
  trackingSignalSequenceNumber: undefined,
178
187
  };
188
+ this.batchManager = new batchManager_1.BatchManager();
179
189
  this.summarizeOnDemand = (...args) => {
180
190
  if (this.clientDetails.type === summarizerClientElection_1.summarizerClientType) {
181
191
  return this.summarizer.summarizeOnDemand(...args);
@@ -270,7 +280,6 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
270
280
  flush: this.flush.bind(this),
271
281
  flushMode: () => this.flushMode,
272
282
  reSubmit: this.reSubmit.bind(this),
273
- rollback: this.rollback.bind(this),
274
283
  setFlushMode: (mode) => this.setFlushMode(mode),
275
284
  }, this._flushMode, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pending);
276
285
  this.context.quorum.on("removeMember", (clientId) => {
@@ -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, "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,
@@ -870,41 +882,42 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
870
882
  process(messageArg, local) {
871
883
  var _a;
872
884
  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
885
  // Do shallow copy of message, as methods below will modify it.
881
886
  // There might be multiple container instances receiving same message
882
887
  // We do not need to make deep copy, as each layer will just replace message.content itself,
883
888
  // but would not modify contents details
884
889
  let message = Object.assign({}, messageArg);
890
+ // back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
891
+ // System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
892
+ // Old ops may contain empty string (I assume noops).
893
+ if (typeof message.contents === "string" && message.contents !== "") {
894
+ message.contents = JSON.parse(message.contents);
895
+ }
896
+ // Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
897
+ // This format was not shipped to production workflows.
898
+ const runtimeMessage = unpackRuntimeMessage(message);
899
+ if ((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad) {
900
+ this.savedOps.push(messageArg);
901
+ }
885
902
  // Surround the actual processing of the operation with messages to the schedule manager indicating
886
903
  // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
887
904
  // messages once a batch has been fully processed.
888
905
  this.scheduleManager.beforeOpProcessing(message);
889
906
  try {
890
- message = unpackRuntimeMessage(message);
891
907
  // Chunk processing must come first given that we will transform the message to the unchunked version
892
908
  // once all pieces are available
893
909
  message = this.processRemoteChunkedMessage(message);
894
910
  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
- }
911
+ if (local && runtimeMessage) {
912
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
901
913
  }
902
914
  // If there are no more pending messages after processing a local message,
903
915
  // the document is no longer dirty.
904
- if (!this.pendingStateManager.hasPendingMessages()) {
916
+ if (!this.hasPendingMessages()) {
905
917
  this.updateDocumentDirtyState(false);
906
918
  }
907
- switch (message.type) {
919
+ const type = message.type;
920
+ switch (type) {
908
921
  case ContainerMessageType.Attach:
909
922
  this.dataStores.processAttachMessage(message, local);
910
923
  break;
@@ -917,9 +930,16 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
917
930
  case ContainerMessageType.BlobAttach:
918
931
  this.blobManager.processBlobAttachOp(message, local);
919
932
  break;
933
+ case ContainerMessageType.ChunkedOp:
934
+ case ContainerMessageType.Rejoin:
935
+ break;
920
936
  default:
937
+ (0, common_utils_1.assert)(!runtimeMessage, "Runtime message of unknown type");
938
+ }
939
+ // For back-compat, notify only about runtime messages for now.
940
+ if (runtimeMessage) {
941
+ this.emit("op", message, runtimeMessage);
921
942
  }
922
- this.emit("op", message);
923
943
  this.scheduleManager.afterOpProcessing(undefined, message);
924
944
  if (local) {
925
945
  // If we have processed a local op, this means that the container is
@@ -1013,25 +1033,57 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1013
1033
  }
1014
1034
  flush() {
1015
1035
  (0, common_utils_1.assert)(this._orderSequentiallyCalls === 0, 0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
1016
- if (!this.deltaSender) {
1017
- return;
1036
+ const batch = this.batchManager.popBatch();
1037
+ this.flushBatch(batch);
1038
+ (0, common_utils_1.assert)(this.batchManager.empty, "reentrancy");
1039
+ }
1040
+ flushBatch(batch) {
1041
+ const length = batch.length;
1042
+ if (length > 1) {
1043
+ batch[0].metadata = Object.assign(Object.assign({}, batch[0].metadata), { batch: true });
1044
+ batch[length - 1].metadata = Object.assign(Object.assign({}, batch[length - 1].metadata), { batch: false });
1045
+ // This assert fires for the following reason (there might be more cases like that):
1046
+ // AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
1047
+ // i.e. in the middle of op processing!
1048
+ // Sending ops while processing ops is not good idea - it's not defined when
1049
+ // referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
1050
+ // If we send ops in response to processing multiple ops, then we for sure hit this assert!
1051
+ // Tracked via ADO #1834
1052
+ // assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
1053
+ // "Batch should be generated synchronously, without processing ops in the middle!");
1018
1054
  }
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;
1027
- }
1028
- this.needsFlush = false;
1055
+ let clientSequenceNumber = -1;
1029
1056
  // Did we disconnect in the middle of turn-based batch?
1030
1057
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
1031
- if (!this.canSendOps()) {
1032
- return;
1058
+ if (this.canSendOps()) {
1059
+ if (this.context.submitBatchFn !== undefined) {
1060
+ const batchToSend = [];
1061
+ for (const message of batch) {
1062
+ batchToSend.push({ contents: message.contents, metadata: message.metadata });
1063
+ }
1064
+ // returns clientSequenceNumber of last message in a batch
1065
+ clientSequenceNumber = this.context.submitBatchFn(batchToSend);
1066
+ }
1067
+ else {
1068
+ // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
1069
+ // version that has support for batches (submitBatchFn)
1070
+ for (const message of batch) {
1071
+ clientSequenceNumber = this.context.submitFn(protocol_definitions_1.MessageType.Operation, message.deserializedContent, true, // batch
1072
+ message.metadata);
1073
+ }
1074
+ this.deltaSender.flush();
1075
+ }
1076
+ // Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
1077
+ clientSequenceNumber -= batch.length - 1;
1078
+ (0, common_utils_1.assert)(clientSequenceNumber >= 0, "clientSequenceNumber can't be negative");
1079
+ }
1080
+ // Let the PendingStateManager know that a message was submitted.
1081
+ // In future, need to shift toward keeping batch as a whole!
1082
+ for (const message of batch) {
1083
+ this.pendingStateManager.onSubmitMessage(message.deserializedContent.type, clientSequenceNumber, message.referenceSequenceNumber, message.deserializedContent.contents, message.localOpMetadata, message.metadata);
1084
+ clientSequenceNumber++;
1033
1085
  }
1034
- return this.deltaSender.flush();
1086
+ this.pendingStateManager.onFlush();
1035
1087
  }
1036
1088
  orderSequentially(callback) {
1037
1089
  // If flush mode is already TurnBased we are either
@@ -1056,7 +1108,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1056
1108
  trackOrderSequentiallyCalls(callback) {
1057
1109
  let checkpoint;
1058
1110
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
1059
- checkpoint = this.pendingStateManager.checkpoint();
1111
+ checkpoint = this.batchManager.checkpoint();
1060
1112
  }
1061
1113
  try {
1062
1114
  this._orderSequentiallyCalls++;
@@ -1065,7 +1117,16 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1065
1117
  catch (error) {
1066
1118
  if (checkpoint) {
1067
1119
  // This will throw and close the container if rollback fails
1068
- checkpoint.rollback();
1120
+ try {
1121
+ checkpoint.rollback((message) => this.rollback(message.deserializedContent.type, message.deserializedContent.contents, message.localOpMetadata));
1122
+ }
1123
+ catch (err) {
1124
+ const error2 = (0, telemetry_utils_1.wrapError)(err, (message) => {
1125
+ return container_utils_1.DataProcessingError.create(`RollbackError: ${message}`, "checkpointRollback", undefined);
1126
+ });
1127
+ this.closeFn(error2);
1128
+ throw error2;
1129
+ }
1069
1130
  }
1070
1131
  else {
1071
1132
  // pre-0.58 error message: orderSequentiallyCallbackException
@@ -1170,7 +1231,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1170
1231
  (0, common_utils_1.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
1171
1232
  this.emit("attached");
1172
1233
  }
1173
- if (attachState === container_definitions_1.AttachState.Attached && !this.pendingStateManager.hasPendingMessages()) {
1234
+ if (attachState === container_definitions_1.AttachState.Attached && !this.hasPendingMessages()) {
1174
1235
  this.updateDocumentDirtyState(false);
1175
1236
  }
1176
1237
  this.dataStores.setAttachState(attachState);
@@ -1365,6 +1426,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1365
1426
  const summaryNumberLogger = telemetry_utils_1.ChildLogger.create(summaryLogger, undefined, {
1366
1427
  all: { summaryNumber },
1367
1428
  });
1429
+ (0, common_utils_1.assert)(this.batchManager.empty, "Can't trigger summary in the middle of a batch");
1368
1430
  let latestSnapshotVersionId;
1369
1431
  if (refreshLatestAck) {
1370
1432
  const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(telemetry_utils_1.ChildLogger.create(summaryNumberLogger, undefined, { all: { safeSummary: true } }));
@@ -1528,7 +1590,7 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1528
1590
  }
1529
1591
  let clientSequenceNumber;
1530
1592
  try {
1531
- clientSequenceNumber = this.submitSystemMessage(protocol_definitions_1.MessageType.Summarize, summaryMessage);
1593
+ clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
1532
1594
  }
1533
1595
  catch (error) {
1534
1596
  return Object.assign(Object.assign({ stage: "upload" }, uploadData), { error });
@@ -1576,7 +1638,17 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1576
1638
  this.chunkMap.delete(clientId);
1577
1639
  }
1578
1640
  }
1641
+ hasPendingMessages() {
1642
+ return this.pendingStateManager.hasPendingMessages() || !this.batchManager.empty;
1643
+ }
1579
1644
  updateDocumentDirtyState(dirty) {
1645
+ if (this.attachState !== container_definitions_1.AttachState.Attached) {
1646
+ (0, common_utils_1.assert)(dirty, "Non-attached container is dirty");
1647
+ }
1648
+ else {
1649
+ // Other way is not true = see this.isContainerMessageDirtyable()
1650
+ (0, common_utils_1.assert)(!dirty || this.hasPendingMessages(), "if doc is dirty, there has to be pending ops");
1651
+ }
1580
1652
  if (this.dirtyContainer === dirty) {
1581
1653
  return;
1582
1654
  }
@@ -1604,20 +1676,52 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1604
1676
  this.verifyNotClosed();
1605
1677
  return this.blobManager.createBlob(blob);
1606
1678
  }
1607
- submit(type, content, localOpMetadata = undefined, opMetadata = undefined) {
1679
+ submit(type, contents, localOpMetadata = undefined, metadata = undefined) {
1608
1680
  this.verifyNotClosed();
1609
1681
  // There should be no ops in detached container state!
1610
1682
  (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) {
1683
+ const deserializedContent = { type, contents };
1684
+ const serializedContent = JSON.stringify(deserializedContent);
1685
+ if (this.deltaManager.readOnlyInfo.readonly) {
1686
+ this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
1687
+ }
1688
+ const message = {
1689
+ contents: serializedContent,
1690
+ deserializedContent,
1691
+ metadata,
1692
+ localOpMetadata,
1693
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
1694
+ };
1695
+ try {
1696
+ // If this is attach message for new data store, and we are in a batch, send this op out of order
1697
+ // Is it safe:
1698
+ // Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
1699
+ // They become visible only when aliased, or handle to some sub-element of newly created datastore
1700
+ // is stored in some DDS, i.e. only after some other op.
1701
+ // Why:
1702
+ // Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
1703
+ // stores are created, causing issues like relay service throttling (too many ops) and catastrophic
1704
+ // failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
1705
+ // these issues.
1706
+ // Cons:
1707
+ // With large batches, relay service may throttle clients. Clients may disconnect while throttled.
1708
+ // This change creates new possibility of a lot of newly created data stores never being referenced
1709
+ // because client died before it had a change to submit the rest of the ops. This will create more
1710
+ // garbage that needs to be collected leveraging GC (Garbage Collection) feature.
1711
+ // Please note that this does not change file format, so it can be disabled in the future if this
1712
+ // optimization no longer makes sense (for example, batch compression may make it less appealing).
1713
+ if (this._flushMode === runtime_definitions_1.FlushMode.TurnBased && type === ContainerMessageType.Attach &&
1714
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
1715
+ this.flushBatch([message]);
1716
+ }
1717
+ else {
1718
+ this.batchManager.push(message);
1719
+ if (this._flushMode !== runtime_definitions_1.FlushMode.TurnBased) {
1720
+ this.flush();
1721
+ }
1722
+ else if (!this.flushTrigger) {
1723
+ this.flushTrigger = true;
1724
+ // Queue a microtask to detect the end of the turn and force a flush.
1621
1725
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1622
1726
  Promise.resolve().then(() => {
1623
1727
  this.flushTrigger = false;
@@ -1625,43 +1729,27 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1625
1729
  });
1626
1730
  }
1627
1731
  }
1628
- if (!serializedContent || serializedContent.length <= defaultMaxOpSizeInBytes) {
1629
- clientSequenceNumber = this.submitRuntimeMessage(type, content, this._flushMode === runtime_definitions_1.FlushMode.TurnBased /* batch */, opMetadataInternal);
1630
- }
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;
1640
- }
1641
1732
  }
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)) {
1733
+ catch (error) {
1734
+ this.closeFn(error);
1735
+ throw error;
1736
+ }
1737
+ if (this.isContainerMessageDirtyable(type, contents)) {
1645
1738
  this.updateDocumentDirtyState(true);
1646
1739
  }
1647
1740
  }
1648
- submitSystemMessage(type, contents) {
1741
+ submitSummaryMessage(contents) {
1649
1742
  this.verifyNotClosed();
1650
1743
  (0, common_utils_1.assert)(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
1651
1744
  // 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 });
1745
+ (0, common_utils_1.assert)(this.batchManager.empty, "System op in the middle of a batch");
1746
+ // back-compat: ADO #1385: Make this call unconditional in the future
1747
+ if (this.context.submitSummaryFn !== undefined) {
1748
+ return this.context.submitSummaryFn(contents);
1749
+ }
1750
+ else {
1751
+ return this.context.submitFn(protocol_definitions_1.MessageType.Summarize, contents, false); // batch
1657
1752
  }
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);
1665
1753
  }
1666
1754
  /**
1667
1755
  * Throw an error if the runtime is closed. Methods that are expected to potentially
@@ -1715,13 +1803,18 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1715
1803
  /** Implementation of ISummarizerInternalsProvider.refreshLatestSummaryAck */
1716
1804
  async refreshLatestSummaryAck(proposalHandle, ackHandle, summaryRefSeq, summaryLogger) {
1717
1805
  const readAndParseBlob = async (id) => (0, driver_utils_1.readAndParse)(this.storage, id);
1718
- const { snapshotTree } = await this.fetchSnapshotFromStorage(ackHandle, summaryLogger, {
1719
- eventName: "RefreshLatestSummaryGetSnapshot",
1720
- ackHandle,
1721
- summaryRefSeq,
1722
- fetchLatest: false,
1723
- });
1724
- const result = await this.summarizerNode.refreshLatestSummary(proposalHandle, summaryRefSeq, async () => snapshotTree, readAndParseBlob, summaryLogger);
1806
+ // The call to fetch the snapshot is very expensive and not always needed.
1807
+ // It should only be done by the summarizerNode, if required.
1808
+ const snapshotTreeFetcher = async () => {
1809
+ const fetchResult = await this.fetchSnapshotFromStorage(ackHandle, summaryLogger, {
1810
+ eventName: "RefreshLatestSummaryGetSnapshot",
1811
+ ackHandle,
1812
+ summaryRefSeq,
1813
+ fetchLatest: false,
1814
+ });
1815
+ return fetchResult.snapshotTree;
1816
+ };
1817
+ const result = await this.summarizerNode.refreshLatestSummary(proposalHandle, summaryRefSeq, snapshotTreeFetcher, readAndParseBlob, summaryLogger);
1725
1818
  // Notify the garbage collector so it can update its latest summary state.
1726
1819
  await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
1727
1820
  }
@@ -1777,6 +1870,10 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1777
1870
  if (!((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad)) {
1778
1871
  throw new container_utils_1.UsageError("can't get state when offline load disabled");
1779
1872
  }
1873
+ // Flush pending batch.
1874
+ // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
1875
+ // to close current batch.
1876
+ this.flush();
1780
1877
  const previousPendingState = this.context.pendingLocalState;
1781
1878
  if (previousPendingState) {
1782
1879
  return {
@@ -1831,6 +1928,11 @@ class ContainerRuntime extends common_utils_1.TypedEventEmitter {
1831
1928
  // we may not have seen every sequence number (because of system ops) so apply everything once we
1832
1929
  // don't have any more saved ops
1833
1930
  await this.pendingStateManager.applyStashedOpsAt();
1931
+ // If it's not the case, we should take it into account when calculating dirty state.
1932
+ (0, common_utils_1.assert)(this.context.attachState === container_definitions_1.AttachState.Attached, "this function is called for attached containers only");
1933
+ if (!this.hasPendingMessages()) {
1934
+ this.updateDocumentDirtyState(false);
1935
+ }
1834
1936
  }
1835
1937
  validateSummaryHeuristicConfiguration(configuration) {
1836
1938
  // eslint-disable-next-line no-restricted-syntax