@fluidframework/container-runtime 1.3.0 → 2.0.0-dev.1.4.5.105745
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/.eslintrc.js +8 -22
- package/.mocharc.js +12 -0
- package/dist/batchManager.d.ts +37 -0
- package/dist/batchManager.d.ts.map +1 -0
- package/dist/batchManager.js +73 -0
- package/dist/batchManager.js.map +1 -0
- package/dist/batchTracker.d.ts +1 -2
- package/dist/batchTracker.d.ts.map +1 -1
- package/dist/batchTracker.js +2 -3
- package/dist/batchTracker.js.map +1 -1
- package/dist/blobManager.d.ts +87 -25
- package/dist/blobManager.d.ts.map +1 -1
- package/dist/blobManager.js +317 -99
- package/dist/blobManager.js.map +1 -1
- package/dist/containerRuntime.d.ts +110 -125
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +360 -549
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.js +29 -24
- package/dist/dataStore.js.map +1 -1
- package/dist/dataStoreContext.d.ts +20 -14
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +49 -58
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +12 -5
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +21 -20
- package/dist/dataStores.js.map +1 -1
- package/dist/deltaScheduler.d.ts +6 -4
- package/dist/deltaScheduler.d.ts.map +1 -1
- package/dist/deltaScheduler.js +6 -4
- package/dist/deltaScheduler.js.map +1 -1
- package/dist/garbageCollection.d.ts +74 -14
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +248 -169
- package/dist/garbageCollection.js.map +1 -1
- package/dist/gcSweepReadyUsageDetection.d.ts +53 -0
- package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -0
- package/dist/gcSweepReadyUsageDetection.js +135 -0
- package/dist/gcSweepReadyUsageDetection.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/opProperties.d.ts +7 -0
- package/dist/opProperties.d.ts.map +1 -0
- package/dist/opProperties.js +20 -0
- package/dist/opProperties.js.map +1 -0
- package/dist/orderedClientElection.d.ts +28 -10
- package/dist/orderedClientElection.d.ts.map +1 -1
- package/dist/orderedClientElection.js +14 -4
- package/dist/orderedClientElection.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 +24 -46
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runningSummarizer.d.ts +14 -4
- package/dist/runningSummarizer.d.ts.map +1 -1
- package/dist/runningSummarizer.js +69 -27
- package/dist/runningSummarizer.js.map +1 -1
- package/dist/scheduleManager.d.ts +31 -0
- package/dist/scheduleManager.d.ts.map +1 -0
- package/dist/scheduleManager.js +243 -0
- package/dist/scheduleManager.js.map +1 -0
- package/dist/summarizer.d.ts +0 -2
- package/dist/summarizer.d.ts.map +1 -1
- package/dist/summarizer.js +1 -12
- package/dist/summarizer.js.map +1 -1
- package/dist/summarizerHeuristics.d.ts +26 -4
- package/dist/summarizerHeuristics.d.ts.map +1 -1
- package/dist/summarizerHeuristics.js +98 -18
- package/dist/summarizerHeuristics.js.map +1 -1
- package/dist/summarizerTypes.d.ts +45 -18
- package/dist/summarizerTypes.d.ts.map +1 -1
- 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 +31 -15
- package/dist/summaryCollection.js.map +1 -1
- package/dist/summaryFormat.d.ts +0 -5
- package/dist/summaryFormat.d.ts.map +1 -1
- package/dist/summaryFormat.js.map +1 -1
- package/dist/summaryGenerator.d.ts +1 -0
- package/dist/summaryGenerator.d.ts.map +1 -1
- package/dist/summaryGenerator.js +11 -9
- package/dist/summaryGenerator.js.map +1 -1
- package/dist/summaryManager.d.ts +2 -2
- package/dist/summaryManager.d.ts.map +1 -1
- package/dist/summaryManager.js +22 -7
- package/dist/summaryManager.js.map +1 -1
- package/lib/batchManager.d.ts +37 -0
- package/lib/batchManager.d.ts.map +1 -0
- package/lib/batchManager.js +69 -0
- package/lib/batchManager.js.map +1 -0
- package/lib/batchTracker.d.ts +1 -2
- package/lib/batchTracker.d.ts.map +1 -1
- package/lib/batchTracker.js +2 -3
- package/lib/batchTracker.js.map +1 -1
- package/lib/blobManager.d.ts +87 -25
- package/lib/blobManager.d.ts.map +1 -1
- package/lib/blobManager.js +319 -101
- package/lib/blobManager.js.map +1 -1
- package/lib/containerRuntime.d.ts +110 -125
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +366 -554
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.js +29 -24
- package/lib/dataStore.js.map +1 -1
- package/lib/dataStoreContext.d.ts +20 -14
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +46 -55
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStores.d.ts +12 -5
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +21 -20
- package/lib/dataStores.js.map +1 -1
- package/lib/deltaScheduler.d.ts +6 -4
- package/lib/deltaScheduler.d.ts.map +1 -1
- package/lib/deltaScheduler.js +6 -4
- package/lib/deltaScheduler.js.map +1 -1
- package/lib/garbageCollection.d.ts +74 -14
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +237 -159
- package/lib/garbageCollection.js.map +1 -1
- package/lib/gcSweepReadyUsageDetection.d.ts +53 -0
- package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -0
- package/lib/gcSweepReadyUsageDetection.js +130 -0
- package/lib/gcSweepReadyUsageDetection.js.map +1 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/opProperties.d.ts +7 -0
- package/lib/opProperties.d.ts.map +1 -0
- package/lib/opProperties.js +16 -0
- package/lib/opProperties.js.map +1 -0
- package/lib/orderedClientElection.d.ts +28 -10
- package/lib/orderedClientElection.d.ts.map +1 -1
- package/lib/orderedClientElection.js +14 -4
- package/lib/orderedClientElection.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 +24 -46
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runningSummarizer.d.ts +14 -4
- package/lib/runningSummarizer.d.ts.map +1 -1
- package/lib/runningSummarizer.js +69 -27
- package/lib/runningSummarizer.js.map +1 -1
- package/lib/scheduleManager.d.ts +31 -0
- package/lib/scheduleManager.d.ts.map +1 -0
- package/lib/scheduleManager.js +239 -0
- package/lib/scheduleManager.js.map +1 -0
- package/lib/summarizer.d.ts +0 -2
- package/lib/summarizer.d.ts.map +1 -1
- package/lib/summarizer.js +1 -12
- package/lib/summarizer.js.map +1 -1
- package/lib/summarizerHeuristics.d.ts +26 -4
- package/lib/summarizerHeuristics.d.ts.map +1 -1
- package/lib/summarizerHeuristics.js +98 -18
- package/lib/summarizerHeuristics.js.map +1 -1
- package/lib/summarizerTypes.d.ts +45 -18
- package/lib/summarizerTypes.d.ts.map +1 -1
- 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 +31 -15
- package/lib/summaryCollection.js.map +1 -1
- package/lib/summaryFormat.d.ts +0 -5
- package/lib/summaryFormat.d.ts.map +1 -1
- package/lib/summaryFormat.js.map +1 -1
- package/lib/summaryGenerator.d.ts +1 -0
- package/lib/summaryGenerator.d.ts.map +1 -1
- package/lib/summaryGenerator.js +11 -9
- package/lib/summaryGenerator.js.map +1 -1
- package/lib/summaryManager.d.ts +2 -2
- package/lib/summaryManager.d.ts.map +1 -1
- package/lib/summaryManager.js +22 -7
- package/lib/summaryManager.js.map +1 -1
- package/package.json +68 -25
- package/src/batchManager.ts +91 -0
- package/src/batchTracker.ts +2 -3
- package/src/blobManager.ts +385 -118
- package/src/containerRuntime.ts +523 -732
- package/src/dataStore.ts +49 -37
- package/src/dataStoreContext.ts +44 -56
- package/src/dataStores.ts +34 -30
- package/src/deltaScheduler.ts +6 -4
- package/src/garbageCollection.ts +296 -205
- package/src/gcSweepReadyUsageDetection.ts +147 -0
- package/src/index.ts +1 -2
- package/src/opProperties.ts +19 -0
- package/src/orderedClientElection.ts +31 -10
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +27 -59
- package/src/runningSummarizer.ts +76 -23
- package/src/scheduleManager.ts +314 -0
- package/src/summarizer.ts +1 -18
- package/src/summarizerHeuristics.ts +136 -19
- package/src/summarizerTypes.ts +53 -18
- package/src/summaryCollection.ts +33 -18
- package/src/summaryFormat.ts +0 -6
- package/src/summaryGenerator.ts +40 -22
- package/src/summaryManager.ts +22 -7
- package/dist/opTelemetry.d.ts +0 -22
- package/dist/opTelemetry.d.ts.map +0 -1
- package/dist/opTelemetry.js +0 -59
- package/dist/opTelemetry.js.map +0 -1
- package/lib/opTelemetry.d.ts +0 -22
- package/lib/opTelemetry.d.ts.map +0 -1
- package/lib/opTelemetry.js +0 -55
- package/lib/opTelemetry.js.map +0 -1
- package/src/opTelemetry.ts +0 -71
package/lib/containerRuntime.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AttachState, LoaderHeader, } from "@fluidframework/container-definitions";
|
|
2
|
-
import { assert, Trace, TypedEventEmitter, unreachableCase,
|
|
3
|
-
import { ChildLogger, raiseConnectedEvent, PerformanceEvent, TaggedLoggerAdapter, loggerToMonitoringContext,
|
|
4
|
-
import { DriverHeader } from "@fluidframework/driver-definitions";
|
|
5
|
-
import { readAndParse
|
|
6
|
-
import { DataCorruptionError, DataProcessingError, GenericError, UsageError,
|
|
2
|
+
import { assert, Trace, TypedEventEmitter, unreachableCase, } from "@fluidframework/common-utils";
|
|
3
|
+
import { ChildLogger, raiseConnectedEvent, PerformanceEvent, TaggedLoggerAdapter, loggerToMonitoringContext, wrapError, } from "@fluidframework/telemetry-utils";
|
|
4
|
+
import { DriverHeader, FetchSource, } from "@fluidframework/driver-definitions";
|
|
5
|
+
import { readAndParse } from "@fluidframework/driver-utils";
|
|
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";
|
|
9
9
|
import { addBlobToSummary, addSummarizeResultToSummary, addTreeToSummary, createRootSummarizerNodeWithGC, RequestParser, create404Response, exceptionToResponse, requestFluidObject, responseToException, seqFromTree, calculateStats, TelemetryContext, } from "@fluidframework/runtime-utils";
|
|
@@ -13,9 +13,9 @@ import { ContainerFluidHandleContext } from "./containerHandleContext";
|
|
|
13
13
|
import { FluidDataStoreRegistry } from "./dataStoreRegistry";
|
|
14
14
|
import { Summarizer } from "./summarizer";
|
|
15
15
|
import { SummaryManager } from "./summaryManager";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
16
|
+
import { ReportOpPerfTelemetry, } from "./connectionTelemetry";
|
|
17
|
+
import { PendingStateManager, } from "./pendingStateManager";
|
|
18
|
+
import { BatchManager } from "./batchManager";
|
|
19
19
|
import { pkgVersion } from "./packageVersion";
|
|
20
20
|
import { BlobManager } from "./blobManager";
|
|
21
21
|
import { DataStores, getSummaryForDatastores } from "./dataStores";
|
|
@@ -29,7 +29,7 @@ import { GarbageCollector, GCNodeType, gcTreeKey, } from "./garbageCollection";
|
|
|
29
29
|
import { channelToDataStore, isDataStoreAliasMessage, } from "./dataStore";
|
|
30
30
|
import { BindBatchTracker } from "./batchTracker";
|
|
31
31
|
import { SerializedSnapshotStorage } from "./serializedSnapshotStorage";
|
|
32
|
-
import {
|
|
32
|
+
import { ScheduleManager } from "./scheduleManager";
|
|
33
33
|
export var ContainerMessageType;
|
|
34
34
|
(function (ContainerMessageType) {
|
|
35
35
|
// An op to be delivered to store
|
|
@@ -47,14 +47,17 @@ export var ContainerMessageType;
|
|
|
47
47
|
})(ContainerMessageType || (ContainerMessageType = {}));
|
|
48
48
|
export const DefaultSummaryConfiguration = {
|
|
49
49
|
state: "enabled",
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
minIdleTime: 0,
|
|
51
|
+
maxIdleTime: 30 * 1000,
|
|
52
|
+
maxTime: 60 * 1000,
|
|
52
53
|
maxOps: 100,
|
|
53
54
|
minOpsForLastSummaryAttempt: 10,
|
|
54
|
-
maxAckWaitTime:
|
|
55
|
+
maxAckWaitTime: 10 * 60 * 1000,
|
|
55
56
|
maxOpsSinceLastSummary: 7000,
|
|
56
|
-
initialSummarizerDelayMs:
|
|
57
|
+
initialSummarizerDelayMs: 5 * 1000,
|
|
57
58
|
summarizerClientElection: false,
|
|
59
|
+
nonRuntimeOpWeight: 0.1,
|
|
60
|
+
runtimeOpWeight: 1.0,
|
|
58
61
|
};
|
|
59
62
|
/**
|
|
60
63
|
* Accepted header keys for requests coming to the runtime.
|
|
@@ -71,21 +74,11 @@ export var RuntimeHeaders;
|
|
|
71
74
|
/** True if the request is coming from an IFluidHandle. */
|
|
72
75
|
RuntimeHeaders["viaHandle"] = "viaHandle";
|
|
73
76
|
})(RuntimeHeaders || (RuntimeHeaders = {}));
|
|
74
|
-
const useDataStoreAliasingKey = "Fluid.ContainerRuntime.UseDataStoreAliasing";
|
|
75
77
|
const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
|
|
76
|
-
// Feature gate for the max op size. If the value is negative, chunking is enabled
|
|
77
|
-
// and all ops over 16k would be chunked. If the value is positive, all ops with
|
|
78
|
-
// a size strictly larger will be rejected and the container closed with an error.
|
|
79
|
-
const maxOpSizeInBytesKey = "Fluid.ContainerRuntime.MaxOpSizeInBytes";
|
|
80
|
-
// By default, we should reject any op larger than 768KB,
|
|
81
|
-
// in order to account for some extra overhead from serialization
|
|
82
|
-
// to not reach the 1MB limits in socket.io and Kafka.
|
|
83
|
-
const defaultMaxOpSizeInBytes = 768000;
|
|
84
|
-
// By default, the size of the contents for the incoming ops is tracked.
|
|
85
|
-
// However, in certain situations, this may incur a performance hit.
|
|
86
|
-
// The feature-gate below can be used to disable this feature.
|
|
87
|
-
const disableOpTrackingKey = "Fluid.ContainerRuntime.DisableOpTracking";
|
|
88
78
|
const defaultFlushMode = FlushMode.TurnBased;
|
|
79
|
+
/**
|
|
80
|
+
* @deprecated - use ContainerRuntimeMessage instead
|
|
81
|
+
*/
|
|
89
82
|
export var RuntimeMessage;
|
|
90
83
|
(function (RuntimeMessage) {
|
|
91
84
|
RuntimeMessage["FluidDataStoreOp"] = "component";
|
|
@@ -96,12 +89,24 @@ export var RuntimeMessage;
|
|
|
96
89
|
RuntimeMessage["Alias"] = "alias";
|
|
97
90
|
RuntimeMessage["Operation"] = "op";
|
|
98
91
|
})(RuntimeMessage || (RuntimeMessage = {}));
|
|
92
|
+
/**
|
|
93
|
+
* @deprecated - please use version in driver-utils
|
|
94
|
+
*/
|
|
99
95
|
export function isRuntimeMessage(message) {
|
|
100
96
|
if (Object.values(RuntimeMessage).includes(message.type)) {
|
|
101
97
|
return true;
|
|
102
98
|
}
|
|
103
99
|
return false;
|
|
104
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Unpacks runtime messages
|
|
103
|
+
*
|
|
104
|
+
* @remarks This API makes no promises regarding backward-compatability. This is internal API.
|
|
105
|
+
* @param message - message (as it observed in storage / service)
|
|
106
|
+
* @returns unpacked runtime message
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
105
110
|
export function unpackRuntimeMessage(message) {
|
|
106
111
|
if (message.type === MessageType.Operation) {
|
|
107
112
|
// legacy op format?
|
|
@@ -115,236 +120,14 @@ export function unpackRuntimeMessage(message) {
|
|
|
115
120
|
message.type = innerContents.type;
|
|
116
121
|
message.contents = innerContents.contents;
|
|
117
122
|
}
|
|
118
|
-
|
|
123
|
+
return true;
|
|
119
124
|
}
|
|
120
125
|
else {
|
|
121
126
|
// Legacy format, but it's already "unpacked",
|
|
122
127
|
// i.e. message.type is actually ContainerMessageType.
|
|
128
|
+
// Or it's non-runtime message.
|
|
123
129
|
// Nothing to do in such case.
|
|
124
|
-
|
|
125
|
-
return message;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* This class controls pausing and resuming of inbound queue to ensure that we never
|
|
129
|
-
* start processing ops in a batch IF we do not have all ops in the batch.
|
|
130
|
-
*/
|
|
131
|
-
class ScheduleManagerCore {
|
|
132
|
-
constructor(deltaManager, logger) {
|
|
133
|
-
this.deltaManager = deltaManager;
|
|
134
|
-
this.logger = logger;
|
|
135
|
-
this.localPaused = false;
|
|
136
|
-
this.timePaused = 0;
|
|
137
|
-
this.batchCount = 0;
|
|
138
|
-
// Listen for delta manager sends and add batch metadata to messages
|
|
139
|
-
this.deltaManager.on("prepareSend", (messages) => {
|
|
140
|
-
if (messages.length === 0) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
// First message will have the batch flag set to true if doing a batched send
|
|
144
|
-
const firstMessageMetadata = messages[0].metadata;
|
|
145
|
-
if (!(firstMessageMetadata === null || firstMessageMetadata === void 0 ? void 0 : firstMessageMetadata.batch)) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
// If the batch contains only a single op, clear the batch flag.
|
|
149
|
-
if (messages.length === 1) {
|
|
150
|
-
delete firstMessageMetadata.batch;
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
// Set the batch flag to false on the last message to indicate the end of the send batch
|
|
154
|
-
const lastMessage = messages[messages.length - 1];
|
|
155
|
-
lastMessage.metadata = Object.assign(Object.assign({}, lastMessage.metadata), { batch: false });
|
|
156
|
-
});
|
|
157
|
-
// Listen for updates and peek at the inbound
|
|
158
|
-
this.deltaManager.inbound.on("push", (message) => {
|
|
159
|
-
this.trackPending(message);
|
|
160
|
-
});
|
|
161
|
-
// Start with baseline - empty inbound queue.
|
|
162
|
-
assert(!this.localPaused, 0x293 /* "initial state" */);
|
|
163
|
-
const allPending = this.deltaManager.inbound.toArray();
|
|
164
|
-
for (const pending of allPending) {
|
|
165
|
-
this.trackPending(pending);
|
|
166
|
-
}
|
|
167
|
-
// We are intentionally directly listening to the "op" to inspect system ops as well.
|
|
168
|
-
// If we do not observe system ops, we are likely to hit 0x296 assert when system ops
|
|
169
|
-
// precedes start of incomplete batch.
|
|
170
|
-
this.deltaManager.on("op", (message) => this.afterOpProcessing(message.sequenceNumber));
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* The only public function in this class - called when we processed an op,
|
|
174
|
-
* to make decision if op processing should be paused or not afer that.
|
|
175
|
-
*/
|
|
176
|
-
afterOpProcessing(sequenceNumber) {
|
|
177
|
-
assert(!this.localPaused, 0x294 /* "can't have op processing paused if we are processing an op" */);
|
|
178
|
-
// If the inbound queue is ever empty, nothing to do!
|
|
179
|
-
if (this.deltaManager.inbound.length === 0) {
|
|
180
|
-
assert(this.pauseSequenceNumber === undefined, 0x295 /* "there should be no pending batch if we have no ops" */);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
// The queue is
|
|
184
|
-
// 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
|
|
185
|
-
// - here (processing ops until reaching start of incomplete batch)
|
|
186
|
-
// - in trackPending(), when queue was empty and start of batch showed up.
|
|
187
|
-
// 2. resumed when batch end comes in (in trackPending())
|
|
188
|
-
// do we have incomplete batch to worry about?
|
|
189
|
-
if (this.pauseSequenceNumber !== undefined) {
|
|
190
|
-
assert(sequenceNumber < this.pauseSequenceNumber, 0x296 /* "we should never start processing incomplete batch!" */);
|
|
191
|
-
// If the next op is the start of incomplete batch, then we can't process it until it's fully in - pause!
|
|
192
|
-
if (sequenceNumber + 1 === this.pauseSequenceNumber) {
|
|
193
|
-
this.pauseQueue();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
pauseQueue() {
|
|
198
|
-
assert(!this.localPaused, 0x297 /* "always called from resumed state" */);
|
|
199
|
-
this.localPaused = true;
|
|
200
|
-
this.timePaused = performance.now();
|
|
201
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
202
|
-
this.deltaManager.inbound.pause();
|
|
203
|
-
}
|
|
204
|
-
resumeQueue(startBatch, messageEndBatch) {
|
|
205
|
-
const endBatch = messageEndBatch.sequenceNumber;
|
|
206
|
-
const duration = this.localPaused ? (performance.now() - this.timePaused) : undefined;
|
|
207
|
-
this.batchCount++;
|
|
208
|
-
if (this.batchCount % 1000 === 1) {
|
|
209
|
-
this.logger.sendTelemetryEvent({
|
|
210
|
-
eventName: "BatchStats",
|
|
211
|
-
sequenceNumber: endBatch,
|
|
212
|
-
length: endBatch - startBatch + 1,
|
|
213
|
-
msnDistance: endBatch - messageEndBatch.minimumSequenceNumber,
|
|
214
|
-
duration,
|
|
215
|
-
batchCount: this.batchCount,
|
|
216
|
-
interrupted: this.localPaused,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
// Return early if no change in value
|
|
220
|
-
if (!this.localPaused) {
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
this.localPaused = false;
|
|
224
|
-
// Random round number - we want to know when batch waiting paused op processing.
|
|
225
|
-
if (duration !== undefined && duration > latencyThreshold) {
|
|
226
|
-
this.logger.sendErrorEvent({
|
|
227
|
-
eventName: "MaxBatchWaitTimeExceeded",
|
|
228
|
-
duration,
|
|
229
|
-
sequenceNumber: endBatch,
|
|
230
|
-
length: endBatch - startBatch,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
this.deltaManager.inbound.resume();
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Called for each incoming op (i.e. inbound "push" notification)
|
|
237
|
-
*/
|
|
238
|
-
trackPending(message) {
|
|
239
|
-
assert(this.deltaManager.inbound.length !== 0, 0x298 /* "we have something in the queue that generates this event" */);
|
|
240
|
-
assert((this.currentBatchClientId === undefined) === (this.pauseSequenceNumber === undefined), 0x299 /* "non-synchronized state" */);
|
|
241
|
-
const metadata = message.metadata;
|
|
242
|
-
const batchMetadata = metadata === null || metadata === void 0 ? void 0 : metadata.batch;
|
|
243
|
-
// Protocol messages are never part of a runtime batch of messages
|
|
244
|
-
if (!isUnpackedRuntimeMessage(message)) {
|
|
245
|
-
// Protocol messages should never show up in the middle of the batch!
|
|
246
|
-
assert(this.currentBatchClientId === undefined, 0x29a /* "System message in the middle of batch!" */);
|
|
247
|
-
assert(batchMetadata === undefined, 0x29b /* "system op in a batch?" */);
|
|
248
|
-
assert(!this.localPaused, 0x29c /* "we should be processing ops when there is no active batch" */);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
if (this.currentBatchClientId === undefined && batchMetadata === undefined) {
|
|
252
|
-
assert(!this.localPaused, 0x29d /* "we should be processing ops when there is no active batch" */);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
// If the client ID changes then we can move the pause point. If it stayed the same then we need to check.
|
|
256
|
-
// If batchMetadata is not undefined then if it's true we've begun a new batch - if false we've ended
|
|
257
|
-
// the previous one
|
|
258
|
-
if (this.currentBatchClientId !== undefined || batchMetadata === false) {
|
|
259
|
-
if (this.currentBatchClientId !== message.clientId) {
|
|
260
|
-
// "Batch not closed, yet message from another client!"
|
|
261
|
-
throw new DataCorruptionError("OpBatchIncomplete", Object.assign({ runtimeVersion: pkgVersion, batchClientId: this.currentBatchClientId }, extractSafePropertiesFromMessage(message)));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// The queue is
|
|
265
|
-
// 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
|
|
266
|
-
// - in afterOpProcessing() - processing ops until reaching start of incomplete batch
|
|
267
|
-
// - here (batchMetadata == false below), when queue was empty and start of batch showed up.
|
|
268
|
-
// 2. resumed when batch end comes in (batchMetadata === true case below)
|
|
269
|
-
if (batchMetadata) {
|
|
270
|
-
assert(this.currentBatchClientId === undefined, 0x29e /* "there can't be active batch" */);
|
|
271
|
-
assert(!this.localPaused, 0x29f /* "we should be processing ops when there is no active batch" */);
|
|
272
|
-
this.pauseSequenceNumber = message.sequenceNumber;
|
|
273
|
-
this.currentBatchClientId = message.clientId;
|
|
274
|
-
// Start of the batch
|
|
275
|
-
// Only pause processing if queue has no other ops!
|
|
276
|
-
// If there are any other ops in the queue, processing will be stopped when they are processed!
|
|
277
|
-
if (this.deltaManager.inbound.length === 1) {
|
|
278
|
-
this.pauseQueue();
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
else if (batchMetadata === false) {
|
|
282
|
-
assert(this.pauseSequenceNumber !== undefined, 0x2a0 /* "batch presence was validated above" */);
|
|
283
|
-
// Batch is complete, we can process it!
|
|
284
|
-
this.resumeQueue(this.pauseSequenceNumber, message);
|
|
285
|
-
this.pauseSequenceNumber = undefined;
|
|
286
|
-
this.currentBatchClientId = undefined;
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
// Continuation of current batch. Do nothing
|
|
290
|
-
assert(this.currentBatchClientId !== undefined, 0x2a1 /* "logic error" */);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* This class has the following responsibilities:
|
|
296
|
-
* 1. It tracks batches as we process ops and raises "batchBegin" and "batchEnd" events.
|
|
297
|
-
* As part of it, it validates batch correctness (i.e. no system ops in the middle of batch)
|
|
298
|
-
* 2. It creates instance of ScheduleManagerCore that ensures we never start processing ops from batch
|
|
299
|
-
* unless all ops of the batch are in.
|
|
300
|
-
*/
|
|
301
|
-
export class ScheduleManager {
|
|
302
|
-
constructor(deltaManager, emitter, logger) {
|
|
303
|
-
this.deltaManager = deltaManager;
|
|
304
|
-
this.emitter = emitter;
|
|
305
|
-
this.logger = logger;
|
|
306
|
-
this.hitError = false;
|
|
307
|
-
this.deltaScheduler = new DeltaScheduler(this.deltaManager, ChildLogger.create(this.logger, "DeltaScheduler"));
|
|
308
|
-
void new ScheduleManagerCore(deltaManager, logger);
|
|
309
|
-
}
|
|
310
|
-
beforeOpProcessing(message) {
|
|
311
|
-
var _a;
|
|
312
|
-
if (this.batchClientId !== message.clientId) {
|
|
313
|
-
assert(this.batchClientId === undefined, 0x2a2 /* "Batch is interrupted by other client op. Should be caught by trackPending()" */);
|
|
314
|
-
// This could be the beginning of a new batch or an individual message.
|
|
315
|
-
this.emitter.emit("batchBegin", message);
|
|
316
|
-
this.deltaScheduler.batchBegin(message);
|
|
317
|
-
const batch = (_a = message === null || message === void 0 ? void 0 : message.metadata) === null || _a === void 0 ? void 0 : _a.batch;
|
|
318
|
-
if (batch) {
|
|
319
|
-
this.batchClientId = message.clientId;
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
this.batchClientId = undefined;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
afterOpProcessing(error, message) {
|
|
327
|
-
var _a;
|
|
328
|
-
// If this is no longer true, we need to revisit what we do where we set this.hitError.
|
|
329
|
-
assert(!this.hitError, 0x2a3 /* "container should be closed on any error" */);
|
|
330
|
-
if (error) {
|
|
331
|
-
// We assume here that loader will close container and stop processing all future ops.
|
|
332
|
-
// This is implicit dependency. If this flow changes, this code might no longer be correct.
|
|
333
|
-
this.hitError = true;
|
|
334
|
-
this.batchClientId = undefined;
|
|
335
|
-
this.emitter.emit("batchEnd", error, message);
|
|
336
|
-
this.deltaScheduler.batchEnd(message);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
const batch = (_a = message === null || message === void 0 ? void 0 : message.metadata) === null || _a === void 0 ? void 0 : _a.batch;
|
|
340
|
-
// If no batchClientId has been set then we're in an individual batch. Else, if we get
|
|
341
|
-
// batch end metadata, this is end of the current batch.
|
|
342
|
-
if (this.batchClientId === undefined || batch === false) {
|
|
343
|
-
this.batchClientId = undefined;
|
|
344
|
-
this.emitter.emit("batchEnd", undefined, message);
|
|
345
|
-
this.deltaScheduler.batchEnd(message);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
130
|
+
return false;
|
|
348
131
|
}
|
|
349
132
|
}
|
|
350
133
|
/**
|
|
@@ -373,7 +156,7 @@ export function getDeviceSpec() {
|
|
|
373
156
|
*/
|
|
374
157
|
export class ContainerRuntime extends TypedEventEmitter {
|
|
375
158
|
constructor(context, registry, metadata, electedSummarizerData, chunks, dataStoreAliasMap, runtimeOptions, containerScope, logger, existing, blobManagerSnapshot, _storage, requestHandler, summaryConfiguration) {
|
|
376
|
-
var _a, _b, _c, _d
|
|
159
|
+
var _a, _b, _c, _d;
|
|
377
160
|
if (summaryConfiguration === void 0) { summaryConfiguration = Object.assign(Object.assign({}, DefaultSummaryConfiguration), (_a = runtimeOptions.summaryOptions) === null || _a === void 0 ? void 0 : _a.summaryConfigOverrides); }
|
|
378
161
|
super();
|
|
379
162
|
this.context = context;
|
|
@@ -384,9 +167,8 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
384
167
|
this._storage = _storage;
|
|
385
168
|
this.requestHandler = requestHandler;
|
|
386
169
|
this.summaryConfiguration = summaryConfiguration;
|
|
387
|
-
this.defaultMaxConsecutiveReconnects =
|
|
170
|
+
this.defaultMaxConsecutiveReconnects = 7;
|
|
388
171
|
this._orderSequentiallyCalls = 0;
|
|
389
|
-
this.needsFlush = false;
|
|
390
172
|
this.flushTrigger = false;
|
|
391
173
|
this.savedOps = [];
|
|
392
174
|
this.consecutiveReconnects = 0;
|
|
@@ -399,6 +181,12 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
399
181
|
signalTimestamp: 0,
|
|
400
182
|
trackingSignalSequenceNumber: undefined,
|
|
401
183
|
};
|
|
184
|
+
// Provide lower soft limit - we want to have some number of ops to get efficiency in compression & bandwidth usage,
|
|
185
|
+
// but at the same time we want to send these ops sooner, to reduce overall latency of processing a batch.
|
|
186
|
+
// So there is some ballance here, that depends on compression algorithm and its efficiency working with smaller
|
|
187
|
+
// payloads. That number represents final (compressed) bits (once compression is implemented).
|
|
188
|
+
this.pendingAttachBatch = new BatchManager(64 * 1024);
|
|
189
|
+
this.pendingBatch = new BatchManager();
|
|
402
190
|
this.summarizeOnDemand = (...args) => {
|
|
403
191
|
if (this.clientDetails.type === summarizerClientType) {
|
|
404
192
|
return this.summarizer.summarizeOnDemand(...args);
|
|
@@ -428,26 +216,23 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
428
216
|
}
|
|
429
217
|
};
|
|
430
218
|
this.messageAtLastSummary = metadata === null || metadata === void 0 ? void 0 : metadata.message;
|
|
431
|
-
// Default to false (enabled).
|
|
432
|
-
this.disableIsolatedChannels = (_b = this.runtimeOptions.summaryOptions.disableIsolatedChannels) !== null && _b !== void 0 ? _b : false;
|
|
433
219
|
this._connected = this.context.connected;
|
|
434
220
|
this.chunkMap = new Map(chunks);
|
|
435
221
|
this.handleContext = new ContainerFluidHandleContext("", this);
|
|
436
222
|
this.mc = loggerToMonitoringContext(ChildLogger.create(this.logger, "ContainerRuntime"));
|
|
223
|
+
if (this.summaryConfiguration.state === "enabled") {
|
|
224
|
+
this.validateSummaryHeuristicConfiguration(this.summaryConfiguration);
|
|
225
|
+
}
|
|
437
226
|
this.summariesDisabled = this.isSummariesDisabled();
|
|
438
227
|
this.heuristicsDisabled = this.isHeuristicsDisabled();
|
|
439
228
|
this.summarizerClientElectionEnabled = this.isSummarizerClientElectionEnabled();
|
|
440
229
|
this.maxOpsSinceLastSummary = this.getMaxOpsSinceLastSummary();
|
|
441
230
|
this.initialSummarizerDelayMs = this.getInitialSummarizerDelayMs();
|
|
442
|
-
this._aliasingEnabled =
|
|
443
|
-
((_c = this.mc.config.getBoolean(useDataStoreAliasingKey)) !== null && _c !== void 0 ? _c : false) ||
|
|
444
|
-
((_d = runtimeOptions.useDataStoreAliasing) !== null && _d !== void 0 ? _d : false);
|
|
445
|
-
this._maxOpSizeInBytes = ((_e = this.mc.config.getNumber(maxOpSizeInBytesKey)) !== null && _e !== void 0 ? _e : defaultMaxOpSizeInBytes);
|
|
446
231
|
this.maxConsecutiveReconnects =
|
|
447
|
-
(
|
|
232
|
+
(_b = this.mc.config.getNumber(maxConsecutiveReconnectsKey)) !== null && _b !== void 0 ? _b : this.defaultMaxConsecutiveReconnects;
|
|
448
233
|
this._flushMode = runtimeOptions.flushMode;
|
|
449
234
|
const pendingRuntimeState = context.pendingLocalState;
|
|
450
|
-
const baseSnapshot = (
|
|
235
|
+
const baseSnapshot = (_c = pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.baseSnapshot) !== null && _c !== void 0 ? _c : context.baseSnapshot;
|
|
451
236
|
this.garbageCollector = GarbageCollector.create({
|
|
452
237
|
runtime: this,
|
|
453
238
|
gcOptions: this.runtimeOptions.gcOptions,
|
|
@@ -459,6 +244,8 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
459
244
|
getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
|
|
460
245
|
getLastSummaryTimestampMs: () => { var _a; return (_a = this.messageAtLastSummary) === null || _a === void 0 ? void 0 : _a.timestamp; },
|
|
461
246
|
readAndParseBlob: async (id) => readAndParse(this.storage, id),
|
|
247
|
+
getContainerDiagnosticId: () => this.context.id,
|
|
248
|
+
activeConnection: () => this.deltaManager.active,
|
|
462
249
|
});
|
|
463
250
|
const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
|
|
464
251
|
this.summarizerNode = createRootSummarizerNodeWithGC(ChildLogger.create(this.logger, "SummarizerNode"),
|
|
@@ -481,8 +268,12 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
481
268
|
this.summarizerNode.loadBaseSummaryWithoutDifferential(baseSnapshot);
|
|
482
269
|
}
|
|
483
270
|
this.dataStores = new DataStores(getSummaryForDatastores(baseSnapshot, metadata), this, (attachMsg) => this.submit(ContainerMessageType.Attach, attachMsg), (id, createParam) => (summarizeInternal, getGCDataFn, getBaseGCDetailsFn) => this.summarizerNode.createChild(summarizeInternal, id, createParam, undefined, getGCDataFn, getBaseGCDetailsFn), (id) => this.summarizerNode.deleteChild(id), this.mc.logger, async () => this.garbageCollector.getBaseGCDetails(), (path, timestampMs, packagePath) => this.garbageCollector.nodeUpdated(path, "Changed", timestampMs, packagePath), new Map(dataStoreAliasMap), this.garbageCollector.writeDataAtRoot);
|
|
484
|
-
this.blobManager = new BlobManager(this.handleContext, blobManagerSnapshot, () => this.storage, (blobId
|
|
485
|
-
|
|
271
|
+
this.blobManager = new BlobManager(this.handleContext, blobManagerSnapshot, () => this.storage, (blobId, localId) => {
|
|
272
|
+
if (!this.disposed) {
|
|
273
|
+
this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId, localId });
|
|
274
|
+
}
|
|
275
|
+
}, (blobPath) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"), this, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pendingAttachmentBlobs);
|
|
276
|
+
this.scheduleManager = new ScheduleManager(context.deltaManager, this, () => this.clientId, ChildLogger.create(this.logger, "ScheduleManager"));
|
|
486
277
|
this.deltaSender = this.deltaManager;
|
|
487
278
|
this.pendingStateManager = new PendingStateManager({
|
|
488
279
|
applyStashedOp: this.applyStashedOp.bind(this),
|
|
@@ -492,7 +283,6 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
492
283
|
flush: this.flush.bind(this),
|
|
493
284
|
flushMode: () => this.flushMode,
|
|
494
285
|
reSubmit: this.reSubmit.bind(this),
|
|
495
|
-
rollback: this.rollback.bind(this),
|
|
496
286
|
setFlushMode: (mode) => this.setFlushMode(mode),
|
|
497
287
|
}, this._flushMode, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pending);
|
|
498
288
|
this.context.quorum.on("removeMember", (clientId) => {
|
|
@@ -572,9 +362,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
572
362
|
createContainerRuntimeVersion: metadata === null || metadata === void 0 ? void 0 : metadata.createContainerRuntimeVersion,
|
|
573
363
|
createContainerTimestamp: metadata === null || metadata === void 0 ? void 0 : metadata.createContainerTimestamp,
|
|
574
364
|
};
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
loadSummaryNumber = (
|
|
365
|
+
// summaryNumber was renamed from summaryCount. For older docs that haven't been opened for a long time,
|
|
366
|
+
// the count is reset to 0.
|
|
367
|
+
loadSummaryNumber = (_d = metadata === null || metadata === void 0 ? void 0 : metadata.summaryNumber) !== null && _d !== void 0 ? _d : 0;
|
|
578
368
|
}
|
|
579
369
|
else {
|
|
580
370
|
this.createContainerMetadata = {
|
|
@@ -587,7 +377,6 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
587
377
|
this.logger.sendTelemetryEvent(Object.assign(Object.assign(Object.assign({ eventName: "ContainerLoadStats" }, this.createContainerMetadata), this.dataStores.containerLoadStats), { summaryNumber: loadSummaryNumber, summaryFormatVersion: metadata === null || metadata === void 0 ? void 0 : metadata.summaryFormatVersion, disableIsolatedChannels: metadata === null || metadata === void 0 ? void 0 : metadata.disableIsolatedChannels, gcVersion: metadata === null || metadata === void 0 ? void 0 : metadata.gcFeature }));
|
|
588
378
|
ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
|
|
589
379
|
BindBatchTracker(this, this.logger);
|
|
590
|
-
this.opTracker = new OpTracker(this.deltaManager, this.mc.config.getBoolean(disableOpTrackingKey) === true);
|
|
591
380
|
}
|
|
592
381
|
get IContainerRuntime() { return this; }
|
|
593
382
|
get IFluidRouter() { return this; }
|
|
@@ -610,7 +399,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
610
399
|
runtimeVersion: pkgVersion,
|
|
611
400
|
},
|
|
612
401
|
});
|
|
613
|
-
const { summaryOptions = {}, gcOptions = {}, loadSequenceNumberVerification = "close",
|
|
402
|
+
const { summaryOptions = {}, gcOptions = {}, loadSequenceNumberVerification = "close", flushMode = defaultFlushMode, enableOfflineLoad = false, } = runtimeOptions;
|
|
614
403
|
const pendingRuntimeState = context.pendingLocalState;
|
|
615
404
|
const baseSnapshot = (_b = pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.baseSnapshot) !== null && _b !== void 0 ? _b : context.baseSnapshot;
|
|
616
405
|
const storage = !pendingRuntimeState ?
|
|
@@ -663,7 +452,6 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
663
452
|
summaryOptions,
|
|
664
453
|
gcOptions,
|
|
665
454
|
loadSequenceNumberVerification,
|
|
666
|
-
useDataStoreAliasing,
|
|
667
455
|
flushMode,
|
|
668
456
|
enableOfflineLoad,
|
|
669
457
|
}, containerScope, logger, loadExisting, blobManagerSnapshot, storage, requestHandler);
|
|
@@ -721,6 +509,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
721
509
|
return (_a = this.summarizerClientElection) === null || _a === void 0 ? void 0 : _a.electedClientId;
|
|
722
510
|
}
|
|
723
511
|
get disposed() { return this._disposed; }
|
|
512
|
+
get emptyBatch() {
|
|
513
|
+
return this.pendingBatch.empty && this.pendingAttachBatch.empty;
|
|
514
|
+
}
|
|
724
515
|
get summarizer() {
|
|
725
516
|
assert(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
|
|
726
517
|
return this._summarizer;
|
|
@@ -752,12 +543,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
752
543
|
if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
|
|
753
544
|
return true;
|
|
754
545
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
else {
|
|
759
|
-
return false;
|
|
760
|
-
}
|
|
546
|
+
return this.summaryConfiguration.state !== "disabled"
|
|
547
|
+
? this.summaryConfiguration.summarizerClientElection === true
|
|
548
|
+
: false;
|
|
761
549
|
}
|
|
762
550
|
getMaxOpsSinceLastSummary() {
|
|
763
551
|
// back-compat: maxOpsSinceLastSummary was moved from ISummaryRuntimeOptions
|
|
@@ -765,12 +553,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
765
553
|
if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
|
|
766
554
|
return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
|
|
767
555
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
else {
|
|
772
|
-
return 0;
|
|
773
|
-
}
|
|
556
|
+
return this.summaryConfiguration.state !== "disabled"
|
|
557
|
+
? this.summaryConfiguration.maxOpsSinceLastSummary
|
|
558
|
+
: 0;
|
|
774
559
|
}
|
|
775
560
|
getInitialSummarizerDelayMs() {
|
|
776
561
|
// back-compat: initialSummarizerDelayMs was moved from ISummaryRuntimeOptions
|
|
@@ -778,12 +563,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
778
563
|
if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
|
|
779
564
|
return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
|
|
780
565
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
else {
|
|
785
|
-
return 0;
|
|
786
|
-
}
|
|
566
|
+
return this.summaryConfiguration.state !== "disabled"
|
|
567
|
+
? this.summaryConfiguration.initialSummarizerDelayMs
|
|
568
|
+
: 0;
|
|
787
569
|
}
|
|
788
570
|
dispose(error) {
|
|
789
571
|
var _a;
|
|
@@ -856,17 +638,13 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
856
638
|
return this.resolveHandle(requestParser.createSubRequest(1));
|
|
857
639
|
}
|
|
858
640
|
if (id === BlobManager.basePath && requestParser.isLeaf(2)) {
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
641
|
+
const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
|
|
642
|
+
return blob
|
|
643
|
+
? {
|
|
862
644
|
status: 200,
|
|
863
645
|
mimeType: "fluid/object",
|
|
864
|
-
value:
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
else {
|
|
868
|
-
return create404Response(request);
|
|
869
|
-
}
|
|
646
|
+
value: blob,
|
|
647
|
+
} : create404Response(request);
|
|
870
648
|
}
|
|
871
649
|
else if (requestParser.pathParts.length > 0) {
|
|
872
650
|
const dataStore = await this.getDataStoreFromRequest(id, request);
|
|
@@ -884,13 +662,14 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
884
662
|
}
|
|
885
663
|
internalId(maybeAlias) {
|
|
886
664
|
var _a;
|
|
887
|
-
return (_a = this.dataStores.aliases
|
|
665
|
+
return (_a = this.dataStores.aliases.get(maybeAlias)) !== null && _a !== void 0 ? _a : maybeAlias;
|
|
888
666
|
}
|
|
889
667
|
async getDataStoreFromRequest(id, request) {
|
|
890
668
|
var _a, _b, _c;
|
|
891
669
|
const wait = typeof ((_a = request.headers) === null || _a === void 0 ? void 0 : _a[RuntimeHeaders.wait]) === "boolean"
|
|
892
670
|
? (_b = request.headers) === null || _b === void 0 ? void 0 : _b[RuntimeHeaders.wait]
|
|
893
671
|
: true;
|
|
672
|
+
await this.dataStores.waitIfPendingAlias(id);
|
|
894
673
|
const internalId = this.internalId(id);
|
|
895
674
|
const dataStoreContext = await this.dataStores.getDataStore(internalId, wait);
|
|
896
675
|
/**
|
|
@@ -920,10 +699,8 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
920
699
|
addMetadataToSummary(summaryTree) {
|
|
921
700
|
var _a;
|
|
922
701
|
const metadata = Object.assign(Object.assign(Object.assign(Object.assign({}, this.createContainerMetadata), {
|
|
923
|
-
// back-compat 0.59.3000: This is renamed to summaryNumber. Can be removed when 0.59.3000 saturates.
|
|
924
|
-
summaryCount: this.nextSummaryNumber,
|
|
925
702
|
// Increment the summary number for the next summary that will be generated.
|
|
926
|
-
summaryNumber: this.nextSummaryNumber++, summaryFormatVersion: 1
|
|
703
|
+
summaryNumber: this.nextSummaryNumber++, summaryFormatVersion: 1 }), this.garbageCollector.getMetadata()), {
|
|
927
704
|
// The last message processed at the time of summary. If there are no new messages, use the message from the
|
|
928
705
|
// last summary.
|
|
929
706
|
message: (_a = extractSummaryMetadataMessage(this.deltaManager.lastMessage)) !== null && _a !== void 0 ? _a : this.messageAtLastSummary });
|
|
@@ -936,7 +713,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
936
713
|
const content = JSON.stringify([...this.chunkMap]);
|
|
937
714
|
addBlobToSummary(summaryTree, chunksBlobName, content);
|
|
938
715
|
}
|
|
939
|
-
const dataStoreAliases = this.dataStores.aliases
|
|
716
|
+
const dataStoreAliases = this.dataStores.aliases;
|
|
940
717
|
if (dataStoreAliases.size > 0) {
|
|
941
718
|
addBlobToSummary(summaryTree, aliasBlobName, JSON.stringify([...dataStoreAliases]));
|
|
942
719
|
}
|
|
@@ -967,7 +744,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
967
744
|
// Feature disabled, we never stop reconnecting
|
|
968
745
|
return true;
|
|
969
746
|
}
|
|
970
|
-
if (!this.
|
|
747
|
+
if (!this.hasPendingMessages()) {
|
|
971
748
|
// If there are no pending messages, we can always reconnect
|
|
972
749
|
this.resetReconnectCount();
|
|
973
750
|
return true;
|
|
@@ -1033,22 +810,55 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1033
810
|
}
|
|
1034
811
|
}
|
|
1035
812
|
setConnectionState(connected, clientId) {
|
|
813
|
+
if (connected === false && this.delayConnectClientId !== undefined) {
|
|
814
|
+
this.delayConnectClientId = undefined;
|
|
815
|
+
this.mc.logger.sendTelemetryEvent({
|
|
816
|
+
eventName: "UnsuccessfulConnectedTransition",
|
|
817
|
+
});
|
|
818
|
+
// Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
// If attachment blobs were added while disconnected, we need to delay
|
|
822
|
+
// propagation of the "connected" event until we have uploaded them to
|
|
823
|
+
// ensure we don't submit ops referencing a blob that has not been uploaded
|
|
824
|
+
const connecting = connected && !this._connected && !this.deltaManager.readOnlyInfo.readonly;
|
|
825
|
+
if (connecting && this.blobManager.hasPendingOfflineUploads) {
|
|
826
|
+
assert(!this.delayConnectClientId, 0x392 /* Connect event delay must be canceled before subsequent connect event */);
|
|
827
|
+
assert(!!clientId, 0x393 /* Must have clientId when connecting */);
|
|
828
|
+
this.delayConnectClientId = clientId;
|
|
829
|
+
this.blobManager.onConnected().then(() => {
|
|
830
|
+
// make sure we didn't reconnect before the promise resolved
|
|
831
|
+
if (this.delayConnectClientId === clientId && !this.disposed) {
|
|
832
|
+
this.delayConnectClientId = undefined;
|
|
833
|
+
this.setConnectionStateCore(connected, clientId);
|
|
834
|
+
}
|
|
835
|
+
}, (error) => this.closeFn(error));
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
this.setConnectionStateCore(connected, clientId);
|
|
839
|
+
}
|
|
840
|
+
setConnectionStateCore(connected, clientId) {
|
|
841
|
+
assert(!this.delayConnectClientId, 0x394 /* connect event delay must be cleared before propagating connect event */);
|
|
1036
842
|
this.verifyNotClosed();
|
|
1037
843
|
// There might be no change of state due to Container calling this API after loading runtime.
|
|
1038
844
|
const changeOfState = this._connected !== connected;
|
|
1039
|
-
const reconnection = changeOfState && connected;
|
|
845
|
+
const reconnection = changeOfState && !connected;
|
|
1040
846
|
this._connected = connected;
|
|
1041
847
|
if (!connected) {
|
|
1042
848
|
this._perfSignalData.signalsLost = 0;
|
|
1043
849
|
this._perfSignalData.signalTimestamp = 0;
|
|
1044
850
|
this._perfSignalData.trackingSignalSequenceNumber = undefined;
|
|
1045
851
|
}
|
|
852
|
+
else {
|
|
853
|
+
assert(this.attachState === AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
|
|
854
|
+
}
|
|
855
|
+
// Fail while disconnected
|
|
1046
856
|
if (reconnection) {
|
|
1047
857
|
this.consecutiveReconnects++;
|
|
1048
858
|
if (!this.shouldContinueReconnecting()) {
|
|
1049
|
-
this.closeFn(
|
|
1050
|
-
//
|
|
1051
|
-
|
|
859
|
+
this.closeFn(DataProcessingError.create(
|
|
860
|
+
// eslint-disable-next-line max-len
|
|
861
|
+
"Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)", "setConnectionState", undefined, {
|
|
1052
862
|
dataLoss: 1,
|
|
1053
863
|
attempts: this.consecutiveReconnects,
|
|
1054
864
|
pendingMessages: this.pendingStateManager.pendingMessagesCount,
|
|
@@ -1060,46 +870,48 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1060
870
|
this.replayPendingStates();
|
|
1061
871
|
}
|
|
1062
872
|
this.dataStores.setConnectionState(connected, clientId);
|
|
873
|
+
this.garbageCollector.setConnectionState(connected, clientId);
|
|
1063
874
|
raiseConnectedEvent(this.mc.logger, this, connected, clientId);
|
|
1064
875
|
}
|
|
1065
876
|
process(messageArg, local) {
|
|
1066
|
-
var _a
|
|
877
|
+
var _a;
|
|
1067
878
|
this.verifyNotClosed();
|
|
1068
|
-
// If it's not message for runtime, bail out right away.
|
|
1069
|
-
if (!isUnpackedRuntimeMessage(messageArg)) {
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
if ((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad) {
|
|
1073
|
-
this.savedOps.push(messageArg);
|
|
1074
|
-
}
|
|
1075
879
|
// Do shallow copy of message, as methods below will modify it.
|
|
1076
880
|
// There might be multiple container instances receiving same message
|
|
1077
881
|
// We do not need to make deep copy, as each layer will just replace message.content itself,
|
|
1078
882
|
// but would not modify contents details
|
|
1079
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
|
+
}
|
|
1080
896
|
// Surround the actual processing of the operation with messages to the schedule manager indicating
|
|
1081
897
|
// the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
|
|
1082
898
|
// messages once a batch has been fully processed.
|
|
1083
899
|
this.scheduleManager.beforeOpProcessing(message);
|
|
1084
900
|
try {
|
|
1085
|
-
message = unpackRuntimeMessage(message);
|
|
1086
901
|
// Chunk processing must come first given that we will transform the message to the unchunked version
|
|
1087
902
|
// once all pieces are available
|
|
1088
903
|
message = this.processRemoteChunkedMessage(message);
|
|
1089
904
|
let localOpMetadata;
|
|
1090
|
-
if (local) {
|
|
1091
|
-
|
|
1092
|
-
// Do not process local chunked ops until all pieces are available.
|
|
1093
|
-
if (message.type !== ContainerMessageType.ChunkedOp) {
|
|
1094
|
-
localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
|
|
1095
|
-
}
|
|
905
|
+
if (local && runtimeMessage) {
|
|
906
|
+
localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
|
|
1096
907
|
}
|
|
1097
908
|
// If there are no more pending messages after processing a local message,
|
|
1098
909
|
// the document is no longer dirty.
|
|
1099
|
-
if (!this.
|
|
910
|
+
if (!this.hasPendingMessages()) {
|
|
1100
911
|
this.updateDocumentDirtyState(false);
|
|
1101
912
|
}
|
|
1102
|
-
|
|
913
|
+
const type = message.type;
|
|
914
|
+
switch (type) {
|
|
1103
915
|
case ContainerMessageType.Attach:
|
|
1104
916
|
this.dataStores.processAttachMessage(message, local);
|
|
1105
917
|
break;
|
|
@@ -1110,12 +922,18 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1110
922
|
this.dataStores.processFluidDataStoreOp(message, local, localOpMetadata);
|
|
1111
923
|
break;
|
|
1112
924
|
case ContainerMessageType.BlobAttach:
|
|
1113
|
-
|
|
1114
|
-
|
|
925
|
+
this.blobManager.processBlobAttachOp(message, local);
|
|
926
|
+
break;
|
|
927
|
+
case ContainerMessageType.ChunkedOp:
|
|
928
|
+
case ContainerMessageType.Rejoin:
|
|
1115
929
|
break;
|
|
1116
930
|
default:
|
|
931
|
+
assert(!runtimeMessage, 0x3ce /* 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);
|
|
1117
936
|
}
|
|
1118
|
-
this.emit("op", message);
|
|
1119
937
|
this.scheduleManager.afterOpProcessing(undefined, message);
|
|
1120
938
|
if (local) {
|
|
1121
939
|
// If we have processed a local op, this means that the container is
|
|
@@ -1181,6 +999,10 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1181
999
|
this.dataStores.processSignal(envelope.address, transformed, local);
|
|
1182
1000
|
}
|
|
1183
1001
|
async getRootDataStore(id, wait = true) {
|
|
1002
|
+
return this.getRootDataStoreChannel(id, wait);
|
|
1003
|
+
}
|
|
1004
|
+
async getRootDataStoreChannel(id, wait = true) {
|
|
1005
|
+
await this.dataStores.waitIfPendingAlias(id);
|
|
1184
1006
|
const internalId = this.internalId(id);
|
|
1185
1007
|
const context = await this.dataStores.getDataStore(internalId, wait);
|
|
1186
1008
|
assert(await context.isRoot(), 0x12b /* "did not get root data store" */);
|
|
@@ -1205,25 +1027,57 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1205
1027
|
}
|
|
1206
1028
|
flush() {
|
|
1207
1029
|
assert(this._orderSequentiallyCalls === 0, 0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
|
|
1208
|
-
|
|
1209
|
-
|
|
1030
|
+
this.flushBatch(this.pendingAttachBatch.popBatch());
|
|
1031
|
+
this.flushBatch(this.pendingBatch.popBatch());
|
|
1032
|
+
assert(this.emptyBatch, 0x3cf /* 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!");
|
|
1210
1048
|
}
|
|
1211
|
-
|
|
1212
|
-
// Note that this should happen before the `this.needsFlush` check below because in the scenario where we are
|
|
1213
|
-
// not connected, `this.needsFlush` will be false but the PendingStateManager might have pending messages and
|
|
1214
|
-
// hence needs to track this.
|
|
1215
|
-
this.pendingStateManager.onFlush();
|
|
1216
|
-
// If flush has already been called then exit early
|
|
1217
|
-
if (!this.needsFlush) {
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
this.needsFlush = false;
|
|
1049
|
+
let clientSequenceNumber = -1;
|
|
1221
1050
|
// Did we disconnect in the middle of turn-based batch?
|
|
1222
1051
|
// If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
|
|
1223
|
-
if (
|
|
1224
|
-
|
|
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, 0x3d0 /* 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++;
|
|
1225
1079
|
}
|
|
1226
|
-
|
|
1080
|
+
this.pendingStateManager.onFlush();
|
|
1227
1081
|
}
|
|
1228
1082
|
orderSequentially(callback) {
|
|
1229
1083
|
// If flush mode is already TurnBased we are either
|
|
@@ -1248,7 +1102,10 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1248
1102
|
trackOrderSequentiallyCalls(callback) {
|
|
1249
1103
|
let checkpoint;
|
|
1250
1104
|
if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
|
|
1251
|
-
|
|
1105
|
+
// Note: we are not touching this.pendingAttachBatch here, for two reasons:
|
|
1106
|
+
// 1. It would not help, as we flush attach ops as they become available.
|
|
1107
|
+
// 2. There is no way to undo process of data store creation.
|
|
1108
|
+
checkpoint = this.pendingBatch.checkpoint();
|
|
1252
1109
|
}
|
|
1253
1110
|
try {
|
|
1254
1111
|
this._orderSequentiallyCalls++;
|
|
@@ -1257,7 +1114,16 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1257
1114
|
catch (error) {
|
|
1258
1115
|
if (checkpoint) {
|
|
1259
1116
|
// This will throw and close the container if rollback fails
|
|
1260
|
-
|
|
1117
|
+
try {
|
|
1118
|
+
checkpoint.rollback((message) => this.rollback(message.deserializedContent.type, message.deserializedContent.contents, message.localOpMetadata));
|
|
1119
|
+
}
|
|
1120
|
+
catch (err) {
|
|
1121
|
+
const error2 = wrapError(err, (message) => {
|
|
1122
|
+
return DataProcessingError.create(`RollbackError: ${message}`, "checkpointRollback", undefined);
|
|
1123
|
+
});
|
|
1124
|
+
this.closeFn(error2);
|
|
1125
|
+
throw error2;
|
|
1126
|
+
}
|
|
1261
1127
|
}
|
|
1262
1128
|
else {
|
|
1263
1129
|
// pre-0.58 error message: orderSequentiallyCallbackException
|
|
@@ -1271,67 +1137,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1271
1137
|
}
|
|
1272
1138
|
async createDataStore(pkg) {
|
|
1273
1139
|
const internalId = uuid();
|
|
1274
|
-
return channelToDataStore(await this._createDataStore(pkg,
|
|
1275
|
-
}
|
|
1276
|
-
/**
|
|
1277
|
-
* Creates a root datastore directly with a user generated id and attaches it to storage.
|
|
1278
|
-
* It is vulnerable to name collisions and should not be used.
|
|
1279
|
-
*
|
|
1280
|
-
* This method will be removed. See #6465.
|
|
1281
|
-
*/
|
|
1282
|
-
async createRootDataStoreLegacy(pkg, rootDataStoreId) {
|
|
1283
|
-
const fluidDataStore = await this._createDataStore(pkg, true /* isRoot */, rootDataStoreId);
|
|
1284
|
-
// back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel. For
|
|
1285
|
-
// older versions, we still have to call bindToContext.
|
|
1286
|
-
if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
|
|
1287
|
-
fluidDataStore.makeVisibleAndAttachGraph();
|
|
1288
|
-
}
|
|
1289
|
-
else {
|
|
1290
|
-
fluidDataStore.bindToContext();
|
|
1291
|
-
}
|
|
1292
|
-
return fluidDataStore;
|
|
1293
|
-
}
|
|
1294
|
-
/**
|
|
1295
|
-
* @deprecated - will be removed in an upcoming release. See #9660.
|
|
1296
|
-
*/
|
|
1297
|
-
async createRootDataStore(pkg, rootDataStoreId) {
|
|
1298
|
-
if (rootDataStoreId.includes("/")) {
|
|
1299
|
-
throw new UsageError(`Id cannot contain slashes: '${rootDataStoreId}'`);
|
|
1300
|
-
}
|
|
1301
|
-
return this._aliasingEnabled === true ?
|
|
1302
|
-
this.createAndAliasDataStore(pkg, rootDataStoreId) :
|
|
1303
|
-
this.createRootDataStoreLegacy(pkg, rootDataStoreId);
|
|
1304
|
-
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Creates a data store then attempts to alias it.
|
|
1307
|
-
* If aliasing fails, it will raise an exception.
|
|
1308
|
-
*
|
|
1309
|
-
* This method will be removed. See #6465.
|
|
1310
|
-
*
|
|
1311
|
-
* @param pkg - Package name of the data store
|
|
1312
|
-
* @param alias - Alias to be assigned to the data store
|
|
1313
|
-
* @param props - Properties for the data store
|
|
1314
|
-
* @returns - An aliased data store which can can be found / loaded by alias.
|
|
1315
|
-
*/
|
|
1316
|
-
async createAndAliasDataStore(pkg, alias, props) {
|
|
1317
|
-
const internalId = uuid();
|
|
1318
|
-
const dataStore = await this._createDataStore(pkg, false /* isRoot */, internalId, props);
|
|
1319
|
-
const aliasedDataStore = channelToDataStore(dataStore, internalId, this, this.dataStores, this.mc.logger);
|
|
1320
|
-
const result = await aliasedDataStore.trySetAlias(alias);
|
|
1321
|
-
if (result !== "Success") {
|
|
1322
|
-
throw new GenericError("dataStoreAliasFailure", undefined /* error */, {
|
|
1323
|
-
alias: {
|
|
1324
|
-
value: alias,
|
|
1325
|
-
tag: TelemetryDataTag.UserData,
|
|
1326
|
-
},
|
|
1327
|
-
internalId: {
|
|
1328
|
-
value: internalId,
|
|
1329
|
-
tag: TelemetryDataTag.PackageData,
|
|
1330
|
-
},
|
|
1331
|
-
aliasResult: result,
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
return aliasedDataStore;
|
|
1140
|
+
return channelToDataStore(await this._createDataStore(pkg, internalId), internalId, this, this.dataStores, this.mc.logger);
|
|
1335
1141
|
}
|
|
1336
1142
|
createDetachedRootDataStore(pkg, rootDataStoreId) {
|
|
1337
1143
|
if (rootDataStoreId.includes("/")) {
|
|
@@ -1342,38 +1148,13 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1342
1148
|
createDetachedDataStore(pkg) {
|
|
1343
1149
|
return this.dataStores.createDetachedDataStoreCore(pkg, false);
|
|
1344
1150
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
* It is vulnerable to name collisions if both aforementioned conditions are true, and should not be used.
|
|
1348
|
-
*
|
|
1349
|
-
* This method will be removed. See #6465.
|
|
1350
|
-
*/
|
|
1351
|
-
async _createDataStoreWithPropsLegacy(pkg, props, id = uuid(), isRoot = false) {
|
|
1352
|
-
const fluidDataStore = await this.dataStores._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props).realize();
|
|
1353
|
-
if (isRoot) {
|
|
1354
|
-
// back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel.
|
|
1355
|
-
// For older versions, we still have to call bindToContext.
|
|
1356
|
-
if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
|
|
1357
|
-
fluidDataStore.makeVisibleAndAttachGraph();
|
|
1358
|
-
}
|
|
1359
|
-
else {
|
|
1360
|
-
fluidDataStore.bindToContext();
|
|
1361
|
-
}
|
|
1362
|
-
this.logger.sendTelemetryEvent({
|
|
1363
|
-
eventName: "Root datastore with props",
|
|
1364
|
-
hasProps: props !== undefined,
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1151
|
+
async _createDataStoreWithProps(pkg, props, id = uuid()) {
|
|
1152
|
+
const fluidDataStore = await this.dataStores._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, props).realize();
|
|
1367
1153
|
return channelToDataStore(fluidDataStore, id, this, this.dataStores, this.mc.logger);
|
|
1368
1154
|
}
|
|
1369
|
-
async
|
|
1370
|
-
return this._aliasingEnabled === true && isRoot ?
|
|
1371
|
-
this.createAndAliasDataStore(pkg, id, props) :
|
|
1372
|
-
this._createDataStoreWithPropsLegacy(pkg, props, id, isRoot);
|
|
1373
|
-
}
|
|
1374
|
-
async _createDataStore(pkg, isRoot, id = uuid(), props) {
|
|
1155
|
+
async _createDataStore(pkg, id = uuid(), props) {
|
|
1375
1156
|
return this.dataStores
|
|
1376
|
-
._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id,
|
|
1157
|
+
._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, props)
|
|
1377
1158
|
.realize();
|
|
1378
1159
|
}
|
|
1379
1160
|
canSendOps() {
|
|
@@ -1447,7 +1228,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1447
1228
|
assert(this.attachState === AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
|
|
1448
1229
|
this.emit("attached");
|
|
1449
1230
|
}
|
|
1450
|
-
if (attachState === AttachState.Attached && !this.
|
|
1231
|
+
if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
|
|
1451
1232
|
this.updateDocumentDirtyState(false);
|
|
1452
1233
|
}
|
|
1453
1234
|
this.dataStores.setAttachState(attachState);
|
|
@@ -1465,10 +1246,8 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1465
1246
|
this.blobManager.setRedirectTable(blobRedirectTable);
|
|
1466
1247
|
}
|
|
1467
1248
|
const summarizeResult = this.dataStores.createSummary(telemetryContext);
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
wrapSummaryInChannelsTree(summarizeResult);
|
|
1471
|
-
}
|
|
1249
|
+
// Wrap data store summaries in .channels subtree.
|
|
1250
|
+
wrapSummaryInChannelsTree(summarizeResult);
|
|
1472
1251
|
this.addContainerStateToSummary(summarizeResult, true /* fullTree */, false /* trackState */, telemetryContext);
|
|
1473
1252
|
return summarizeResult.summary;
|
|
1474
1253
|
}
|
|
@@ -1483,12 +1262,9 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1483
1262
|
}
|
|
1484
1263
|
async summarizeInternal(fullTree, trackState, telemetryContext) {
|
|
1485
1264
|
const summarizeResult = await this.dataStores.summarize(fullTree, trackState, telemetryContext);
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
wrapSummaryInChannelsTree(summarizeResult);
|
|
1490
|
-
pathPartsForChildren = [channelsTreeName];
|
|
1491
|
-
}
|
|
1265
|
+
// Wrap data store summaries in .channels subtree.
|
|
1266
|
+
wrapSummaryInChannelsTree(summarizeResult);
|
|
1267
|
+
const pathPartsForChildren = [channelsTreeName];
|
|
1492
1268
|
this.addContainerStateToSummary(summarizeResult, fullTree, trackState, telemetryContext);
|
|
1493
1269
|
return Object.assign(Object.assign({}, summarizeResult), { id: "", pathPartsForChildren });
|
|
1494
1270
|
}
|
|
@@ -1616,7 +1392,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1616
1392
|
}
|
|
1617
1393
|
/**
|
|
1618
1394
|
* Runs garbage collection and updates the reference / used state of the nodes in the container.
|
|
1619
|
-
* @returns the statistics of the garbage collection run.
|
|
1395
|
+
* @returns the statistics of the garbage collection run; undefined if GC did not run.
|
|
1620
1396
|
*/
|
|
1621
1397
|
async collectGarbage(options) {
|
|
1622
1398
|
return this.garbageCollector.collectGarbage(options);
|
|
@@ -1639,7 +1415,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1639
1415
|
* @param options - options controlling how the summary is generated or submitted
|
|
1640
1416
|
*/
|
|
1641
1417
|
async submitSummary(options) {
|
|
1642
|
-
var _a, _b
|
|
1418
|
+
var _a, _b;
|
|
1643
1419
|
const { fullTree, refreshLatestAck, summaryLogger } = options;
|
|
1644
1420
|
// The summary number for this summary. This will be updated during the summary process, so get it now and
|
|
1645
1421
|
// use it for all events logged during this summary.
|
|
@@ -1647,6 +1423,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1647
1423
|
const summaryNumberLogger = ChildLogger.create(summaryLogger, undefined, {
|
|
1648
1424
|
all: { summaryNumber },
|
|
1649
1425
|
});
|
|
1426
|
+
assert(this.emptyBatch, 0x3d1 /* Can't trigger summary in the middle of a batch */);
|
|
1650
1427
|
let latestSnapshotVersionId;
|
|
1651
1428
|
if (refreshLatestAck) {
|
|
1652
1429
|
const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(ChildLogger.create(summaryNumberLogger, undefined, { all: { safeSummary: true } }));
|
|
@@ -1667,17 +1444,11 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1667
1444
|
const summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
|
|
1668
1445
|
const minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
|
|
1669
1446
|
const message = `Summary @${summaryRefSeqNum}:${this.deltaManager.minimumSequenceNumber}`;
|
|
1670
|
-
|
|
1671
|
-
// doesn't match the last processed sequence number, log an error.
|
|
1672
|
-
if (summaryRefSeqNum !== ((_a = this.deltaManager.lastMessage) === null || _a === void 0 ? void 0 : _a.sequenceNumber)) {
|
|
1673
|
-
summaryNumberLogger.sendErrorEvent({
|
|
1674
|
-
eventName: "LastSequenceMismatch",
|
|
1675
|
-
error: message,
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1447
|
+
const lastAck = this.summaryCollection.latestAck;
|
|
1678
1448
|
this.summarizerNode.startSummary(summaryRefSeqNum, summaryNumberLogger);
|
|
1679
1449
|
// Helper function to check whether we should still continue between each async step.
|
|
1680
1450
|
const checkContinue = () => {
|
|
1451
|
+
var _a;
|
|
1681
1452
|
// Do not check for loss of connectivity directly! Instead leave it up to
|
|
1682
1453
|
// RunWhileConnectedCoordinator to control policy in a single place.
|
|
1683
1454
|
// This will allow easier change of design if we chose to. For example, we may chose to allow
|
|
@@ -1701,6 +1472,14 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1701
1472
|
error: `lastSequenceNumber changed before uploading to storage. ${this.deltaManager.lastSequenceNumber} !== ${summaryRefSeqNum}`,
|
|
1702
1473
|
};
|
|
1703
1474
|
}
|
|
1475
|
+
assert(summaryRefSeqNum === ((_a = this.deltaManager.lastMessage) === null || _a === void 0 ? void 0 : _a.sequenceNumber), 0x395 /* it's one and the same thing */);
|
|
1476
|
+
if (lastAck !== this.summaryCollection.latestAck) {
|
|
1477
|
+
return {
|
|
1478
|
+
continue: false,
|
|
1479
|
+
// eslint-disable-next-line max-len
|
|
1480
|
+
error: `Last summary changed while summarizing. ${this.summaryCollection.latestAck} !== ${lastAck}`,
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1704
1483
|
return { continue: true };
|
|
1705
1484
|
};
|
|
1706
1485
|
let continueResult = checkContinue();
|
|
@@ -1719,7 +1498,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1719
1498
|
const forcedFullTree = this.garbageCollector.summaryStateNeedsReset;
|
|
1720
1499
|
try {
|
|
1721
1500
|
summarizeResult = await this.summarize({
|
|
1722
|
-
fullTree: fullTree
|
|
1501
|
+
fullTree: fullTree !== null && fullTree !== void 0 ? fullTree : forcedFullTree,
|
|
1723
1502
|
trackState: true,
|
|
1724
1503
|
summaryLogger: summaryNumberLogger,
|
|
1725
1504
|
runGC: this.garbageCollector.shouldRunGC,
|
|
@@ -1739,13 +1518,13 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1739
1518
|
// Counting dataStores and handles
|
|
1740
1519
|
// Because handles are unchanged dataStores in the current logic,
|
|
1741
1520
|
// summarized dataStore count is total dataStore count minus handle count
|
|
1742
|
-
const dataStoreTree =
|
|
1521
|
+
const dataStoreTree = summaryTree.tree[channelsTreeName];
|
|
1743
1522
|
assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
|
|
1744
1523
|
const handleCount = Object.values(dataStoreTree.tree).filter((value) => value.type === SummaryType.Handle).length;
|
|
1745
1524
|
const gcSummaryTreeStats = summaryTree.tree[gcTreeKey]
|
|
1746
1525
|
? calculateStats(summaryTree.tree[gcTreeKey])
|
|
1747
1526
|
: undefined;
|
|
1748
|
-
const summaryStats = Object.assign({ dataStoreCount: this.dataStores.size, summarizedDataStoreCount: this.dataStores.size - handleCount, gcStateUpdatedDataStoreCount: (
|
|
1527
|
+
const summaryStats = Object.assign({ dataStoreCount: this.dataStores.size, summarizedDataStoreCount: this.dataStores.size - handleCount, gcStateUpdatedDataStoreCount: (_a = summarizeResult.gcStats) === null || _a === void 0 ? void 0 : _a.updatedDataStoreCount, gcBlobNodeCount: gcSummaryTreeStats === null || gcSummaryTreeStats === void 0 ? void 0 : gcSummaryTreeStats.blobNodeCount, gcTotalBlobsSize: gcSummaryTreeStats === null || gcSummaryTreeStats === void 0 ? void 0 : gcSummaryTreeStats.totalBlobSize, summaryNumber }, partialStats);
|
|
1749
1528
|
const generateSummaryData = {
|
|
1750
1529
|
referenceSequenceNumber: summaryRefSeqNum,
|
|
1751
1530
|
minimumSequenceNumber,
|
|
@@ -1763,7 +1542,6 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1763
1542
|
// submitting the summaryOp then we can't rely on summaryAck. So in case we have
|
|
1764
1543
|
// latestSnapshotVersionId from storage and it does not match with the lastAck ackHandle, then use
|
|
1765
1544
|
// the one fetched from storage as parent as that is the latest.
|
|
1766
|
-
const lastAck = this.summaryCollection.latestAck;
|
|
1767
1545
|
let summaryContext;
|
|
1768
1546
|
if ((lastAck === null || lastAck === void 0 ? void 0 : lastAck.summaryAck.contents.handle) !== latestSnapshotVersionId
|
|
1769
1547
|
&& latestSnapshotVersionId !== undefined) {
|
|
@@ -1776,7 +1554,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1776
1554
|
else if (lastAck === undefined) {
|
|
1777
1555
|
summaryContext = {
|
|
1778
1556
|
proposalHandle: undefined,
|
|
1779
|
-
ackHandle: (
|
|
1557
|
+
ackHandle: (_b = this.context.getLoadedFromVersion()) === null || _b === void 0 ? void 0 : _b.id,
|
|
1780
1558
|
referenceSequenceNumber: summaryRefSeqNum,
|
|
1781
1559
|
};
|
|
1782
1560
|
}
|
|
@@ -1809,14 +1587,13 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1809
1587
|
}
|
|
1810
1588
|
let clientSequenceNumber;
|
|
1811
1589
|
try {
|
|
1812
|
-
clientSequenceNumber = this.
|
|
1590
|
+
clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
|
|
1813
1591
|
}
|
|
1814
1592
|
catch (error) {
|
|
1815
1593
|
return Object.assign(Object.assign({ stage: "upload" }, uploadData), { error });
|
|
1816
1594
|
}
|
|
1817
1595
|
const submitData = Object.assign(Object.assign({ stage: "submit" }, uploadData), { clientSequenceNumber, submitOpDuration: trace.trace().duration });
|
|
1818
1596
|
this.summarizerNode.completeSummary(handle);
|
|
1819
|
-
this.opTracker.reset();
|
|
1820
1597
|
return submitData;
|
|
1821
1598
|
}
|
|
1822
1599
|
finally {
|
|
@@ -1858,7 +1635,17 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1858
1635
|
this.chunkMap.delete(clientId);
|
|
1859
1636
|
}
|
|
1860
1637
|
}
|
|
1638
|
+
hasPendingMessages() {
|
|
1639
|
+
return this.pendingStateManager.hasPendingMessages() || !this.emptyBatch;
|
|
1640
|
+
}
|
|
1861
1641
|
updateDocumentDirtyState(dirty) {
|
|
1642
|
+
if (this.attachState !== AttachState.Attached) {
|
|
1643
|
+
assert(dirty, 0x3d2 /* Non-attached container is dirty */);
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
// Other way is not true = see this.isContainerMessageDirtyable()
|
|
1647
|
+
assert(!dirty || this.hasPendingMessages(), 0x3d3 /* if doc is dirty, there has to be pending ops */);
|
|
1648
|
+
}
|
|
1862
1649
|
if (this.dirtyContainer === dirty) {
|
|
1863
1650
|
return;
|
|
1864
1651
|
}
|
|
@@ -1886,99 +1673,100 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
1886
1673
|
this.verifyNotClosed();
|
|
1887
1674
|
return this.blobManager.createBlob(blob);
|
|
1888
1675
|
}
|
|
1889
|
-
submit(type,
|
|
1676
|
+
submit(type, contents, localOpMetadata = undefined, metadata = undefined) {
|
|
1890
1677
|
this.verifyNotClosed();
|
|
1891
1678
|
// There should be no ops in detached container state!
|
|
1892
1679
|
assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
if (this.
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1680
|
+
const deserializedContent = { type, contents };
|
|
1681
|
+
const serializedContent = JSON.stringify(deserializedContent);
|
|
1682
|
+
if (this.deltaManager.readOnlyInfo.readonly) {
|
|
1683
|
+
this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
|
|
1684
|
+
}
|
|
1685
|
+
const message = {
|
|
1686
|
+
contents: serializedContent,
|
|
1687
|
+
deserializedContent,
|
|
1688
|
+
metadata,
|
|
1689
|
+
localOpMetadata,
|
|
1690
|
+
referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
|
|
1691
|
+
};
|
|
1692
|
+
try {
|
|
1693
|
+
// If this is attach message for new data store, and we are in a batch, send this op out of order
|
|
1694
|
+
// Is it safe:
|
|
1695
|
+
// Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
|
|
1696
|
+
// They become visible only when aliased, or handle to some sub-element of newly created datastore
|
|
1697
|
+
// is stored in some DDS, i.e. only after some other op.
|
|
1698
|
+
// Why:
|
|
1699
|
+
// Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
|
|
1700
|
+
// stores are created, causing issues like relay service throttling (too many ops) and catastrophic
|
|
1701
|
+
// failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
|
|
1702
|
+
// these issues.
|
|
1703
|
+
// Cons:
|
|
1704
|
+
// 1. With large batches, relay service may throttle clients. Clients may disconnect while throttled.
|
|
1705
|
+
// This change creates new possibility of a lot of newly created data stores never being referenced
|
|
1706
|
+
// because client died before it had a change to submit the rest of the ops. This will create more
|
|
1707
|
+
// garbage that needs to be collected leveraging GC (Garbage Collection) feature.
|
|
1708
|
+
// 2. Sending ops out of order means they are excluded from rollback functionality. This is not an issue
|
|
1709
|
+
// today as rollback can't undo creation of data store. To some extent not sending them is a bigger
|
|
1710
|
+
// issue than sending.
|
|
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 (type === ContainerMessageType.Attach &&
|
|
1714
|
+
this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
|
|
1715
|
+
if (!this.pendingAttachBatch.push(message)) {
|
|
1716
|
+
// BatchManager has two limits - soft limit & hard limit. Soft limit is only engaged
|
|
1717
|
+
// when queue is not empty.
|
|
1718
|
+
// Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
|
|
1719
|
+
this.flushBatch(this.pendingAttachBatch.popBatch());
|
|
1720
|
+
if (!this.pendingAttachBatch.push(message)) {
|
|
1721
|
+
throw new GenericError("BatchTooLarge",
|
|
1722
|
+
/* error */ undefined, {
|
|
1723
|
+
opSize: message.contents.length,
|
|
1724
|
+
count: this.pendingAttachBatch.length,
|
|
1725
|
+
limit: this.pendingAttachBatch.limit,
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
else {
|
|
1731
|
+
if (!this.pendingBatch.push(message)) {
|
|
1732
|
+
throw new GenericError("BatchTooLarge",
|
|
1733
|
+
/* error */ undefined, {
|
|
1734
|
+
opSize: message.contents.length,
|
|
1735
|
+
count: this.pendingBatch.length,
|
|
1736
|
+
limit: this.pendingBatch.limit,
|
|
1908
1737
|
});
|
|
1909
1738
|
}
|
|
1910
1739
|
}
|
|
1911
|
-
|
|
1740
|
+
if (this._flushMode !== FlushMode.TurnBased) {
|
|
1741
|
+
this.flush();
|
|
1742
|
+
}
|
|
1743
|
+
else if (!this.flushTrigger) {
|
|
1744
|
+
this.flushTrigger = true;
|
|
1745
|
+
// Queue a microtask to detect the end of the turn and force a flush.
|
|
1746
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1747
|
+
Promise.resolve().then(() => {
|
|
1748
|
+
this.flushTrigger = false;
|
|
1749
|
+
this.flush();
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1912
1752
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
this.updateDocumentDirtyState(true);
|
|
1753
|
+
catch (error) {
|
|
1754
|
+
this.closeFn(error);
|
|
1755
|
+
throw error;
|
|
1917
1756
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
if (this._maxOpSizeInBytes >= 0) {
|
|
1921
|
-
// Chunking disabled
|
|
1922
|
-
if (!serializedContent || serializedContent.length <= this._maxOpSizeInBytes) {
|
|
1923
|
-
return this.submitRuntimeMessage(type, content, batch, opMetadataInternal);
|
|
1924
|
-
}
|
|
1925
|
-
// When chunking is disabled, we ignore the server max message size
|
|
1926
|
-
// and if the content length is larger than the client configured message size
|
|
1927
|
-
// instead of splitting the content, we will fail by explicitly close the container
|
|
1928
|
-
this.closeFn(new GenericError("OpTooLarge",
|
|
1929
|
-
/* error */ undefined, {
|
|
1930
|
-
length: {
|
|
1931
|
-
value: serializedContent.length,
|
|
1932
|
-
tag: TelemetryDataTag.PackageData,
|
|
1933
|
-
},
|
|
1934
|
-
limit: {
|
|
1935
|
-
value: this._maxOpSizeInBytes,
|
|
1936
|
-
tag: TelemetryDataTag.PackageData,
|
|
1937
|
-
},
|
|
1938
|
-
}));
|
|
1939
|
-
return -1;
|
|
1940
|
-
}
|
|
1941
|
-
// Chunking enabled, fallback on the server's max message size
|
|
1942
|
-
// and split the content accordingly
|
|
1943
|
-
if (!serializedContent || serializedContent.length <= serverMaxOpSize) {
|
|
1944
|
-
return this.submitRuntimeMessage(type, content, batch, opMetadataInternal);
|
|
1945
|
-
}
|
|
1946
|
-
return this.submitChunkedMessage(type, serializedContent, serverMaxOpSize);
|
|
1947
|
-
}
|
|
1948
|
-
submitChunkedMessage(type, content, maxOpSize) {
|
|
1949
|
-
const contentLength = content.length;
|
|
1950
|
-
const chunkN = Math.floor((contentLength - 1) / maxOpSize) + 1;
|
|
1951
|
-
let offset = 0;
|
|
1952
|
-
let clientSequenceNumber = 0;
|
|
1953
|
-
for (let i = 1; i <= chunkN; i = i + 1) {
|
|
1954
|
-
const chunkedOp = {
|
|
1955
|
-
chunkId: i,
|
|
1956
|
-
contents: content.substr(offset, maxOpSize),
|
|
1957
|
-
originalType: type,
|
|
1958
|
-
totalChunks: chunkN,
|
|
1959
|
-
};
|
|
1960
|
-
offset += maxOpSize;
|
|
1961
|
-
clientSequenceNumber = this.submitRuntimeMessage(ContainerMessageType.ChunkedOp, chunkedOp, false);
|
|
1757
|
+
if (this.isContainerMessageDirtyable(type, contents)) {
|
|
1758
|
+
this.updateDocumentDirtyState(true);
|
|
1962
1759
|
}
|
|
1963
|
-
return clientSequenceNumber;
|
|
1964
1760
|
}
|
|
1965
|
-
|
|
1761
|
+
submitSummaryMessage(contents) {
|
|
1966
1762
|
this.verifyNotClosed();
|
|
1967
1763
|
assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
|
|
1968
1764
|
// System message should not be sent in the middle of the batch.
|
|
1969
|
-
|
|
1970
|
-
//
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
this.
|
|
1974
|
-
}
|
|
1975
|
-
return this.context.submitFn(type, contents, middleOfBatch);
|
|
1976
|
-
}
|
|
1977
|
-
submitRuntimeMessage(type, contents, batch, appData) {
|
|
1978
|
-
this.verifyNotClosed();
|
|
1979
|
-
assert(this.connected, 0x259 /* "Container disconnected when trying to submit system message" */);
|
|
1980
|
-
const payload = { type, contents };
|
|
1981
|
-
return this.context.submitFn(MessageType.Operation, payload, batch, appData);
|
|
1765
|
+
assert(this.emptyBatch, 0x3d4 /* System op in the middle of a batch */);
|
|
1766
|
+
// back-compat: ADO #1385: Make this call unconditional in the future
|
|
1767
|
+
return this.context.submitSummaryFn !== undefined
|
|
1768
|
+
? this.context.submitSummaryFn(contents)
|
|
1769
|
+
: this.context.submitFn(MessageType.Summarize, contents, false);
|
|
1982
1770
|
}
|
|
1983
1771
|
/**
|
|
1984
1772
|
* Throw an error if the runtime is closed. Methods that are expected to potentially
|
|
@@ -2009,7 +1797,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2009
1797
|
case ContainerMessageType.ChunkedOp:
|
|
2010
1798
|
throw new Error(`chunkedOp not expected here`);
|
|
2011
1799
|
case ContainerMessageType.BlobAttach:
|
|
2012
|
-
this.
|
|
1800
|
+
this.blobManager.reSubmit(opMetadata);
|
|
2013
1801
|
break;
|
|
2014
1802
|
case ContainerMessageType.Rejoin:
|
|
2015
1803
|
this.submit(type, content);
|
|
@@ -2032,13 +1820,18 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2032
1820
|
/** Implementation of ISummarizerInternalsProvider.refreshLatestSummaryAck */
|
|
2033
1821
|
async refreshLatestSummaryAck(proposalHandle, ackHandle, summaryRefSeq, summaryLogger) {
|
|
2034
1822
|
const readAndParseBlob = async (id) => readAndParse(this.storage, id);
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
1823
|
+
// The call to fetch the snapshot is very expensive and not always needed.
|
|
1824
|
+
// It should only be done by the summarizerNode, if required.
|
|
1825
|
+
const snapshotTreeFetcher = async () => {
|
|
1826
|
+
const fetchResult = await this.fetchSnapshotFromStorage(ackHandle, summaryLogger, {
|
|
1827
|
+
eventName: "RefreshLatestSummaryGetSnapshot",
|
|
1828
|
+
ackHandle,
|
|
1829
|
+
summaryRefSeq,
|
|
1830
|
+
fetchLatest: false,
|
|
1831
|
+
});
|
|
1832
|
+
return fetchResult.snapshotTree;
|
|
1833
|
+
};
|
|
1834
|
+
const result = await this.summarizerNode.refreshLatestSummary(proposalHandle, summaryRefSeq, snapshotTreeFetcher, readAndParseBlob, summaryLogger);
|
|
2042
1835
|
// Notify the garbage collector so it can update its latest summary state.
|
|
2043
1836
|
await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
|
|
2044
1837
|
}
|
|
@@ -2052,7 +1845,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2052
1845
|
const { snapshotTree, versionId } = await this.fetchSnapshotFromStorage(null, summaryLogger, {
|
|
2053
1846
|
eventName: "RefreshLatestSummaryGetSnapshot",
|
|
2054
1847
|
fetchLatest: true,
|
|
2055
|
-
});
|
|
1848
|
+
}, FetchSource.noCache);
|
|
2056
1849
|
const readAndParseBlob = async (id) => readAndParse(this.storage, id);
|
|
2057
1850
|
const latestSnapshotRefSeq = await seqFromTree(snapshotTree, readAndParseBlob);
|
|
2058
1851
|
const result = await this.summarizerNode.refreshLatestSummary(undefined, latestSnapshotRefSeq, async () => snapshotTree, readAndParseBlob, summaryLogger);
|
|
@@ -2060,11 +1853,11 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2060
1853
|
await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
|
|
2061
1854
|
return { latestSnapshotRefSeq, latestSnapshotVersionId: versionId };
|
|
2062
1855
|
}
|
|
2063
|
-
async fetchSnapshotFromStorage(versionId, logger, event) {
|
|
1856
|
+
async fetchSnapshotFromStorage(versionId, logger, event, fetchSource) {
|
|
2064
1857
|
return PerformanceEvent.timedExecAsync(logger, event, async (perfEvent) => {
|
|
2065
1858
|
const stats = {};
|
|
2066
1859
|
const trace = Trace.start();
|
|
2067
|
-
const versions = await this.storage.getVersions(versionId, 1);
|
|
1860
|
+
const versions = await this.storage.getVersions(versionId, 1, "refreshLatestSummaryAckFromServer", fetchSource);
|
|
2068
1861
|
assert(!!versions && !!versions[0], 0x137 /* "Failed to get version from storage" */);
|
|
2069
1862
|
stats.getVersionDuration = trace.trace().duration;
|
|
2070
1863
|
const maybeSnapshot = await this.storage.getSnapshotTree(versions[0]);
|
|
@@ -2094,10 +1887,15 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2094
1887
|
if (!((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad)) {
|
|
2095
1888
|
throw new UsageError("can't get state when offline load disabled");
|
|
2096
1889
|
}
|
|
1890
|
+
// Flush pending batch.
|
|
1891
|
+
// getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
|
|
1892
|
+
// to close current batch.
|
|
1893
|
+
this.flush();
|
|
2097
1894
|
const previousPendingState = this.context.pendingLocalState;
|
|
2098
1895
|
if (previousPendingState) {
|
|
2099
1896
|
return {
|
|
2100
1897
|
pending: this.pendingStateManager.getLocalState(),
|
|
1898
|
+
pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
|
|
2101
1899
|
snapshotBlobs: previousPendingState.snapshotBlobs,
|
|
2102
1900
|
baseSnapshot: previousPendingState.baseSnapshot,
|
|
2103
1901
|
savedOps: this.savedOps,
|
|
@@ -2107,6 +1905,7 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2107
1905
|
assert(!!this.baseSnapshotBlobs, 0x2e7 /* "Must serialize base snapshot blobs before getting runtime state" */);
|
|
2108
1906
|
return {
|
|
2109
1907
|
pending: this.pendingStateManager.getLocalState(),
|
|
1908
|
+
pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
|
|
2110
1909
|
snapshotBlobs: this.baseSnapshotBlobs,
|
|
2111
1910
|
baseSnapshot: this.context.baseSnapshot,
|
|
2112
1911
|
savedOps: this.savedOps,
|
|
@@ -2146,6 +1945,19 @@ export class ContainerRuntime extends TypedEventEmitter {
|
|
|
2146
1945
|
// we may not have seen every sequence number (because of system ops) so apply everything once we
|
|
2147
1946
|
// don't have any more saved ops
|
|
2148
1947
|
await this.pendingStateManager.applyStashedOpsAt();
|
|
1948
|
+
// If it's not the case, we should take it into account when calculating dirty state.
|
|
1949
|
+
assert(this.context.attachState === AttachState.Attached, 0x3d5 /* this function is called for attached containers only */);
|
|
1950
|
+
if (!this.hasPendingMessages()) {
|
|
1951
|
+
this.updateDocumentDirtyState(false);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
validateSummaryHeuristicConfiguration(configuration) {
|
|
1955
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1956
|
+
for (const prop in configuration) {
|
|
1957
|
+
if (typeof configuration[prop] === "number" && configuration[prop] < 0) {
|
|
1958
|
+
throw new UsageError(`Summary heuristic configuration property "${prop}" cannot be less than 0`);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
2149
1961
|
}
|
|
2150
1962
|
}
|
|
2151
1963
|
/**
|