@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.
- package/dist/batchManager.d.ts +32 -0
- package/dist/batchManager.d.ts.map +1 -0
- package/dist/batchManager.js +71 -0
- package/dist/batchManager.js.map +1 -0
- package/dist/containerRuntime.d.ts +44 -17
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +197 -95
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts +4 -4
- package/dist/dataStoreContext.js +5 -5
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +0 -11
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +6 -43
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runningSummarizer.js +1 -1
- package/dist/runningSummarizer.js.map +1 -1
- package/dist/scheduleManager.js +1 -1
- package/dist/scheduleManager.js.map +1 -1
- package/dist/summarizerTypes.d.ts +3 -3
- package/dist/summarizerTypes.js +1 -1
- package/dist/summarizerTypes.js.map +1 -1
- package/dist/summaryCollection.d.ts +1 -0
- package/dist/summaryCollection.d.ts.map +1 -1
- package/dist/summaryCollection.js +32 -13
- package/dist/summaryCollection.js.map +1 -1
- package/lib/batchManager.d.ts +32 -0
- package/lib/batchManager.d.ts.map +1 -0
- package/lib/batchManager.js +67 -0
- package/lib/batchManager.js.map +1 -0
- package/lib/containerRuntime.d.ts +44 -17
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +200 -98
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStoreContext.d.ts +4 -4
- package/lib/dataStoreContext.js +5 -5
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +0 -11
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +6 -43
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runningSummarizer.js +1 -1
- package/lib/runningSummarizer.js.map +1 -1
- package/lib/scheduleManager.js +2 -2
- package/lib/scheduleManager.js.map +1 -1
- package/lib/summarizerTypes.d.ts +3 -3
- package/lib/summarizerTypes.js +1 -1
- package/lib/summarizerTypes.js.map +1 -1
- package/lib/summaryCollection.d.ts +1 -0
- package/lib/summaryCollection.d.ts.map +1 -1
- package/lib/summaryCollection.js +32 -13
- package/lib/summaryCollection.js.map +1 -1
- package/package.json +17 -17
- package/src/batchManager.ts +88 -0
- package/src/containerRuntime.ts +273 -156
- package/src/dataStoreContext.ts +7 -7
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +6 -56
- package/src/runningSummarizer.ts +1 -1
- package/src/scheduleManager.ts +2 -2
- package/src/summarizerTypes.ts +3 -3
- package/src/summaryCollection.ts +33 -16
package/lib/containerRuntime.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
850
|
-
|
|
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
|
-
|
|
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.
|
|
910
|
+
if (!this.hasPendingMessages()) {
|
|
899
911
|
this.updateDocumentDirtyState(false);
|
|
900
912
|
}
|
|
901
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
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 (
|
|
1026
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
1606
|
-
|
|
1607
|
-
if (this.
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1647
|
-
//
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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
|