@fluidframework/container-runtime 2.0.0-internal.1.1.1 → 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
@@ -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,21 @@ 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
+ * @internal - no promises RE back-compat - this is internal API.
105
+ * @param message - message (as it observed in storage / service)
106
+ * @returns unpacked runtime message
107
+ */
99
108
  export function unpackRuntimeMessage(message) {
100
109
  if (message.type === MessageType.Operation) {
101
110
  // legacy op format?
@@ -109,14 +118,15 @@ export function unpackRuntimeMessage(message) {
109
118
  message.type = innerContents.type;
110
119
  message.contents = innerContents.contents;
111
120
  }
112
- assert(isUnpackedRuntimeMessage(message), 0x122 /* "Message to unpack is not proper runtime message" */);
121
+ return true;
113
122
  }
114
123
  else {
115
124
  // Legacy format, but it's already "unpacked",
116
125
  // i.e. message.type is actually ContainerMessageType.
126
+ // Or it's non-runtime message.
117
127
  // Nothing to do in such case.
128
+ return false;
118
129
  }
119
- return message;
120
130
  }
121
131
  /**
122
132
  * Legacy ID for the built-in AgentScheduler. To minimize disruption while removing it, retaining this as a
@@ -157,7 +167,6 @@ export class ContainerRuntime extends TypedEventEmitter {
157
167
  this.summaryConfiguration = summaryConfiguration;
158
168
  this.defaultMaxConsecutiveReconnects = 7;
159
169
  this._orderSequentiallyCalls = 0;
160
- this.needsFlush = false;
161
170
  this.flushTrigger = false;
162
171
  this.savedOps = [];
163
172
  this.consecutiveReconnects = 0;
@@ -170,6 +179,7 @@ export class ContainerRuntime extends TypedEventEmitter {
170
179
  signalTimestamp: 0,
171
180
  trackingSignalSequenceNumber: undefined,
172
181
  };
182
+ this.batchManager = new BatchManager();
173
183
  this.summarizeOnDemand = (...args) => {
174
184
  if (this.clientDetails.type === summarizerClientType) {
175
185
  return this.summarizer.summarizeOnDemand(...args);
@@ -264,7 +274,6 @@ export class ContainerRuntime extends TypedEventEmitter {
264
274
  flush: this.flush.bind(this),
265
275
  flushMode: () => this.flushMode,
266
276
  reSubmit: this.reSubmit.bind(this),
267
- rollback: this.rollback.bind(this),
268
277
  setFlushMode: (mode) => this.setFlushMode(mode),
269
278
  }, this._flushMode, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pending);
270
279
  this.context.quorum.on("removeMember", (clientId) => {
@@ -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, "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,
@@ -864,41 +876,42 @@ export class ContainerRuntime extends TypedEventEmitter {
864
876
  process(messageArg, local) {
865
877
  var _a;
866
878
  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
879
  // Do shallow copy of message, as methods below will modify it.
875
880
  // There might be multiple container instances receiving same message
876
881
  // We do not need to make deep copy, as each layer will just replace message.content itself,
877
882
  // but would not modify contents details
878
883
  let message = Object.assign({}, messageArg);
884
+ // back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
885
+ // System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
886
+ // Old ops may contain empty string (I assume noops).
887
+ if (typeof message.contents === "string" && message.contents !== "") {
888
+ message.contents = JSON.parse(message.contents);
889
+ }
890
+ // Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
891
+ // This format was not shipped to production workflows.
892
+ const runtimeMessage = unpackRuntimeMessage(message);
893
+ if ((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad) {
894
+ this.savedOps.push(messageArg);
895
+ }
879
896
  // Surround the actual processing of the operation with messages to the schedule manager indicating
880
897
  // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
881
898
  // messages once a batch has been fully processed.
882
899
  this.scheduleManager.beforeOpProcessing(message);
883
900
  try {
884
- message = unpackRuntimeMessage(message);
885
901
  // Chunk processing must come first given that we will transform the message to the unchunked version
886
902
  // once all pieces are available
887
903
  message = this.processRemoteChunkedMessage(message);
888
904
  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
- }
905
+ if (local && runtimeMessage) {
906
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
895
907
  }
896
908
  // If there are no more pending messages after processing a local message,
897
909
  // the document is no longer dirty.
898
- if (!this.pendingStateManager.hasPendingMessages()) {
910
+ if (!this.hasPendingMessages()) {
899
911
  this.updateDocumentDirtyState(false);
900
912
  }
901
- switch (message.type) {
913
+ const type = message.type;
914
+ switch (type) {
902
915
  case ContainerMessageType.Attach:
903
916
  this.dataStores.processAttachMessage(message, local);
904
917
  break;
@@ -911,9 +924,16 @@ export class ContainerRuntime extends TypedEventEmitter {
911
924
  case ContainerMessageType.BlobAttach:
912
925
  this.blobManager.processBlobAttachOp(message, local);
913
926
  break;
927
+ case ContainerMessageType.ChunkedOp:
928
+ case ContainerMessageType.Rejoin:
929
+ break;
914
930
  default:
931
+ assert(!runtimeMessage, "Runtime message of unknown type");
932
+ }
933
+ // For back-compat, notify only about runtime messages for now.
934
+ if (runtimeMessage) {
935
+ this.emit("op", message, runtimeMessage);
915
936
  }
916
- this.emit("op", message);
917
937
  this.scheduleManager.afterOpProcessing(undefined, message);
918
938
  if (local) {
919
939
  // If we have processed a local op, this means that the container is
@@ -1007,25 +1027,57 @@ export class ContainerRuntime extends TypedEventEmitter {
1007
1027
  }
1008
1028
  flush() {
1009
1029
  assert(this._orderSequentiallyCalls === 0, 0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
1010
- if (!this.deltaSender) {
1011
- return;
1030
+ const batch = this.batchManager.popBatch();
1031
+ this.flushBatch(batch);
1032
+ assert(this.batchManager.empty, "reentrancy");
1033
+ }
1034
+ flushBatch(batch) {
1035
+ const length = batch.length;
1036
+ if (length > 1) {
1037
+ batch[0].metadata = Object.assign(Object.assign({}, batch[0].metadata), { batch: true });
1038
+ batch[length - 1].metadata = Object.assign(Object.assign({}, batch[length - 1].metadata), { batch: false });
1039
+ // This assert fires for the following reason (there might be more cases like that):
1040
+ // AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
1041
+ // i.e. in the middle of op processing!
1042
+ // Sending ops while processing ops is not good idea - it's not defined when
1043
+ // referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
1044
+ // If we send ops in response to processing multiple ops, then we for sure hit this assert!
1045
+ // Tracked via ADO #1834
1046
+ // assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
1047
+ // "Batch should be generated synchronously, without processing ops in the middle!");
1012
1048
  }
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;
1021
- }
1022
- this.needsFlush = false;
1049
+ let clientSequenceNumber = -1;
1023
1050
  // Did we disconnect in the middle of turn-based batch?
1024
1051
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
1025
- if (!this.canSendOps()) {
1026
- return;
1052
+ if (this.canSendOps()) {
1053
+ if (this.context.submitBatchFn !== undefined) {
1054
+ const batchToSend = [];
1055
+ for (const message of batch) {
1056
+ batchToSend.push({ contents: message.contents, metadata: message.metadata });
1057
+ }
1058
+ // returns clientSequenceNumber of last message in a batch
1059
+ clientSequenceNumber = this.context.submitBatchFn(batchToSend);
1060
+ }
1061
+ else {
1062
+ // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
1063
+ // version that has support for batches (submitBatchFn)
1064
+ for (const message of batch) {
1065
+ clientSequenceNumber = this.context.submitFn(MessageType.Operation, message.deserializedContent, true, // batch
1066
+ message.metadata);
1067
+ }
1068
+ this.deltaSender.flush();
1069
+ }
1070
+ // Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
1071
+ clientSequenceNumber -= batch.length - 1;
1072
+ assert(clientSequenceNumber >= 0, "clientSequenceNumber can't be negative");
1073
+ }
1074
+ // Let the PendingStateManager know that a message was submitted.
1075
+ // In future, need to shift toward keeping batch as a whole!
1076
+ for (const message of batch) {
1077
+ this.pendingStateManager.onSubmitMessage(message.deserializedContent.type, clientSequenceNumber, message.referenceSequenceNumber, message.deserializedContent.contents, message.localOpMetadata, message.metadata);
1078
+ clientSequenceNumber++;
1027
1079
  }
1028
- return this.deltaSender.flush();
1080
+ this.pendingStateManager.onFlush();
1029
1081
  }
1030
1082
  orderSequentially(callback) {
1031
1083
  // If flush mode is already TurnBased we are either
@@ -1050,7 +1102,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1050
1102
  trackOrderSequentiallyCalls(callback) {
1051
1103
  let checkpoint;
1052
1104
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
1053
- checkpoint = this.pendingStateManager.checkpoint();
1105
+ checkpoint = this.batchManager.checkpoint();
1054
1106
  }
1055
1107
  try {
1056
1108
  this._orderSequentiallyCalls++;
@@ -1059,7 +1111,16 @@ export class ContainerRuntime extends TypedEventEmitter {
1059
1111
  catch (error) {
1060
1112
  if (checkpoint) {
1061
1113
  // This will throw and close the container if rollback fails
1062
- checkpoint.rollback();
1114
+ try {
1115
+ checkpoint.rollback((message) => this.rollback(message.deserializedContent.type, message.deserializedContent.contents, message.localOpMetadata));
1116
+ }
1117
+ catch (err) {
1118
+ const error2 = wrapError(err, (message) => {
1119
+ return DataProcessingError.create(`RollbackError: ${message}`, "checkpointRollback", undefined);
1120
+ });
1121
+ this.closeFn(error2);
1122
+ throw error2;
1123
+ }
1063
1124
  }
1064
1125
  else {
1065
1126
  // pre-0.58 error message: orderSequentiallyCallbackException
@@ -1164,7 +1225,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1164
1225
  assert(this.attachState === AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
1165
1226
  this.emit("attached");
1166
1227
  }
1167
- if (attachState === AttachState.Attached && !this.pendingStateManager.hasPendingMessages()) {
1228
+ if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
1168
1229
  this.updateDocumentDirtyState(false);
1169
1230
  }
1170
1231
  this.dataStores.setAttachState(attachState);
@@ -1359,6 +1420,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1359
1420
  const summaryNumberLogger = ChildLogger.create(summaryLogger, undefined, {
1360
1421
  all: { summaryNumber },
1361
1422
  });
1423
+ assert(this.batchManager.empty, "Can't trigger summary in the middle of a batch");
1362
1424
  let latestSnapshotVersionId;
1363
1425
  if (refreshLatestAck) {
1364
1426
  const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(ChildLogger.create(summaryNumberLogger, undefined, { all: { safeSummary: true } }));
@@ -1522,7 +1584,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1522
1584
  }
1523
1585
  let clientSequenceNumber;
1524
1586
  try {
1525
- clientSequenceNumber = this.submitSystemMessage(MessageType.Summarize, summaryMessage);
1587
+ clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
1526
1588
  }
1527
1589
  catch (error) {
1528
1590
  return Object.assign(Object.assign({ stage: "upload" }, uploadData), { error });
@@ -1570,7 +1632,17 @@ export class ContainerRuntime extends TypedEventEmitter {
1570
1632
  this.chunkMap.delete(clientId);
1571
1633
  }
1572
1634
  }
1635
+ hasPendingMessages() {
1636
+ return this.pendingStateManager.hasPendingMessages() || !this.batchManager.empty;
1637
+ }
1573
1638
  updateDocumentDirtyState(dirty) {
1639
+ if (this.attachState !== AttachState.Attached) {
1640
+ assert(dirty, "Non-attached container is dirty");
1641
+ }
1642
+ else {
1643
+ // Other way is not true = see this.isContainerMessageDirtyable()
1644
+ assert(!dirty || this.hasPendingMessages(), "if doc is dirty, there has to be pending ops");
1645
+ }
1574
1646
  if (this.dirtyContainer === dirty) {
1575
1647
  return;
1576
1648
  }
@@ -1598,20 +1670,52 @@ export class ContainerRuntime extends TypedEventEmitter {
1598
1670
  this.verifyNotClosed();
1599
1671
  return this.blobManager.createBlob(blob);
1600
1672
  }
1601
- submit(type, content, localOpMetadata = undefined, opMetadata = undefined) {
1673
+ submit(type, contents, localOpMetadata = undefined, metadata = undefined) {
1602
1674
  this.verifyNotClosed();
1603
1675
  // There should be no ops in detached container state!
1604
1676
  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) {
1677
+ const deserializedContent = { type, contents };
1678
+ const serializedContent = JSON.stringify(deserializedContent);
1679
+ if (this.deltaManager.readOnlyInfo.readonly) {
1680
+ this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
1681
+ }
1682
+ const message = {
1683
+ contents: serializedContent,
1684
+ deserializedContent,
1685
+ metadata,
1686
+ localOpMetadata,
1687
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
1688
+ };
1689
+ try {
1690
+ // If this is attach message for new data store, and we are in a batch, send this op out of order
1691
+ // Is it safe:
1692
+ // Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
1693
+ // They become visible only when aliased, or handle to some sub-element of newly created datastore
1694
+ // is stored in some DDS, i.e. only after some other op.
1695
+ // Why:
1696
+ // Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
1697
+ // stores are created, causing issues like relay service throttling (too many ops) and catastrophic
1698
+ // failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
1699
+ // these issues.
1700
+ // Cons:
1701
+ // With large batches, relay service may throttle clients. Clients may disconnect while throttled.
1702
+ // This change creates new possibility of a lot of newly created data stores never being referenced
1703
+ // because client died before it had a change to submit the rest of the ops. This will create more
1704
+ // garbage that needs to be collected leveraging GC (Garbage Collection) feature.
1705
+ // Please note that this does not change file format, so it can be disabled in the future if this
1706
+ // optimization no longer makes sense (for example, batch compression may make it less appealing).
1707
+ if (this._flushMode === FlushMode.TurnBased && type === ContainerMessageType.Attach &&
1708
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
1709
+ this.flushBatch([message]);
1710
+ }
1711
+ else {
1712
+ this.batchManager.push(message);
1713
+ if (this._flushMode !== FlushMode.TurnBased) {
1714
+ this.flush();
1715
+ }
1716
+ else if (!this.flushTrigger) {
1717
+ this.flushTrigger = true;
1718
+ // Queue a microtask to detect the end of the turn and force a flush.
1615
1719
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1616
1720
  Promise.resolve().then(() => {
1617
1721
  this.flushTrigger = false;
@@ -1619,43 +1723,27 @@ export class ContainerRuntime extends TypedEventEmitter {
1619
1723
  });
1620
1724
  }
1621
1725
  }
1622
- if (!serializedContent || serializedContent.length <= defaultMaxOpSizeInBytes) {
1623
- clientSequenceNumber = this.submitRuntimeMessage(type, content, this._flushMode === FlushMode.TurnBased /* batch */, opMetadataInternal);
1624
- }
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;
1634
- }
1635
1726
  }
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)) {
1727
+ catch (error) {
1728
+ this.closeFn(error);
1729
+ throw error;
1730
+ }
1731
+ if (this.isContainerMessageDirtyable(type, contents)) {
1639
1732
  this.updateDocumentDirtyState(true);
1640
1733
  }
1641
1734
  }
1642
- submitSystemMessage(type, contents) {
1735
+ submitSummaryMessage(contents) {
1643
1736
  this.verifyNotClosed();
1644
1737
  assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
1645
1738
  // 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 });
1739
+ assert(this.batchManager.empty, "System op in the middle of a batch");
1740
+ // back-compat: ADO #1385: Make this call unconditional in the future
1741
+ if (this.context.submitSummaryFn !== undefined) {
1742
+ return this.context.submitSummaryFn(contents);
1743
+ }
1744
+ else {
1745
+ return this.context.submitFn(MessageType.Summarize, contents, false); // batch
1651
1746
  }
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);
1659
1747
  }
1660
1748
  /**
1661
1749
  * Throw an error if the runtime is closed. Methods that are expected to potentially
@@ -1709,13 +1797,18 @@ export class ContainerRuntime extends TypedEventEmitter {
1709
1797
  /** Implementation of ISummarizerInternalsProvider.refreshLatestSummaryAck */
1710
1798
  async refreshLatestSummaryAck(proposalHandle, ackHandle, summaryRefSeq, summaryLogger) {
1711
1799
  const readAndParseBlob = async (id) => readAndParse(this.storage, id);
1712
- const { snapshotTree } = await this.fetchSnapshotFromStorage(ackHandle, summaryLogger, {
1713
- eventName: "RefreshLatestSummaryGetSnapshot",
1714
- ackHandle,
1715
- summaryRefSeq,
1716
- fetchLatest: false,
1717
- });
1718
- const result = await this.summarizerNode.refreshLatestSummary(proposalHandle, summaryRefSeq, async () => snapshotTree, readAndParseBlob, summaryLogger);
1800
+ // The call to fetch the snapshot is very expensive and not always needed.
1801
+ // It should only be done by the summarizerNode, if required.
1802
+ const snapshotTreeFetcher = async () => {
1803
+ const fetchResult = await this.fetchSnapshotFromStorage(ackHandle, summaryLogger, {
1804
+ eventName: "RefreshLatestSummaryGetSnapshot",
1805
+ ackHandle,
1806
+ summaryRefSeq,
1807
+ fetchLatest: false,
1808
+ });
1809
+ return fetchResult.snapshotTree;
1810
+ };
1811
+ const result = await this.summarizerNode.refreshLatestSummary(proposalHandle, summaryRefSeq, snapshotTreeFetcher, readAndParseBlob, summaryLogger);
1719
1812
  // Notify the garbage collector so it can update its latest summary state.
1720
1813
  await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
1721
1814
  }
@@ -1771,6 +1864,10 @@ export class ContainerRuntime extends TypedEventEmitter {
1771
1864
  if (!((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad)) {
1772
1865
  throw new UsageError("can't get state when offline load disabled");
1773
1866
  }
1867
+ // Flush pending batch.
1868
+ // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
1869
+ // to close current batch.
1870
+ this.flush();
1774
1871
  const previousPendingState = this.context.pendingLocalState;
1775
1872
  if (previousPendingState) {
1776
1873
  return {
@@ -1825,6 +1922,11 @@ export class ContainerRuntime extends TypedEventEmitter {
1825
1922
  // we may not have seen every sequence number (because of system ops) so apply everything once we
1826
1923
  // don't have any more saved ops
1827
1924
  await this.pendingStateManager.applyStashedOpsAt();
1925
+ // If it's not the case, we should take it into account when calculating dirty state.
1926
+ assert(this.context.attachState === AttachState.Attached, "this function is called for attached containers only");
1927
+ if (!this.hasPendingMessages()) {
1928
+ this.updateDocumentDirtyState(false);
1929
+ }
1828
1930
  }
1829
1931
  validateSummaryHeuristicConfiguration(configuration) {
1830
1932
  // eslint-disable-next-line no-restricted-syntax