@fluidframework/container-runtime 1.2.7 → 2.0.0-dev.1.3.0.96595
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/.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 +109 -124
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +349 -542
- 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 +249 -170
- 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 +126 -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 +68 -26
- 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 +95 -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 +109 -124
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +355 -547
- 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 +238 -160
- 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 +121 -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 +68 -26
- 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 +95 -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 +65 -24
- package/src/batchManager.ts +91 -0
- package/src/batchTracker.ts +2 -3
- package/src/blobManager.ts +385 -118
- package/src/containerRuntime.ts +529 -740
- 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 +297 -206
- package/src/gcSweepReadyUsageDetection.ts +139 -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 +75 -22
- package/src/scheduleManager.ts +314 -0
- package/src/summarizer.ts +1 -18
- package/src/summarizerHeuristics.ts +133 -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/src/containerRuntime.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
|
-
import { EventEmitter } from "events";
|
|
6
5
|
import { ITelemetryBaseLogger, ITelemetryGenericEvent, ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
7
6
|
import {
|
|
8
7
|
FluidObject,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
ILoaderOptions,
|
|
25
24
|
LoaderHeader,
|
|
26
25
|
ISnapshotTreeWithBlobContents,
|
|
26
|
+
IBatchMessage,
|
|
27
27
|
} from "@fluidframework/container-definitions";
|
|
28
28
|
import {
|
|
29
29
|
IContainerRuntime,
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
Trace,
|
|
35
35
|
TypedEventEmitter,
|
|
36
36
|
unreachableCase,
|
|
37
|
-
performance,
|
|
38
37
|
} from "@fluidframework/common-utils";
|
|
39
38
|
import {
|
|
40
39
|
ChildLogger,
|
|
@@ -43,16 +42,20 @@ import {
|
|
|
43
42
|
TaggedLoggerAdapter,
|
|
44
43
|
MonitoringContext,
|
|
45
44
|
loggerToMonitoringContext,
|
|
46
|
-
|
|
45
|
+
wrapError,
|
|
47
46
|
} from "@fluidframework/telemetry-utils";
|
|
48
|
-
import {
|
|
49
|
-
|
|
47
|
+
import {
|
|
48
|
+
DriverHeader,
|
|
49
|
+
FetchSource,
|
|
50
|
+
IDocumentStorageService,
|
|
51
|
+
ISummaryContext,
|
|
52
|
+
} from "@fluidframework/driver-definitions";
|
|
53
|
+
import { readAndParse } from "@fluidframework/driver-utils";
|
|
50
54
|
import {
|
|
51
55
|
DataCorruptionError,
|
|
52
56
|
DataProcessingError,
|
|
53
57
|
GenericError,
|
|
54
58
|
UsageError,
|
|
55
|
-
extractSafePropertiesFromMessage,
|
|
56
59
|
} from "@fluidframework/container-utils";
|
|
57
60
|
import {
|
|
58
61
|
IClientDetails,
|
|
@@ -108,15 +111,17 @@ import { ContainerFluidHandleContext } from "./containerHandleContext";
|
|
|
108
111
|
import { FluidDataStoreRegistry } from "./dataStoreRegistry";
|
|
109
112
|
import { Summarizer } from "./summarizer";
|
|
110
113
|
import { SummaryManager } from "./summaryManager";
|
|
111
|
-
import { DeltaScheduler } from "./deltaScheduler";
|
|
112
114
|
import {
|
|
113
115
|
ReportOpPerfTelemetry,
|
|
114
|
-
latencyThreshold,
|
|
115
116
|
IPerfSignalReport,
|
|
116
117
|
} from "./connectionTelemetry";
|
|
117
|
-
import {
|
|
118
|
+
import {
|
|
119
|
+
IPendingLocalState,
|
|
120
|
+
PendingStateManager,
|
|
121
|
+
} from "./pendingStateManager";
|
|
122
|
+
import { BatchManager, BatchMessage } from "./batchManager";
|
|
118
123
|
import { pkgVersion } from "./packageVersion";
|
|
119
|
-
import { BlobManager, IBlobManagerLoadInfo } from "./blobManager";
|
|
124
|
+
import { BlobManager, IBlobManagerLoadInfo, IPendingBlobs } from "./blobManager";
|
|
120
125
|
import { DataStores, getSummaryForDatastores } from "./dataStores";
|
|
121
126
|
import {
|
|
122
127
|
aliasBlobName,
|
|
@@ -160,7 +165,7 @@ import {
|
|
|
160
165
|
} from "./dataStore";
|
|
161
166
|
import { BindBatchTracker } from "./batchTracker";
|
|
162
167
|
import { ISerializedBaseSnapshotBlobs, SerializedSnapshotStorage } from "./serializedSnapshotStorage";
|
|
163
|
-
import {
|
|
168
|
+
import { ScheduleManager } from "./scheduleManager";
|
|
164
169
|
|
|
165
170
|
export enum ContainerMessageType {
|
|
166
171
|
// An op to be delivered to store
|
|
@@ -196,9 +201,10 @@ export interface ContainerRuntimeMessage {
|
|
|
196
201
|
contents: any;
|
|
197
202
|
type: ContainerMessageType;
|
|
198
203
|
}
|
|
204
|
+
|
|
199
205
|
export interface ISummaryBaseConfiguration {
|
|
200
206
|
/**
|
|
201
|
-
*
|
|
207
|
+
* Delay before first attempt to spawn summarizing container.
|
|
202
208
|
*/
|
|
203
209
|
initialSummarizerDelayMs: number;
|
|
204
210
|
|
|
@@ -224,12 +230,15 @@ export interface ISummaryBaseConfiguration {
|
|
|
224
230
|
export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfiguration {
|
|
225
231
|
state: "enabled";
|
|
226
232
|
/**
|
|
227
|
-
*
|
|
233
|
+
* @deprecated Please move all implementations to {@link ISummaryConfigurationHeuristics.minIdleTime} and
|
|
234
|
+
* {@link ISummaryConfigurationHeuristics.maxIdleTime} instead.
|
|
228
235
|
*/
|
|
229
236
|
idleTime: number;
|
|
230
237
|
/**
|
|
231
|
-
* Defines the maximum allowed time, since the last received Ack,
|
|
238
|
+
* Defines the maximum allowed time, since the last received Ack, before running the summary
|
|
232
239
|
* with reason maxTime.
|
|
240
|
+
* For example, say we receive ops one by one just before the idle time is triggered.
|
|
241
|
+
* In this case, we still want to run a summary since it's been a while since the last summary.
|
|
233
242
|
*/
|
|
234
243
|
maxTime: number;
|
|
235
244
|
/**
|
|
@@ -242,6 +251,34 @@ export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfigurati
|
|
|
242
251
|
* before running the last summary.
|
|
243
252
|
*/
|
|
244
253
|
minOpsForLastSummaryAttempt: number;
|
|
254
|
+
/**
|
|
255
|
+
* Defines the lower boundary for the allowed time in between summarizations.
|
|
256
|
+
* Pairs with maxIdleTime to form a range.
|
|
257
|
+
* For example, if we only receive 1 op, we don't want to have the same idle time as say 100 ops.
|
|
258
|
+
* Based on the boundaries we set in minIdleTime and maxIdleTime, the idle time will change
|
|
259
|
+
* linearly depending on the number of ops we receive.
|
|
260
|
+
*/
|
|
261
|
+
minIdleTime: number;
|
|
262
|
+
/**
|
|
263
|
+
* Defines the upper boundary for the allowed time in between summarizations.
|
|
264
|
+
* Pairs with minIdleTime to form a range.
|
|
265
|
+
* For example, if we only receive 1 op, we don't want to have the same idle time as say 100 ops.
|
|
266
|
+
* Based on the boundaries we set in minIdleTime and maxIdleTime, the idle time will change
|
|
267
|
+
* linearly depending on the number of ops we receive.
|
|
268
|
+
*/
|
|
269
|
+
maxIdleTime: number;
|
|
270
|
+
/**
|
|
271
|
+
* Runtime op weight to use in heuristic summarizing.
|
|
272
|
+
* This number is a multiplier on the number of runtime ops we process when running summarize heuristics.
|
|
273
|
+
* For example: (multiplier) * (number of runtime ops) = weighted number of runtime ops
|
|
274
|
+
*/
|
|
275
|
+
runtimeOpWeight: number;
|
|
276
|
+
/**
|
|
277
|
+
* Non-runtime op weight to use in heuristic summarizing
|
|
278
|
+
* This number is a multiplier on the number of non-runtime ops we process when running summarize heuristics.
|
|
279
|
+
* For example: (multiplier) * (number of non-runtime ops) = weighted number of non-runtime ops
|
|
280
|
+
*/
|
|
281
|
+
nonRuntimeOpWeight: number;
|
|
245
282
|
}
|
|
246
283
|
|
|
247
284
|
export interface ISummaryConfigurationDisableSummarizer {
|
|
@@ -260,44 +297,57 @@ export type ISummaryConfiguration =
|
|
|
260
297
|
export const DefaultSummaryConfiguration: ISummaryConfiguration = {
|
|
261
298
|
state: "enabled",
|
|
262
299
|
|
|
263
|
-
idleTime:
|
|
300
|
+
idleTime: 15 * 1000, // 15 secs.
|
|
301
|
+
|
|
302
|
+
minIdleTime: 0,
|
|
264
303
|
|
|
265
|
-
|
|
304
|
+
maxIdleTime: 30 * 1000, // 30 secs.
|
|
266
305
|
|
|
267
|
-
|
|
306
|
+
maxTime: 60 * 1000, // 1 min.
|
|
307
|
+
|
|
308
|
+
maxOps: 100, // Summarize if 100 weighted ops received since last snapshot.
|
|
268
309
|
|
|
269
310
|
minOpsForLastSummaryAttempt: 10,
|
|
270
311
|
|
|
271
|
-
maxAckWaitTime:
|
|
312
|
+
maxAckWaitTime: 10 * 60 * 1000, // 10 mins.
|
|
272
313
|
|
|
273
314
|
maxOpsSinceLastSummary: 7000,
|
|
274
315
|
|
|
275
|
-
initialSummarizerDelayMs:
|
|
316
|
+
initialSummarizerDelayMs: 5 * 1000, // 5 secs.
|
|
276
317
|
|
|
277
318
|
summarizerClientElection: false,
|
|
319
|
+
|
|
320
|
+
nonRuntimeOpWeight: 0.1,
|
|
321
|
+
|
|
322
|
+
runtimeOpWeight: 1.0,
|
|
278
323
|
};
|
|
279
324
|
|
|
280
325
|
export interface IGCRuntimeOptions {
|
|
281
326
|
/**
|
|
282
|
-
* Flag that if true, will enable running garbage collection (GC)
|
|
283
|
-
*
|
|
284
|
-
* mark phase.
|
|
327
|
+
* Flag that if true, will enable running garbage collection (GC) for a new container.
|
|
328
|
+
*
|
|
329
|
+
* GC has mark phase and sweep phase. In mark phase, unreferenced objects are identified
|
|
330
|
+
* and marked as such in the summary. This option enables the mark phase.
|
|
285
331
|
* In sweep phase, unreferenced objects are eventually deleted from the container if they meet certain conditions.
|
|
286
332
|
* Sweep phase can be enabled via the "sweepAllowed" option.
|
|
287
|
-
*
|
|
333
|
+
*
|
|
334
|
+
* Note: This setting is persisted in the container's summary and cannot be changed.
|
|
288
335
|
*/
|
|
289
336
|
gcAllowed?: boolean;
|
|
290
337
|
|
|
291
338
|
/**
|
|
292
|
-
* Flag that if true, enables GC's sweep phase
|
|
339
|
+
* Flag that if true, enables GC's sweep phase for a new container.
|
|
340
|
+
*
|
|
341
|
+
* This will allow GC to eventually delete unreferenced objects from the container.
|
|
293
342
|
* This flag should only be set to true if "gcAllowed" is true.
|
|
294
|
-
*
|
|
343
|
+
*
|
|
344
|
+
* Note: This setting is persisted in the container's summary and cannot be changed.
|
|
295
345
|
*/
|
|
296
346
|
sweepAllowed?: boolean;
|
|
297
347
|
|
|
298
348
|
/**
|
|
299
|
-
* Flag that will disable garbage collection
|
|
300
|
-
* is allowed via the gcAllowed option.
|
|
349
|
+
* Flag that if true, will disable garbage collection for the session.
|
|
350
|
+
* Can be used to disable running GC on containers where it is allowed via the gcAllowed option.
|
|
301
351
|
*/
|
|
302
352
|
disableGC?: boolean;
|
|
303
353
|
|
|
@@ -307,6 +357,13 @@ export interface IGCRuntimeOptions {
|
|
|
307
357
|
*/
|
|
308
358
|
runFullGC?: boolean;
|
|
309
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Maximum session duration for a new container. If not present, a default value will be used.
|
|
362
|
+
*
|
|
363
|
+
* Note: This setting is persisted in the container's summary and cannot be changed.
|
|
364
|
+
*/
|
|
365
|
+
sessionExpiryTimeoutMs?: number;
|
|
366
|
+
|
|
310
367
|
/**
|
|
311
368
|
* Allows additional GC options to be passed.
|
|
312
369
|
*/
|
|
@@ -318,39 +375,46 @@ export interface ISummaryRuntimeOptions {
|
|
|
318
375
|
/** Override summary configurations set by the server. */
|
|
319
376
|
summaryConfigOverrides?: ISummaryConfiguration;
|
|
320
377
|
|
|
321
|
-
// Flag that disables putting channels in isolated subtrees for each data store
|
|
322
|
-
// and the root node when generating a summary if set to true.
|
|
323
|
-
// Defaults to FALSE (enabled) for now.
|
|
324
|
-
disableIsolatedChannels?: boolean;
|
|
325
|
-
|
|
326
378
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
|
|
379
|
+
* Delay before first attempt to spawn summarizing container.
|
|
380
|
+
*
|
|
381
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
382
|
+
* {@link ISummaryBaseConfiguration.initialSummarizerDelayMs} instead.
|
|
383
|
+
*/
|
|
330
384
|
initialSummarizerDelayMs?: number;
|
|
331
385
|
|
|
332
386
|
/**
|
|
333
|
-
* @deprecated - use `summaryConfigOverrides.disableSummaries` instead.
|
|
334
387
|
* Flag that disables summaries if it is set to true.
|
|
388
|
+
*
|
|
389
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
390
|
+
* {@link ISummaryConfigurationDisableSummarizer.state} instead.
|
|
335
391
|
*/
|
|
336
392
|
disableSummaries?: boolean;
|
|
337
393
|
|
|
338
394
|
/**
|
|
339
|
-
* @
|
|
340
|
-
*
|
|
395
|
+
* @defaultValue 7000 operations (ops)
|
|
396
|
+
*
|
|
397
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
398
|
+
* {@link ISummaryBaseConfiguration.maxOpsSinceLastSummary} instead.
|
|
341
399
|
*/
|
|
342
400
|
maxOpsSinceLastSummary?: number;
|
|
343
401
|
|
|
344
402
|
/**
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
403
|
+
* Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
|
|
404
|
+
*
|
|
405
|
+
* @defaultValue `false` (disabled) and must be explicitly set to true to enable.
|
|
406
|
+
*
|
|
407
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
408
|
+
* {@link ISummaryBaseConfiguration.summarizerClientElection} instead.
|
|
409
|
+
*/
|
|
349
410
|
summarizerClientElection?: boolean;
|
|
350
411
|
|
|
351
412
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
413
|
+
* Options that control the running summarizer behavior.
|
|
414
|
+
*
|
|
415
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
416
|
+
* `{@link ISummaryConfiguration.state} = "DisableHeuristics"` instead.
|
|
417
|
+
* */
|
|
354
418
|
summarizerOptions?: Readonly<Partial<ISummarizerOptions>>;
|
|
355
419
|
}
|
|
356
420
|
|
|
@@ -369,12 +433,6 @@ export interface IContainerRuntimeOptions {
|
|
|
369
433
|
* 3. "bypass" will skip the check entirely. This is not recommended.
|
|
370
434
|
*/
|
|
371
435
|
readonly loadSequenceNumberVerification?: "close" | "log" | "bypass";
|
|
372
|
-
/**
|
|
373
|
-
* Should the runtime use data store aliasing for creating root datastores.
|
|
374
|
-
* In case of aliasing conflicts, the runtime will raise an exception which does
|
|
375
|
-
* not effect the status of the container.
|
|
376
|
-
*/
|
|
377
|
-
readonly useDataStoreAliasing?: boolean;
|
|
378
436
|
/**
|
|
379
437
|
* Sets the flush mode for the runtime. In Immediate flush mode the runtime will immediately
|
|
380
438
|
* send all operations to the driver layer, while in TurnBased the operations will be buffered
|
|
@@ -388,10 +446,6 @@ export interface IContainerRuntimeOptions {
|
|
|
388
446
|
readonly enableOfflineLoad?: boolean;
|
|
389
447
|
}
|
|
390
448
|
|
|
391
|
-
type IRuntimeMessageMetadata = undefined | {
|
|
392
|
-
batch?: boolean;
|
|
393
|
-
};
|
|
394
|
-
|
|
395
449
|
/**
|
|
396
450
|
* The summary tree returned by the root node. It adds state relevant to the root of the tree.
|
|
397
451
|
*/
|
|
@@ -431,11 +485,15 @@ interface OldContainerContextWithLogger extends Omit<IContainerContext, "taggedL
|
|
|
431
485
|
* instantiated runtime in a new instance of the container, so it can load to the
|
|
432
486
|
* same state
|
|
433
487
|
*/
|
|
434
|
-
|
|
488
|
+
interface IPendingRuntimeState {
|
|
435
489
|
/**
|
|
436
490
|
* Pending ops from PendingStateManager
|
|
437
491
|
*/
|
|
438
492
|
pending?: IPendingLocalState;
|
|
493
|
+
/**
|
|
494
|
+
* Pending blobs from BlobManager
|
|
495
|
+
*/
|
|
496
|
+
pendingAttachmentBlobs?: IPendingBlobs;
|
|
439
497
|
/**
|
|
440
498
|
* A base snapshot at a sequence number prior to the first pending op
|
|
441
499
|
*/
|
|
@@ -453,26 +511,13 @@ export interface IPendingRuntimeState {
|
|
|
453
511
|
savedOps: ISequencedDocumentMessage[];
|
|
454
512
|
}
|
|
455
513
|
|
|
456
|
-
const useDataStoreAliasingKey = "Fluid.ContainerRuntime.UseDataStoreAliasing";
|
|
457
514
|
const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
|
|
458
515
|
|
|
459
|
-
// Feature gate for the max op size. If the value is negative, chunking is enabled
|
|
460
|
-
// and all ops over 16k would be chunked. If the value is positive, all ops with
|
|
461
|
-
// a size strictly larger will be rejected and the container closed with an error.
|
|
462
|
-
const maxOpSizeInBytesKey = "Fluid.ContainerRuntime.MaxOpSizeInBytes";
|
|
463
|
-
|
|
464
|
-
// By default, we should reject any op larger than 768KB,
|
|
465
|
-
// in order to account for some extra overhead from serialization
|
|
466
|
-
// to not reach the 1MB limits in socket.io and Kafka.
|
|
467
|
-
const defaultMaxOpSizeInBytes = 768000;
|
|
468
|
-
|
|
469
|
-
// By default, the size of the contents for the incoming ops is tracked.
|
|
470
|
-
// However, in certain situations, this may incur a performance hit.
|
|
471
|
-
// The feature-gate below can be used to disable this feature.
|
|
472
|
-
const disableOpTrackingKey = "Fluid.ContainerRuntime.DisableOpTracking";
|
|
473
|
-
|
|
474
516
|
const defaultFlushMode = FlushMode.TurnBased;
|
|
475
517
|
|
|
518
|
+
/**
|
|
519
|
+
* @deprecated - use ContainerRuntimeMessage instead
|
|
520
|
+
*/
|
|
476
521
|
export enum RuntimeMessage {
|
|
477
522
|
FluidDataStoreOp = "component",
|
|
478
523
|
Attach = "attach",
|
|
@@ -483,6 +528,9 @@ export enum RuntimeMessage {
|
|
|
483
528
|
Operation = "op",
|
|
484
529
|
}
|
|
485
530
|
|
|
531
|
+
/**
|
|
532
|
+
* @deprecated - please use version in driver-utils
|
|
533
|
+
*/
|
|
486
534
|
export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
|
|
487
535
|
if ((Object.values(RuntimeMessage) as string[]).includes(message.type)) {
|
|
488
536
|
return true;
|
|
@@ -490,6 +538,15 @@ export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
|
|
|
490
538
|
return false;
|
|
491
539
|
}
|
|
492
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Unpacks runtime messages
|
|
543
|
+
*
|
|
544
|
+
* @remarks This API makes no promises regarding backward-compatability. This is internal API.
|
|
545
|
+
* @param message - message (as it observed in storage / service)
|
|
546
|
+
* @returns unpacked runtime message
|
|
547
|
+
*
|
|
548
|
+
* @internal
|
|
549
|
+
*/
|
|
493
550
|
export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
|
|
494
551
|
if (message.type === MessageType.Operation) {
|
|
495
552
|
// legacy op format?
|
|
@@ -502,287 +559,13 @@ export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
|
|
|
502
559
|
message.type = innerContents.type;
|
|
503
560
|
message.contents = innerContents.contents;
|
|
504
561
|
}
|
|
505
|
-
|
|
562
|
+
return true;
|
|
506
563
|
} else {
|
|
507
564
|
// Legacy format, but it's already "unpacked",
|
|
508
565
|
// i.e. message.type is actually ContainerMessageType.
|
|
566
|
+
// Or it's non-runtime message.
|
|
509
567
|
// Nothing to do in such case.
|
|
510
|
-
|
|
511
|
-
return message;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* This class controls pausing and resuming of inbound queue to ensure that we never
|
|
516
|
-
* start processing ops in a batch IF we do not have all ops in the batch.
|
|
517
|
-
*/
|
|
518
|
-
class ScheduleManagerCore {
|
|
519
|
-
private pauseSequenceNumber: number | undefined;
|
|
520
|
-
private currentBatchClientId: string | undefined;
|
|
521
|
-
private localPaused = false;
|
|
522
|
-
private timePaused = 0;
|
|
523
|
-
private batchCount = 0;
|
|
524
|
-
|
|
525
|
-
constructor(
|
|
526
|
-
private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
|
|
527
|
-
private readonly logger: ITelemetryLogger,
|
|
528
|
-
) {
|
|
529
|
-
// Listen for delta manager sends and add batch metadata to messages
|
|
530
|
-
this.deltaManager.on("prepareSend", (messages: IDocumentMessage[]) => {
|
|
531
|
-
if (messages.length === 0) {
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// First message will have the batch flag set to true if doing a batched send
|
|
536
|
-
const firstMessageMetadata = messages[0].metadata as IRuntimeMessageMetadata;
|
|
537
|
-
if (!firstMessageMetadata?.batch) {
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// If the batch contains only a single op, clear the batch flag.
|
|
542
|
-
if (messages.length === 1) {
|
|
543
|
-
delete firstMessageMetadata.batch;
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Set the batch flag to false on the last message to indicate the end of the send batch
|
|
548
|
-
const lastMessage = messages[messages.length - 1];
|
|
549
|
-
lastMessage.metadata = { ...lastMessage.metadata, batch: false };
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
// Listen for updates and peek at the inbound
|
|
553
|
-
this.deltaManager.inbound.on(
|
|
554
|
-
"push",
|
|
555
|
-
(message: ISequencedDocumentMessage) => {
|
|
556
|
-
this.trackPending(message);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
// Start with baseline - empty inbound queue.
|
|
560
|
-
assert(!this.localPaused, 0x293 /* "initial state" */);
|
|
561
|
-
|
|
562
|
-
const allPending = this.deltaManager.inbound.toArray();
|
|
563
|
-
for (const pending of allPending) {
|
|
564
|
-
this.trackPending(pending);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// We are intentionally directly listening to the "op" to inspect system ops as well.
|
|
568
|
-
// If we do not observe system ops, we are likely to hit 0x296 assert when system ops
|
|
569
|
-
// precedes start of incomplete batch.
|
|
570
|
-
this.deltaManager.on("op", (message) => this.afterOpProcessing(message.sequenceNumber));
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* The only public function in this class - called when we processed an op,
|
|
575
|
-
* to make decision if op processing should be paused or not afer that.
|
|
576
|
-
*/
|
|
577
|
-
public afterOpProcessing(sequenceNumber: number) {
|
|
578
|
-
assert(!this.localPaused, 0x294 /* "can't have op processing paused if we are processing an op" */);
|
|
579
|
-
|
|
580
|
-
// If the inbound queue is ever empty, nothing to do!
|
|
581
|
-
if (this.deltaManager.inbound.length === 0) {
|
|
582
|
-
assert(this.pauseSequenceNumber === undefined,
|
|
583
|
-
0x295 /* "there should be no pending batch if we have no ops" */);
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// The queue is
|
|
588
|
-
// 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
|
|
589
|
-
// - here (processing ops until reaching start of incomplete batch)
|
|
590
|
-
// - in trackPending(), when queue was empty and start of batch showed up.
|
|
591
|
-
// 2. resumed when batch end comes in (in trackPending())
|
|
592
|
-
|
|
593
|
-
// do we have incomplete batch to worry about?
|
|
594
|
-
if (this.pauseSequenceNumber !== undefined) {
|
|
595
|
-
assert(sequenceNumber < this.pauseSequenceNumber,
|
|
596
|
-
0x296 /* "we should never start processing incomplete batch!" */);
|
|
597
|
-
// If the next op is the start of incomplete batch, then we can't process it until it's fully in - pause!
|
|
598
|
-
if (sequenceNumber + 1 === this.pauseSequenceNumber) {
|
|
599
|
-
this.pauseQueue();
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
private pauseQueue() {
|
|
605
|
-
assert(!this.localPaused, 0x297 /* "always called from resumed state" */);
|
|
606
|
-
this.localPaused = true;
|
|
607
|
-
this.timePaused = performance.now();
|
|
608
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
609
|
-
this.deltaManager.inbound.pause();
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
private resumeQueue(startBatch: number, messageEndBatch: ISequencedDocumentMessage) {
|
|
613
|
-
const endBatch = messageEndBatch.sequenceNumber;
|
|
614
|
-
const duration = this.localPaused ? (performance.now() - this.timePaused) : undefined;
|
|
615
|
-
|
|
616
|
-
this.batchCount++;
|
|
617
|
-
if (this.batchCount % 1000 === 1) {
|
|
618
|
-
this.logger.sendTelemetryEvent({
|
|
619
|
-
eventName: "BatchStats",
|
|
620
|
-
sequenceNumber: endBatch,
|
|
621
|
-
length: endBatch - startBatch + 1,
|
|
622
|
-
msnDistance: endBatch - messageEndBatch.minimumSequenceNumber,
|
|
623
|
-
duration,
|
|
624
|
-
batchCount: this.batchCount,
|
|
625
|
-
interrupted: this.localPaused,
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Return early if no change in value
|
|
630
|
-
if (!this.localPaused) {
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
this.localPaused = false;
|
|
635
|
-
|
|
636
|
-
// Random round number - we want to know when batch waiting paused op processing.
|
|
637
|
-
if (duration !== undefined && duration > latencyThreshold) {
|
|
638
|
-
this.logger.sendErrorEvent({
|
|
639
|
-
eventName: "MaxBatchWaitTimeExceeded",
|
|
640
|
-
duration,
|
|
641
|
-
sequenceNumber: endBatch,
|
|
642
|
-
length: endBatch - startBatch,
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
this.deltaManager.inbound.resume();
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Called for each incoming op (i.e. inbound "push" notification)
|
|
650
|
-
*/
|
|
651
|
-
private trackPending(message: ISequencedDocumentMessage) {
|
|
652
|
-
assert(this.deltaManager.inbound.length !== 0,
|
|
653
|
-
0x298 /* "we have something in the queue that generates this event" */);
|
|
654
|
-
|
|
655
|
-
assert((this.currentBatchClientId === undefined) === (this.pauseSequenceNumber === undefined),
|
|
656
|
-
0x299 /* "non-synchronized state" */);
|
|
657
|
-
|
|
658
|
-
const metadata = message.metadata as IRuntimeMessageMetadata;
|
|
659
|
-
const batchMetadata = metadata?.batch;
|
|
660
|
-
|
|
661
|
-
// Protocol messages are never part of a runtime batch of messages
|
|
662
|
-
if (!isUnpackedRuntimeMessage(message)) {
|
|
663
|
-
// Protocol messages should never show up in the middle of the batch!
|
|
664
|
-
assert(this.currentBatchClientId === undefined, 0x29a /* "System message in the middle of batch!" */);
|
|
665
|
-
assert(batchMetadata === undefined, 0x29b /* "system op in a batch?" */);
|
|
666
|
-
assert(!this.localPaused, 0x29c /* "we should be processing ops when there is no active batch" */);
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (this.currentBatchClientId === undefined && batchMetadata === undefined) {
|
|
671
|
-
assert(!this.localPaused, 0x29d /* "we should be processing ops when there is no active batch" */);
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// If the client ID changes then we can move the pause point. If it stayed the same then we need to check.
|
|
676
|
-
// If batchMetadata is not undefined then if it's true we've begun a new batch - if false we've ended
|
|
677
|
-
// the previous one
|
|
678
|
-
if (this.currentBatchClientId !== undefined || batchMetadata === false) {
|
|
679
|
-
if (this.currentBatchClientId !== message.clientId) {
|
|
680
|
-
// "Batch not closed, yet message from another client!"
|
|
681
|
-
throw new DataCorruptionError(
|
|
682
|
-
"OpBatchIncomplete",
|
|
683
|
-
{
|
|
684
|
-
runtimeVersion: pkgVersion,
|
|
685
|
-
batchClientId: this.currentBatchClientId,
|
|
686
|
-
...extractSafePropertiesFromMessage(message),
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// The queue is
|
|
692
|
-
// 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
|
|
693
|
-
// - in afterOpProcessing() - processing ops until reaching start of incomplete batch
|
|
694
|
-
// - here (batchMetadata == false below), when queue was empty and start of batch showed up.
|
|
695
|
-
// 2. resumed when batch end comes in (batchMetadata === true case below)
|
|
696
|
-
|
|
697
|
-
if (batchMetadata) {
|
|
698
|
-
assert(this.currentBatchClientId === undefined, 0x29e /* "there can't be active batch" */);
|
|
699
|
-
assert(!this.localPaused, 0x29f /* "we should be processing ops when there is no active batch" */);
|
|
700
|
-
this.pauseSequenceNumber = message.sequenceNumber;
|
|
701
|
-
this.currentBatchClientId = message.clientId;
|
|
702
|
-
// Start of the batch
|
|
703
|
-
// Only pause processing if queue has no other ops!
|
|
704
|
-
// If there are any other ops in the queue, processing will be stopped when they are processed!
|
|
705
|
-
if (this.deltaManager.inbound.length === 1) {
|
|
706
|
-
this.pauseQueue();
|
|
707
|
-
}
|
|
708
|
-
} else if (batchMetadata === false) {
|
|
709
|
-
assert(this.pauseSequenceNumber !== undefined, 0x2a0 /* "batch presence was validated above" */);
|
|
710
|
-
// Batch is complete, we can process it!
|
|
711
|
-
this.resumeQueue(this.pauseSequenceNumber, message);
|
|
712
|
-
this.pauseSequenceNumber = undefined;
|
|
713
|
-
this.currentBatchClientId = undefined;
|
|
714
|
-
} else {
|
|
715
|
-
// Continuation of current batch. Do nothing
|
|
716
|
-
assert(this.currentBatchClientId !== undefined, 0x2a1 /* "logic error" */);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* This class has the following responsibilities:
|
|
723
|
-
* 1. It tracks batches as we process ops and raises "batchBegin" and "batchEnd" events.
|
|
724
|
-
* As part of it, it validates batch correctness (i.e. no system ops in the middle of batch)
|
|
725
|
-
* 2. It creates instance of ScheduleManagerCore that ensures we never start processing ops from batch
|
|
726
|
-
* unless all ops of the batch are in.
|
|
727
|
-
*/
|
|
728
|
-
export class ScheduleManager {
|
|
729
|
-
private readonly deltaScheduler: DeltaScheduler;
|
|
730
|
-
private batchClientId: string | undefined;
|
|
731
|
-
private hitError = false;
|
|
732
|
-
|
|
733
|
-
constructor(
|
|
734
|
-
private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
|
|
735
|
-
private readonly emitter: EventEmitter,
|
|
736
|
-
private readonly logger: ITelemetryLogger,
|
|
737
|
-
) {
|
|
738
|
-
this.deltaScheduler = new DeltaScheduler(
|
|
739
|
-
this.deltaManager,
|
|
740
|
-
ChildLogger.create(this.logger, "DeltaScheduler"),
|
|
741
|
-
);
|
|
742
|
-
void new ScheduleManagerCore(deltaManager, logger);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
public beforeOpProcessing(message: ISequencedDocumentMessage) {
|
|
746
|
-
if (this.batchClientId !== message.clientId) {
|
|
747
|
-
assert(this.batchClientId === undefined,
|
|
748
|
-
0x2a2 /* "Batch is interrupted by other client op. Should be caught by trackPending()" */);
|
|
749
|
-
|
|
750
|
-
// This could be the beginning of a new batch or an individual message.
|
|
751
|
-
this.emitter.emit("batchBegin", message);
|
|
752
|
-
this.deltaScheduler.batchBegin(message);
|
|
753
|
-
|
|
754
|
-
const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
|
|
755
|
-
if (batch) {
|
|
756
|
-
this.batchClientId = message.clientId;
|
|
757
|
-
} else {
|
|
758
|
-
this.batchClientId = undefined;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
public afterOpProcessing(error: any | undefined, message: ISequencedDocumentMessage) {
|
|
764
|
-
// If this is no longer true, we need to revisit what we do where we set this.hitError.
|
|
765
|
-
assert(!this.hitError, 0x2a3 /* "container should be closed on any error" */);
|
|
766
|
-
|
|
767
|
-
if (error) {
|
|
768
|
-
// We assume here that loader will close container and stop processing all future ops.
|
|
769
|
-
// This is implicit dependency. If this flow changes, this code might no longer be correct.
|
|
770
|
-
this.hitError = true;
|
|
771
|
-
this.batchClientId = undefined;
|
|
772
|
-
this.emitter.emit("batchEnd", error, message);
|
|
773
|
-
this.deltaScheduler.batchEnd(message);
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
|
|
778
|
-
// If no batchClientId has been set then we're in an individual batch. Else, if we get
|
|
779
|
-
// batch end metadata, this is end of the current batch.
|
|
780
|
-
if (this.batchClientId === undefined || batch === false) {
|
|
781
|
-
this.batchClientId = undefined;
|
|
782
|
-
this.emitter.emit("batchEnd", undefined, message);
|
|
783
|
-
this.deltaScheduler.batchEnd(message);
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
568
|
+
return false;
|
|
786
569
|
}
|
|
787
570
|
}
|
|
788
571
|
|
|
@@ -852,7 +635,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
852
635
|
summaryOptions = {},
|
|
853
636
|
gcOptions = {},
|
|
854
637
|
loadSequenceNumberVerification = "close",
|
|
855
|
-
useDataStoreAliasing = false,
|
|
856
638
|
flushMode = defaultFlushMode,
|
|
857
639
|
enableOfflineLoad = false,
|
|
858
640
|
} = runtimeOptions;
|
|
@@ -928,7 +710,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
928
710
|
summaryOptions,
|
|
929
711
|
gcOptions,
|
|
930
712
|
loadSequenceNumberVerification,
|
|
931
|
-
useDataStoreAliasing,
|
|
932
713
|
flushMode,
|
|
933
714
|
enableOfflineLoad,
|
|
934
715
|
},
|
|
@@ -1018,15 +799,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1018
799
|
private readonly summaryCollection: SummaryCollection;
|
|
1019
800
|
|
|
1020
801
|
private readonly summarizerNode: IRootSummarizerNodeWithGC;
|
|
1021
|
-
private readonly _aliasingEnabled: boolean;
|
|
1022
|
-
private readonly _maxOpSizeInBytes: number;
|
|
1023
802
|
|
|
1024
803
|
private readonly maxConsecutiveReconnects: number;
|
|
1025
|
-
private readonly defaultMaxConsecutiveReconnects =
|
|
804
|
+
private readonly defaultMaxConsecutiveReconnects = 7;
|
|
1026
805
|
|
|
1027
806
|
private _orderSequentiallyCalls: number = 0;
|
|
1028
807
|
private _flushMode: FlushMode;
|
|
1029
|
-
private needsFlush = false;
|
|
1030
808
|
private flushTrigger = false;
|
|
1031
809
|
|
|
1032
810
|
private _connected: boolean;
|
|
@@ -1036,6 +814,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1036
814
|
|
|
1037
815
|
private consecutiveReconnects = 0;
|
|
1038
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Used to delay transition to "connected" state while we upload
|
|
819
|
+
* attachment blobs that were added while disconnected
|
|
820
|
+
*/
|
|
821
|
+
private delayConnectClientId?: string;
|
|
822
|
+
|
|
1039
823
|
public get connected(): boolean {
|
|
1040
824
|
return this._connected;
|
|
1041
825
|
}
|
|
@@ -1069,6 +853,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1069
853
|
private readonly scheduleManager: ScheduleManager;
|
|
1070
854
|
private readonly blobManager: BlobManager;
|
|
1071
855
|
private readonly pendingStateManager: PendingStateManager;
|
|
856
|
+
|
|
857
|
+
// Provide lower soft limit - we want to have some number of ops to get efficiency in compression & bandwidth usage,
|
|
858
|
+
// but at the same time we want to send these ops sooner, to reduce overall latency of processing a batch.
|
|
859
|
+
// So there is some ballance here, that depends on compression algorithm and its efficiency working with smaller
|
|
860
|
+
// payloads. That number represents final (compressed) bits (once compression is implemented).
|
|
861
|
+
private readonly pendingAttachBatch = new BatchManager(64 * 1024);
|
|
862
|
+
private readonly pendingBatch = new BatchManager();
|
|
863
|
+
|
|
1072
864
|
private readonly garbageCollector: IGarbageCollector;
|
|
1073
865
|
|
|
1074
866
|
// Local copy of incomplete received chunks.
|
|
@@ -1076,15 +868,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1076
868
|
|
|
1077
869
|
private readonly dataStores: DataStores;
|
|
1078
870
|
|
|
1079
|
-
/**
|
|
1080
|
-
* True if generating summaries with isolated channels is
|
|
1081
|
-
* explicitly disabled. This only affects how summaries are written,
|
|
1082
|
-
* and is the single source of truth for this container.
|
|
1083
|
-
*/
|
|
1084
|
-
public readonly disableIsolatedChannels: boolean;
|
|
1085
871
|
/** The last message processed at the time of the last summary. */
|
|
1086
872
|
private messageAtLastSummary: ISummaryMetadataMessage | undefined;
|
|
1087
873
|
|
|
874
|
+
private get emptyBatch() {
|
|
875
|
+
return this.pendingBatch.empty && this.pendingAttachBatch.empty;
|
|
876
|
+
}
|
|
877
|
+
|
|
1088
878
|
private get summarizer(): Summarizer {
|
|
1089
879
|
assert(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
|
|
1090
880
|
return this._summarizer;
|
|
@@ -1120,11 +910,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1120
910
|
if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
|
|
1121
911
|
return true;
|
|
1122
912
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
return false;
|
|
1127
|
-
}
|
|
913
|
+
return this.summaryConfiguration.state !== "disabled"
|
|
914
|
+
? this.summaryConfiguration.summarizerClientElection === true
|
|
915
|
+
: false;
|
|
1128
916
|
}
|
|
1129
917
|
private readonly maxOpsSinceLastSummary: number;
|
|
1130
918
|
private getMaxOpsSinceLastSummary(): number {
|
|
@@ -1133,11 +921,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1133
921
|
if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
|
|
1134
922
|
return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
|
|
1135
923
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
return 0;
|
|
1140
|
-
}
|
|
924
|
+
return this.summaryConfiguration.state !== "disabled"
|
|
925
|
+
? this.summaryConfiguration.maxOpsSinceLastSummary
|
|
926
|
+
: 0;
|
|
1141
927
|
}
|
|
1142
928
|
|
|
1143
929
|
private readonly initialSummarizerDelayMs: number;
|
|
@@ -1147,11 +933,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1147
933
|
if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
|
|
1148
934
|
return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
|
|
1149
935
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
return 0;
|
|
1154
|
-
}
|
|
936
|
+
return this.summaryConfiguration.state !== "disabled"
|
|
937
|
+
? this.summaryConfiguration.initialSummarizerDelayMs
|
|
938
|
+
: 0;
|
|
1155
939
|
}
|
|
1156
940
|
|
|
1157
941
|
private readonly createContainerMetadata: ICreateContainerMetadata;
|
|
@@ -1160,7 +944,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1160
944
|
* a summary is generated.
|
|
1161
945
|
*/
|
|
1162
946
|
private nextSummaryNumber: number;
|
|
1163
|
-
private readonly opTracker: OpTracker;
|
|
1164
947
|
|
|
1165
948
|
private constructor(
|
|
1166
949
|
private readonly context: IContainerContext,
|
|
@@ -1186,9 +969,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1186
969
|
super();
|
|
1187
970
|
this.messageAtLastSummary = metadata?.message;
|
|
1188
971
|
|
|
1189
|
-
// Default to false (enabled).
|
|
1190
|
-
this.disableIsolatedChannels = this.runtimeOptions.summaryOptions.disableIsolatedChannels ?? false;
|
|
1191
|
-
|
|
1192
972
|
this._connected = this.context.connected;
|
|
1193
973
|
this.chunkMap = new Map<string, string[]>(chunks);
|
|
1194
974
|
|
|
@@ -1197,17 +977,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1197
977
|
this.mc = loggerToMonitoringContext(
|
|
1198
978
|
ChildLogger.create(this.logger, "ContainerRuntime"));
|
|
1199
979
|
|
|
980
|
+
if (this.summaryConfiguration.state === "enabled") {
|
|
981
|
+
this.validateSummaryHeuristicConfiguration(this.summaryConfiguration);
|
|
982
|
+
}
|
|
983
|
+
|
|
1200
984
|
this.summariesDisabled = this.isSummariesDisabled();
|
|
1201
985
|
this.heuristicsDisabled = this.isHeuristicsDisabled();
|
|
1202
986
|
this.summarizerClientElectionEnabled = this.isSummarizerClientElectionEnabled();
|
|
1203
987
|
this.maxOpsSinceLastSummary = this.getMaxOpsSinceLastSummary();
|
|
1204
988
|
this.initialSummarizerDelayMs = this.getInitialSummarizerDelayMs();
|
|
1205
989
|
|
|
1206
|
-
this._aliasingEnabled =
|
|
1207
|
-
(this.mc.config.getBoolean(useDataStoreAliasingKey) ?? false) ||
|
|
1208
|
-
(runtimeOptions.useDataStoreAliasing ?? false);
|
|
1209
|
-
|
|
1210
|
-
this._maxOpSizeInBytes = (this.mc.config.getNumber(maxOpSizeInBytesKey) ?? defaultMaxOpSizeInBytes);
|
|
1211
990
|
this.maxConsecutiveReconnects =
|
|
1212
991
|
this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? this.defaultMaxConsecutiveReconnects;
|
|
1213
992
|
|
|
@@ -1227,6 +1006,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1227
1006
|
getNodePackagePath: async (nodePath: string) => this.getGCNodePackagePath(nodePath),
|
|
1228
1007
|
getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
|
|
1229
1008
|
readAndParseBlob: async <T>(id: string) => readAndParse<T>(this.storage, id),
|
|
1009
|
+
getContainerDiagnosticId: () => this.context.id,
|
|
1010
|
+
activeConnection: () => this.deltaManager.active,
|
|
1230
1011
|
});
|
|
1231
1012
|
|
|
1232
1013
|
const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
|
|
@@ -1288,15 +1069,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1288
1069
|
this.handleContext,
|
|
1289
1070
|
blobManagerSnapshot,
|
|
1290
1071
|
() => this.storage,
|
|
1291
|
-
(blobId
|
|
1072
|
+
(blobId, localId) => {
|
|
1073
|
+
if (!this.disposed) {
|
|
1074
|
+
this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId, localId });
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1292
1077
|
(blobPath: string) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"),
|
|
1293
1078
|
this,
|
|
1294
|
-
|
|
1079
|
+
pendingRuntimeState?.pendingAttachmentBlobs,
|
|
1295
1080
|
);
|
|
1296
1081
|
|
|
1297
1082
|
this.scheduleManager = new ScheduleManager(
|
|
1298
1083
|
context.deltaManager,
|
|
1299
1084
|
this,
|
|
1085
|
+
() => this.clientId,
|
|
1300
1086
|
ChildLogger.create(this.logger, "ScheduleManager"),
|
|
1301
1087
|
);
|
|
1302
1088
|
|
|
@@ -1311,7 +1097,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1311
1097
|
flush: this.flush.bind(this),
|
|
1312
1098
|
flushMode: () => this.flushMode,
|
|
1313
1099
|
reSubmit: this.reSubmit.bind(this),
|
|
1314
|
-
rollback: this.rollback.bind(this),
|
|
1315
1100
|
setFlushMode: (mode) => this.setFlushMode(mode),
|
|
1316
1101
|
},
|
|
1317
1102
|
this._flushMode,
|
|
@@ -1442,9 +1227,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1442
1227
|
createContainerRuntimeVersion: metadata?.createContainerRuntimeVersion,
|
|
1443
1228
|
createContainerTimestamp: metadata?.createContainerTimestamp,
|
|
1444
1229
|
};
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1447
|
-
loadSummaryNumber = metadata?.summaryNumber ??
|
|
1230
|
+
// summaryNumber was renamed from summaryCount. For older docs that haven't been opened for a long time,
|
|
1231
|
+
// the count is reset to 0.
|
|
1232
|
+
loadSummaryNumber = metadata?.summaryNumber ?? 0;
|
|
1448
1233
|
} else {
|
|
1449
1234
|
this.createContainerMetadata = {
|
|
1450
1235
|
createContainerRuntimeVersion: pkgVersion,
|
|
@@ -1466,7 +1251,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1466
1251
|
|
|
1467
1252
|
ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
|
|
1468
1253
|
BindBatchTracker(this, this.logger);
|
|
1469
|
-
this.opTracker = new OpTracker(this.deltaManager, this.mc.config.getBoolean(disableOpTrackingKey) === true);
|
|
1470
1254
|
}
|
|
1471
1255
|
|
|
1472
1256
|
public dispose(error?: Error): void {
|
|
@@ -1546,16 +1330,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1546
1330
|
}
|
|
1547
1331
|
|
|
1548
1332
|
if (id === BlobManager.basePath && requestParser.isLeaf(2)) {
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
|
|
1333
|
+
const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
|
|
1334
|
+
return blob
|
|
1335
|
+
? {
|
|
1552
1336
|
status: 200,
|
|
1553
1337
|
mimeType: "fluid/object",
|
|
1554
|
-
value:
|
|
1555
|
-
};
|
|
1556
|
-
} else {
|
|
1557
|
-
return create404Response(request);
|
|
1558
|
-
}
|
|
1338
|
+
value: blob,
|
|
1339
|
+
} : create404Response(request);
|
|
1559
1340
|
} else if (requestParser.pathParts.length > 0) {
|
|
1560
1341
|
const dataStore = await this.getDataStoreFromRequest(id, request);
|
|
1561
1342
|
const subRequest = requestParser.createSubRequest(1);
|
|
@@ -1573,7 +1354,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1573
1354
|
}
|
|
1574
1355
|
|
|
1575
1356
|
private internalId(maybeAlias: string): string {
|
|
1576
|
-
return this.dataStores.aliases
|
|
1357
|
+
return this.dataStores.aliases.get(maybeAlias) ?? maybeAlias;
|
|
1577
1358
|
}
|
|
1578
1359
|
|
|
1579
1360
|
private async getDataStoreFromRequest(id: string, request: IRequest): Promise<IFluidRouter> {
|
|
@@ -1581,6 +1362,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1581
1362
|
? request.headers?.[RuntimeHeaders.wait]
|
|
1582
1363
|
: true;
|
|
1583
1364
|
|
|
1365
|
+
await this.dataStores.waitIfPendingAlias(id);
|
|
1584
1366
|
const internalId = this.internalId(id);
|
|
1585
1367
|
const dataStoreContext = await this.dataStores.getDataStore(internalId, wait);
|
|
1586
1368
|
|
|
@@ -1620,12 +1402,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1620
1402
|
private addMetadataToSummary(summaryTree: ISummaryTreeWithStats) {
|
|
1621
1403
|
const metadata: IContainerRuntimeMetadata = {
|
|
1622
1404
|
...this.createContainerMetadata,
|
|
1623
|
-
// back-compat 0.59.3000: This is renamed to summaryNumber. Can be removed when 0.59.3000 saturates.
|
|
1624
|
-
summaryCount: this.nextSummaryNumber,
|
|
1625
1405
|
// Increment the summary number for the next summary that will be generated.
|
|
1626
1406
|
summaryNumber: this.nextSummaryNumber++,
|
|
1627
1407
|
summaryFormatVersion: 1,
|
|
1628
|
-
disableIsolatedChannels: this.disableIsolatedChannels || undefined,
|
|
1629
1408
|
...this.garbageCollector.getMetadata(),
|
|
1630
1409
|
// The last message processed at the time of summary. If there are no new messages, use the message from the
|
|
1631
1410
|
// last summary.
|
|
@@ -1647,7 +1426,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1647
1426
|
addBlobToSummary(summaryTree, chunksBlobName, content);
|
|
1648
1427
|
}
|
|
1649
1428
|
|
|
1650
|
-
const dataStoreAliases = this.dataStores.aliases
|
|
1429
|
+
const dataStoreAliases = this.dataStores.aliases;
|
|
1651
1430
|
if (dataStoreAliases.size > 0) {
|
|
1652
1431
|
addBlobToSummary(summaryTree, aliasBlobName, JSON.stringify([...dataStoreAliases]));
|
|
1653
1432
|
}
|
|
@@ -1683,7 +1462,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1683
1462
|
return true;
|
|
1684
1463
|
}
|
|
1685
1464
|
|
|
1686
|
-
if (!this.
|
|
1465
|
+
if (!this.hasPendingMessages()) {
|
|
1687
1466
|
// If there are no pending messages, we can always reconnect
|
|
1688
1467
|
this.resetReconnectCount();
|
|
1689
1468
|
return true;
|
|
@@ -1757,34 +1536,72 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1757
1536
|
}
|
|
1758
1537
|
|
|
1759
1538
|
public setConnectionState(connected: boolean, clientId?: string) {
|
|
1539
|
+
if (connected === false && this.delayConnectClientId !== undefined) {
|
|
1540
|
+
this.delayConnectClientId = undefined;
|
|
1541
|
+
this.mc.logger.sendTelemetryEvent({
|
|
1542
|
+
eventName: "UnsuccessfulConnectedTransition",
|
|
1543
|
+
});
|
|
1544
|
+
// Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// If attachment blobs were added while disconnected, we need to delay
|
|
1549
|
+
// propagation of the "connected" event until we have uploaded them to
|
|
1550
|
+
// ensure we don't submit ops referencing a blob that has not been uploaded
|
|
1551
|
+
const connecting = connected && !this._connected && !this.deltaManager.readOnlyInfo.readonly;
|
|
1552
|
+
if (connecting && this.blobManager.hasPendingOfflineUploads) {
|
|
1553
|
+
assert(!this.delayConnectClientId,
|
|
1554
|
+
0x392 /* Connect event delay must be canceled before subsequent connect event */);
|
|
1555
|
+
assert(!!clientId, 0x393 /* Must have clientId when connecting */);
|
|
1556
|
+
this.delayConnectClientId = clientId;
|
|
1557
|
+
this.blobManager.onConnected().then(() => {
|
|
1558
|
+
// make sure we didn't reconnect before the promise resolved
|
|
1559
|
+
if (this.delayConnectClientId === clientId && !this.disposed) {
|
|
1560
|
+
this.delayConnectClientId = undefined;
|
|
1561
|
+
this.setConnectionStateCore(connected, clientId);
|
|
1562
|
+
}
|
|
1563
|
+
}, (error) => this.closeFn(error));
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
this.setConnectionStateCore(connected, clientId);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
private setConnectionStateCore(connected: boolean, clientId?: string) {
|
|
1571
|
+
assert(!this.delayConnectClientId,
|
|
1572
|
+
0x394 /* connect event delay must be cleared before propagating connect event */);
|
|
1760
1573
|
this.verifyNotClosed();
|
|
1761
1574
|
|
|
1762
1575
|
// There might be no change of state due to Container calling this API after loading runtime.
|
|
1763
1576
|
const changeOfState = this._connected !== connected;
|
|
1764
|
-
const reconnection = changeOfState && connected;
|
|
1577
|
+
const reconnection = changeOfState && !connected;
|
|
1765
1578
|
this._connected = connected;
|
|
1766
1579
|
|
|
1767
1580
|
if (!connected) {
|
|
1768
1581
|
this._perfSignalData.signalsLost = 0;
|
|
1769
1582
|
this._perfSignalData.signalTimestamp = 0;
|
|
1770
1583
|
this._perfSignalData.trackingSignalSequenceNumber = undefined;
|
|
1584
|
+
} else {
|
|
1585
|
+
assert(this.attachState === AttachState.Attached,
|
|
1586
|
+
0x3cd /* Connection is possible only if container exists in storage */);
|
|
1771
1587
|
}
|
|
1772
1588
|
|
|
1589
|
+
// Fail while disconnected
|
|
1773
1590
|
if (reconnection) {
|
|
1774
1591
|
this.consecutiveReconnects++;
|
|
1775
1592
|
|
|
1776
1593
|
if (!this.shouldContinueReconnecting()) {
|
|
1777
1594
|
this.closeFn(
|
|
1778
|
-
// pre-0.58 error message: MaxReconnectsWithNoProgress
|
|
1779
1595
|
DataProcessingError.create(
|
|
1780
|
-
|
|
1596
|
+
// eslint-disable-next-line max-len
|
|
1597
|
+
"Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)",
|
|
1781
1598
|
"setConnectionState",
|
|
1782
1599
|
undefined,
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1600
|
+
{
|
|
1601
|
+
dataLoss: 1,
|
|
1602
|
+
attempts: this.consecutiveReconnects,
|
|
1603
|
+
pendingMessages: this.pendingStateManager.pendingMessagesCount,
|
|
1604
|
+
}));
|
|
1788
1605
|
return;
|
|
1789
1606
|
}
|
|
1790
1607
|
}
|
|
@@ -1794,6 +1611,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1794
1611
|
}
|
|
1795
1612
|
|
|
1796
1613
|
this.dataStores.setConnectionState(connected, clientId);
|
|
1614
|
+
this.garbageCollector.setConnectionState(connected, clientId);
|
|
1797
1615
|
|
|
1798
1616
|
raiseConnectedEvent(this.mc.logger, this, connected, clientId);
|
|
1799
1617
|
}
|
|
@@ -1801,49 +1619,50 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1801
1619
|
public process(messageArg: ISequencedDocumentMessage, local: boolean) {
|
|
1802
1620
|
this.verifyNotClosed();
|
|
1803
1621
|
|
|
1804
|
-
// If it's not message for runtime, bail out right away.
|
|
1805
|
-
if (!isUnpackedRuntimeMessage(messageArg)) {
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
|
|
1810
|
-
this.savedOps.push(messageArg);
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
1622
|
// Do shallow copy of message, as methods below will modify it.
|
|
1814
1623
|
// There might be multiple container instances receiving same message
|
|
1815
1624
|
// We do not need to make deep copy, as each layer will just replace message.content itself,
|
|
1816
1625
|
// but would not modify contents details
|
|
1817
1626
|
let message = { ...messageArg };
|
|
1818
1627
|
|
|
1628
|
+
// back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
|
|
1629
|
+
// System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
|
|
1630
|
+
// Old ops may contain empty string (I assume noops).
|
|
1631
|
+
if (typeof message.contents === "string" && message.contents !== "") {
|
|
1632
|
+
message.contents = JSON.parse(message.contents);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
|
|
1636
|
+
// This format was not shipped to production workflows.
|
|
1637
|
+
const runtimeMessage = unpackRuntimeMessage(message);
|
|
1638
|
+
|
|
1639
|
+
if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
|
|
1640
|
+
this.savedOps.push(messageArg);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1819
1643
|
// Surround the actual processing of the operation with messages to the schedule manager indicating
|
|
1820
1644
|
// the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
|
|
1821
1645
|
// messages once a batch has been fully processed.
|
|
1822
1646
|
this.scheduleManager.beforeOpProcessing(message);
|
|
1823
1647
|
|
|
1824
1648
|
try {
|
|
1825
|
-
message = unpackRuntimeMessage(message);
|
|
1826
|
-
|
|
1827
1649
|
// Chunk processing must come first given that we will transform the message to the unchunked version
|
|
1828
1650
|
// once all pieces are available
|
|
1829
1651
|
message = this.processRemoteChunkedMessage(message);
|
|
1830
1652
|
|
|
1831
1653
|
let localOpMetadata: unknown;
|
|
1832
|
-
if (local) {
|
|
1833
|
-
|
|
1834
|
-
// Do not process local chunked ops until all pieces are available.
|
|
1835
|
-
if (message.type !== ContainerMessageType.ChunkedOp) {
|
|
1836
|
-
localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
|
|
1837
|
-
}
|
|
1654
|
+
if (local && runtimeMessage) {
|
|
1655
|
+
localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
|
|
1838
1656
|
}
|
|
1839
1657
|
|
|
1840
1658
|
// If there are no more pending messages after processing a local message,
|
|
1841
1659
|
// the document is no longer dirty.
|
|
1842
|
-
if (!this.
|
|
1660
|
+
if (!this.hasPendingMessages()) {
|
|
1843
1661
|
this.updateDocumentDirtyState(false);
|
|
1844
1662
|
}
|
|
1845
1663
|
|
|
1846
|
-
|
|
1664
|
+
const type = message.type as ContainerMessageType;
|
|
1665
|
+
switch (type) {
|
|
1847
1666
|
case ContainerMessageType.Attach:
|
|
1848
1667
|
this.dataStores.processAttachMessage(message, local);
|
|
1849
1668
|
break;
|
|
@@ -1854,13 +1673,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1854
1673
|
this.dataStores.processFluidDataStoreOp(message, local, localOpMetadata);
|
|
1855
1674
|
break;
|
|
1856
1675
|
case ContainerMessageType.BlobAttach:
|
|
1857
|
-
|
|
1858
|
-
|
|
1676
|
+
this.blobManager.processBlobAttachOp(message, local);
|
|
1677
|
+
break;
|
|
1678
|
+
case ContainerMessageType.ChunkedOp:
|
|
1679
|
+
case ContainerMessageType.Rejoin:
|
|
1859
1680
|
break;
|
|
1860
1681
|
default:
|
|
1682
|
+
assert(!runtimeMessage, 0x3ce /* Runtime message of unknown type */);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// For back-compat, notify only about runtime messages for now.
|
|
1686
|
+
if (runtimeMessage) {
|
|
1687
|
+
this.emit("op", message, runtimeMessage);
|
|
1861
1688
|
}
|
|
1862
1689
|
|
|
1863
|
-
this.emit("op", message);
|
|
1864
1690
|
this.scheduleManager.afterOpProcessing(undefined, message);
|
|
1865
1691
|
|
|
1866
1692
|
if (local) {
|
|
@@ -1937,6 +1763,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1937
1763
|
}
|
|
1938
1764
|
|
|
1939
1765
|
public async getRootDataStore(id: string, wait = true): Promise<IFluidRouter> {
|
|
1766
|
+
return this.getRootDataStoreChannel(id, wait);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
private async getRootDataStoreChannel(id: string, wait = true): Promise<IFluidDataStoreChannel> {
|
|
1770
|
+
await this.dataStores.waitIfPendingAlias(id);
|
|
1940
1771
|
const internalId = this.internalId(id);
|
|
1941
1772
|
const context = await this.dataStores.getDataStore(internalId, wait);
|
|
1942
1773
|
assert(await context.isRoot(), 0x12b /* "did not get root data store" */);
|
|
@@ -1969,30 +1800,76 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1969
1800
|
assert(this._orderSequentiallyCalls === 0,
|
|
1970
1801
|
0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
|
|
1971
1802
|
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
}
|
|
1803
|
+
this.flushBatch(this.pendingAttachBatch.popBatch());
|
|
1804
|
+
this.flushBatch(this.pendingBatch.popBatch());
|
|
1975
1805
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
// not connected, `this.needsFlush` will be false but the PendingStateManager might have pending messages and
|
|
1979
|
-
// hence needs to track this.
|
|
1980
|
-
this.pendingStateManager.onFlush();
|
|
1806
|
+
assert(this.emptyBatch, 0x3cf /* reentrancy */);
|
|
1807
|
+
}
|
|
1981
1808
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1809
|
+
protected flushBatch(batch: BatchMessage[]): void {
|
|
1810
|
+
const length = batch.length;
|
|
1811
|
+
|
|
1812
|
+
if (length > 1) {
|
|
1813
|
+
batch[0].metadata = { ...batch[0].metadata, batch: true };
|
|
1814
|
+
batch[length - 1].metadata = { ...batch[length - 1].metadata, batch: false };
|
|
1815
|
+
|
|
1816
|
+
// This assert fires for the following reason (there might be more cases like that):
|
|
1817
|
+
// AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
|
|
1818
|
+
// i.e. in the middle of op processing!
|
|
1819
|
+
// Sending ops while processing ops is not good idea - it's not defined when
|
|
1820
|
+
// referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
|
|
1821
|
+
// If we send ops in response to processing multiple ops, then we for sure hit this assert!
|
|
1822
|
+
// Tracked via ADO #1834
|
|
1823
|
+
// assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
|
|
1824
|
+
// "Batch should be generated synchronously, without processing ops in the middle!");
|
|
1985
1825
|
}
|
|
1986
1826
|
|
|
1987
|
-
|
|
1827
|
+
let clientSequenceNumber: number = -1;
|
|
1988
1828
|
|
|
1989
1829
|
// Did we disconnect in the middle of turn-based batch?
|
|
1990
1830
|
// If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
|
|
1991
|
-
if (
|
|
1992
|
-
|
|
1831
|
+
if (this.canSendOps()) {
|
|
1832
|
+
if (this.context.submitBatchFn !== undefined) {
|
|
1833
|
+
const batchToSend: IBatchMessage[] = [];
|
|
1834
|
+
for (const message of batch) {
|
|
1835
|
+
batchToSend.push({ contents: message.contents, metadata: message.metadata });
|
|
1836
|
+
}
|
|
1837
|
+
// returns clientSequenceNumber of last message in a batch
|
|
1838
|
+
clientSequenceNumber = this.context.submitBatchFn(batchToSend);
|
|
1839
|
+
} else {
|
|
1840
|
+
// Legacy path - supporting old loader versions. Can be removed only when LTS moves above
|
|
1841
|
+
// version that has support for batches (submitBatchFn)
|
|
1842
|
+
for (const message of batch) {
|
|
1843
|
+
clientSequenceNumber = this.context.submitFn(
|
|
1844
|
+
MessageType.Operation,
|
|
1845
|
+
message.deserializedContent,
|
|
1846
|
+
true, // batch
|
|
1847
|
+
message.metadata);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
this.deltaSender.flush();
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
|
|
1854
|
+
clientSequenceNumber -= batch.length - 1;
|
|
1855
|
+
assert(clientSequenceNumber >= 0, 0x3d0 /* clientSequenceNumber can't be negative */);
|
|
1993
1856
|
}
|
|
1994
1857
|
|
|
1995
|
-
|
|
1858
|
+
// Let the PendingStateManager know that a message was submitted.
|
|
1859
|
+
// In future, need to shift toward keeping batch as a whole!
|
|
1860
|
+
for (const message of batch) {
|
|
1861
|
+
this.pendingStateManager.onSubmitMessage(
|
|
1862
|
+
message.deserializedContent.type,
|
|
1863
|
+
clientSequenceNumber,
|
|
1864
|
+
message.referenceSequenceNumber,
|
|
1865
|
+
message.deserializedContent.contents,
|
|
1866
|
+
message.localOpMetadata,
|
|
1867
|
+
message.metadata,
|
|
1868
|
+
);
|
|
1869
|
+
clientSequenceNumber++;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
this.pendingStateManager.onFlush();
|
|
1996
1873
|
}
|
|
1997
1874
|
|
|
1998
1875
|
public orderSequentially(callback: () => void): void {
|
|
@@ -2018,9 +1895,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2018
1895
|
}
|
|
2019
1896
|
|
|
2020
1897
|
private trackOrderSequentiallyCalls(callback: () => void): void {
|
|
2021
|
-
let checkpoint: { rollback: () => void; } | undefined;
|
|
1898
|
+
let checkpoint: { rollback: (action: (message: BatchMessage) => void) => void; } | undefined;
|
|
2022
1899
|
if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
|
|
2023
|
-
|
|
1900
|
+
// Note: we are not touching this.pendingAttachBatch here, for two reasons:
|
|
1901
|
+
// 1. It would not help, as we flush attach ops as they become available.
|
|
1902
|
+
// 2. There is no way to undo process of data store creation.
|
|
1903
|
+
checkpoint = this.pendingBatch.checkpoint();
|
|
2024
1904
|
}
|
|
2025
1905
|
|
|
2026
1906
|
try {
|
|
@@ -2029,7 +1909,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2029
1909
|
} catch (error) {
|
|
2030
1910
|
if (checkpoint) {
|
|
2031
1911
|
// This will throw and close the container if rollback fails
|
|
2032
|
-
|
|
1912
|
+
try {
|
|
1913
|
+
checkpoint.rollback((message: BatchMessage) =>
|
|
1914
|
+
this.rollback(
|
|
1915
|
+
message.deserializedContent.type,
|
|
1916
|
+
message.deserializedContent.contents,
|
|
1917
|
+
message.localOpMetadata));
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
const error2 = wrapError(err, (message) => {
|
|
1920
|
+
return DataProcessingError.create(
|
|
1921
|
+
`RollbackError: ${message}`,
|
|
1922
|
+
"checkpointRollback",
|
|
1923
|
+
undefined) as DataProcessingError;
|
|
1924
|
+
});
|
|
1925
|
+
this.closeFn(error2);
|
|
1926
|
+
throw error2;
|
|
1927
|
+
}
|
|
2033
1928
|
} else {
|
|
2034
1929
|
// pre-0.58 error message: orderSequentiallyCallbackException
|
|
2035
1930
|
this.closeFn(new GenericError("orderSequentially callback exception", error));
|
|
@@ -2043,79 +1938,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2043
1938
|
public async createDataStore(pkg: string | string[]): Promise<IDataStore> {
|
|
2044
1939
|
const internalId = uuid();
|
|
2045
1940
|
return channelToDataStore(
|
|
2046
|
-
await this._createDataStore(pkg,
|
|
1941
|
+
await this._createDataStore(pkg, internalId),
|
|
2047
1942
|
internalId,
|
|
2048
1943
|
this,
|
|
2049
1944
|
this.dataStores,
|
|
2050
1945
|
this.mc.logger);
|
|
2051
1946
|
}
|
|
2052
1947
|
|
|
2053
|
-
/**
|
|
2054
|
-
* Creates a root datastore directly with a user generated id and attaches it to storage.
|
|
2055
|
-
* It is vulnerable to name collisions and should not be used.
|
|
2056
|
-
*
|
|
2057
|
-
* This method will be removed. See #6465.
|
|
2058
|
-
*/
|
|
2059
|
-
private async createRootDataStoreLegacy(pkg: string | string[], rootDataStoreId: string): Promise<IFluidRouter> {
|
|
2060
|
-
const fluidDataStore = await this._createDataStore(pkg, true /* isRoot */, rootDataStoreId);
|
|
2061
|
-
// back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel. For
|
|
2062
|
-
// older versions, we still have to call bindToContext.
|
|
2063
|
-
if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
|
|
2064
|
-
fluidDataStore.makeVisibleAndAttachGraph();
|
|
2065
|
-
} else {
|
|
2066
|
-
fluidDataStore.bindToContext();
|
|
2067
|
-
}
|
|
2068
|
-
return fluidDataStore;
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
/**
|
|
2072
|
-
* @deprecated - will be removed in an upcoming release. See #9660.
|
|
2073
|
-
*/
|
|
2074
|
-
public async createRootDataStore(pkg: string | string[], rootDataStoreId: string): Promise<IFluidRouter> {
|
|
2075
|
-
if (rootDataStoreId.includes("/")) {
|
|
2076
|
-
throw new UsageError(`Id cannot contain slashes: '${rootDataStoreId}'`);
|
|
2077
|
-
}
|
|
2078
|
-
return this._aliasingEnabled === true ?
|
|
2079
|
-
this.createAndAliasDataStore(pkg, rootDataStoreId) :
|
|
2080
|
-
this.createRootDataStoreLegacy(pkg, rootDataStoreId);
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
/**
|
|
2084
|
-
* Creates a data store then attempts to alias it.
|
|
2085
|
-
* If aliasing fails, it will raise an exception.
|
|
2086
|
-
*
|
|
2087
|
-
* This method will be removed. See #6465.
|
|
2088
|
-
*
|
|
2089
|
-
* @param pkg - Package name of the data store
|
|
2090
|
-
* @param alias - Alias to be assigned to the data store
|
|
2091
|
-
* @param props - Properties for the data store
|
|
2092
|
-
* @returns - An aliased data store which can can be found / loaded by alias.
|
|
2093
|
-
*/
|
|
2094
|
-
private async createAndAliasDataStore(pkg: string | string[], alias: string, props?: any): Promise<IDataStore> {
|
|
2095
|
-
const internalId = uuid();
|
|
2096
|
-
const dataStore = await this._createDataStore(pkg, false /* isRoot */, internalId, props);
|
|
2097
|
-
const aliasedDataStore = channelToDataStore(dataStore, internalId, this, this.dataStores, this.mc.logger);
|
|
2098
|
-
const result = await aliasedDataStore.trySetAlias(alias);
|
|
2099
|
-
if (result !== "Success") {
|
|
2100
|
-
throw new GenericError(
|
|
2101
|
-
"dataStoreAliasFailure",
|
|
2102
|
-
undefined /* error */,
|
|
2103
|
-
{
|
|
2104
|
-
alias: {
|
|
2105
|
-
value: alias,
|
|
2106
|
-
tag: TelemetryDataTag.UserData,
|
|
2107
|
-
},
|
|
2108
|
-
internalId: {
|
|
2109
|
-
value: internalId,
|
|
2110
|
-
tag: TelemetryDataTag.PackageData,
|
|
2111
|
-
},
|
|
2112
|
-
aliasResult: result,
|
|
2113
|
-
});
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
return aliasedDataStore;
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
1948
|
public createDetachedRootDataStore(
|
|
2120
1949
|
pkg: Readonly<string[]>,
|
|
2121
1950
|
rootDataStoreId: string): IFluidDataStoreContextDetached {
|
|
@@ -2129,55 +1958,23 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2129
1958
|
return this.dataStores.createDetachedDataStoreCore(pkg, false);
|
|
2130
1959
|
}
|
|
2131
1960
|
|
|
2132
|
-
|
|
2133
|
-
* Creates a possibly root datastore directly with a possibly user generated id and attaches it to storage.
|
|
2134
|
-
* It is vulnerable to name collisions if both aforementioned conditions are true, and should not be used.
|
|
2135
|
-
*
|
|
2136
|
-
* This method will be removed. See #6465.
|
|
2137
|
-
*/
|
|
2138
|
-
private async _createDataStoreWithPropsLegacy(
|
|
1961
|
+
public async _createDataStoreWithProps(
|
|
2139
1962
|
pkg: string | string[],
|
|
2140
1963
|
props?: any,
|
|
2141
1964
|
id = uuid(),
|
|
2142
|
-
isRoot = false,
|
|
2143
1965
|
): Promise<IDataStore> {
|
|
2144
1966
|
const fluidDataStore = await this.dataStores._createFluidDataStoreContext(
|
|
2145
|
-
Array.isArray(pkg) ? pkg : [pkg], id,
|
|
2146
|
-
if (isRoot) {
|
|
2147
|
-
// back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel.
|
|
2148
|
-
// For older versions, we still have to call bindToContext.
|
|
2149
|
-
if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
|
|
2150
|
-
fluidDataStore.makeVisibleAndAttachGraph();
|
|
2151
|
-
} else {
|
|
2152
|
-
fluidDataStore.bindToContext();
|
|
2153
|
-
}
|
|
2154
|
-
this.logger.sendTelemetryEvent({
|
|
2155
|
-
eventName: "Root datastore with props",
|
|
2156
|
-
hasProps: props !== undefined,
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
1967
|
+
Array.isArray(pkg) ? pkg : [pkg], id, props).realize();
|
|
2159
1968
|
return channelToDataStore(fluidDataStore, id, this, this.dataStores, this.mc.logger);
|
|
2160
1969
|
}
|
|
2161
1970
|
|
|
2162
|
-
public async _createDataStoreWithProps(
|
|
2163
|
-
pkg: string | string[],
|
|
2164
|
-
props?: any,
|
|
2165
|
-
id = uuid(),
|
|
2166
|
-
isRoot = false,
|
|
2167
|
-
): Promise<IDataStore> {
|
|
2168
|
-
return this._aliasingEnabled === true && isRoot ?
|
|
2169
|
-
this.createAndAliasDataStore(pkg, id, props) :
|
|
2170
|
-
this._createDataStoreWithPropsLegacy(pkg, props, id, isRoot);
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
1971
|
private async _createDataStore(
|
|
2174
1972
|
pkg: string | string[],
|
|
2175
|
-
isRoot: boolean,
|
|
2176
1973
|
id = uuid(),
|
|
2177
1974
|
props?: any,
|
|
2178
1975
|
): Promise<IFluidDataStoreChannel> {
|
|
2179
1976
|
return this.dataStores
|
|
2180
|
-
._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id,
|
|
1977
|
+
._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, props)
|
|
2181
1978
|
.realize();
|
|
2182
1979
|
}
|
|
2183
1980
|
|
|
@@ -2263,7 +2060,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2263
2060
|
this.emit("attached");
|
|
2264
2061
|
}
|
|
2265
2062
|
|
|
2266
|
-
if (attachState === AttachState.Attached && !this.
|
|
2063
|
+
if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
|
|
2267
2064
|
this.updateDocumentDirtyState(false);
|
|
2268
2065
|
}
|
|
2269
2066
|
this.dataStores.setAttachState(attachState);
|
|
@@ -2283,10 +2080,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2283
2080
|
}
|
|
2284
2081
|
|
|
2285
2082
|
const summarizeResult = this.dataStores.createSummary(telemetryContext);
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
}
|
|
2083
|
+
// Wrap data store summaries in .channels subtree.
|
|
2084
|
+
wrapSummaryInChannelsTree(summarizeResult);
|
|
2085
|
+
|
|
2290
2086
|
this.addContainerStateToSummary(
|
|
2291
2087
|
summarizeResult,
|
|
2292
2088
|
true /* fullTree */,
|
|
@@ -2312,13 +2108,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2312
2108
|
telemetryContext?: ITelemetryContext,
|
|
2313
2109
|
): Promise<ISummarizeInternalResult> {
|
|
2314
2110
|
const summarizeResult = await this.dataStores.summarize(fullTree, trackState, telemetryContext);
|
|
2315
|
-
let pathPartsForChildren: string[] | undefined;
|
|
2316
2111
|
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
}
|
|
2112
|
+
// Wrap data store summaries in .channels subtree.
|
|
2113
|
+
wrapSummaryInChannelsTree(summarizeResult);
|
|
2114
|
+
const pathPartsForChildren = [channelsTreeName];
|
|
2115
|
+
|
|
2322
2116
|
this.addContainerStateToSummary(summarizeResult, fullTree, trackState, telemetryContext);
|
|
2323
2117
|
return {
|
|
2324
2118
|
...summarizeResult,
|
|
@@ -2488,7 +2282,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2488
2282
|
|
|
2489
2283
|
/**
|
|
2490
2284
|
* Runs garbage collection and updates the reference / used state of the nodes in the container.
|
|
2491
|
-
* @returns the statistics of the garbage collection run.
|
|
2285
|
+
* @returns the statistics of the garbage collection run; undefined if GC did not run.
|
|
2492
2286
|
*/
|
|
2493
2287
|
public async collectGarbage(
|
|
2494
2288
|
options: {
|
|
@@ -2499,7 +2293,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2499
2293
|
/** True to generate full GC data */
|
|
2500
2294
|
fullGC?: boolean;
|
|
2501
2295
|
},
|
|
2502
|
-
): Promise<IGCStats> {
|
|
2296
|
+
): Promise<IGCStats | undefined> {
|
|
2503
2297
|
return this.garbageCollector.collectGarbage(options);
|
|
2504
2298
|
}
|
|
2505
2299
|
|
|
@@ -2534,6 +2328,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2534
2328
|
},
|
|
2535
2329
|
);
|
|
2536
2330
|
|
|
2331
|
+
assert(this.emptyBatch, 0x3d1 /* Can't trigger summary in the middle of a batch */);
|
|
2332
|
+
|
|
2537
2333
|
let latestSnapshotVersionId: string | undefined;
|
|
2538
2334
|
if (refreshLatestAck) {
|
|
2539
2335
|
const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(
|
|
@@ -2563,15 +2359,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2563
2359
|
const summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
|
|
2564
2360
|
const minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
|
|
2565
2361
|
const message = `Summary @${summaryRefSeqNum}:${this.deltaManager.minimumSequenceNumber}`;
|
|
2566
|
-
|
|
2567
|
-
// We should be here is we haven't processed be here. If we are of if the last message's sequence number
|
|
2568
|
-
// doesn't match the last processed sequence number, log an error.
|
|
2569
|
-
if (summaryRefSeqNum !== this.deltaManager.lastMessage?.sequenceNumber) {
|
|
2570
|
-
summaryNumberLogger.sendErrorEvent({
|
|
2571
|
-
eventName: "LastSequenceMismatch",
|
|
2572
|
-
error: message,
|
|
2573
|
-
});
|
|
2574
|
-
}
|
|
2362
|
+
const lastAck = this.summaryCollection.latestAck;
|
|
2575
2363
|
|
|
2576
2364
|
this.summarizerNode.startSummary(summaryRefSeqNum, summaryNumberLogger);
|
|
2577
2365
|
|
|
@@ -2601,6 +2389,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2601
2389
|
error: `lastSequenceNumber changed before uploading to storage. ${this.deltaManager.lastSequenceNumber} !== ${summaryRefSeqNum}`,
|
|
2602
2390
|
};
|
|
2603
2391
|
}
|
|
2392
|
+
assert(summaryRefSeqNum === this.deltaManager.lastMessage?.sequenceNumber,
|
|
2393
|
+
0x395 /* it's one and the same thing */);
|
|
2394
|
+
|
|
2395
|
+
if (lastAck !== this.summaryCollection.latestAck) {
|
|
2396
|
+
return {
|
|
2397
|
+
continue: false,
|
|
2398
|
+
// eslint-disable-next-line max-len
|
|
2399
|
+
error: `Last summary changed while summarizing. ${this.summaryCollection.latestAck} !== ${lastAck}`,
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2604
2402
|
return { continue: true };
|
|
2605
2403
|
};
|
|
2606
2404
|
|
|
@@ -2621,7 +2419,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2621
2419
|
const forcedFullTree = this.garbageCollector.summaryStateNeedsReset;
|
|
2622
2420
|
try {
|
|
2623
2421
|
summarizeResult = await this.summarize({
|
|
2624
|
-
fullTree: fullTree
|
|
2422
|
+
fullTree: fullTree ?? forcedFullTree,
|
|
2625
2423
|
trackState: true,
|
|
2626
2424
|
summaryLogger: summaryNumberLogger,
|
|
2627
2425
|
runGC: this.garbageCollector.shouldRunGC,
|
|
@@ -2642,7 +2440,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2642
2440
|
// Counting dataStores and handles
|
|
2643
2441
|
// Because handles are unchanged dataStores in the current logic,
|
|
2644
2442
|
// summarized dataStore count is total dataStore count minus handle count
|
|
2645
|
-
const dataStoreTree =
|
|
2443
|
+
const dataStoreTree = summaryTree.tree[channelsTreeName];
|
|
2646
2444
|
|
|
2647
2445
|
assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
|
|
2648
2446
|
const handleCount = Object.values(dataStoreTree.tree).filter(
|
|
@@ -2657,8 +2455,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2657
2455
|
gcStateUpdatedDataStoreCount: summarizeResult.gcStats?.updatedDataStoreCount,
|
|
2658
2456
|
gcBlobNodeCount: gcSummaryTreeStats?.blobNodeCount,
|
|
2659
2457
|
gcTotalBlobsSize: gcSummaryTreeStats?.totalBlobSize,
|
|
2660
|
-
opsSizesSinceLastSummary: this.opTracker.opsSizeAccumulator,
|
|
2661
|
-
nonSystemOpsSinceLastSummary: this.opTracker.nonSystemOpCount,
|
|
2662
2458
|
summaryNumber,
|
|
2663
2459
|
...partialStats,
|
|
2664
2460
|
};
|
|
@@ -2681,7 +2477,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2681
2477
|
// submitting the summaryOp then we can't rely on summaryAck. So in case we have
|
|
2682
2478
|
// latestSnapshotVersionId from storage and it does not match with the lastAck ackHandle, then use
|
|
2683
2479
|
// the one fetched from storage as parent as that is the latest.
|
|
2684
|
-
const lastAck = this.summaryCollection.latestAck;
|
|
2685
2480
|
let summaryContext: ISummaryContext;
|
|
2686
2481
|
if (lastAck?.summaryAck.contents.handle !== latestSnapshotVersionId
|
|
2687
2482
|
&& latestSnapshotVersionId !== undefined) {
|
|
@@ -2732,7 +2527,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2732
2527
|
|
|
2733
2528
|
let clientSequenceNumber: number;
|
|
2734
2529
|
try {
|
|
2735
|
-
clientSequenceNumber = this.
|
|
2530
|
+
clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
|
|
2736
2531
|
} catch (error) {
|
|
2737
2532
|
return { stage: "upload", ...uploadData, error };
|
|
2738
2533
|
}
|
|
@@ -2745,7 +2540,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2745
2540
|
} as const;
|
|
2746
2541
|
|
|
2747
2542
|
this.summarizerNode.completeSummary(handle);
|
|
2748
|
-
this.opTracker.reset();
|
|
2749
2543
|
return submitData;
|
|
2750
2544
|
} finally {
|
|
2751
2545
|
// Cleanup wip summary in case of failure
|
|
@@ -2792,7 +2586,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2792
2586
|
}
|
|
2793
2587
|
}
|
|
2794
2588
|
|
|
2589
|
+
private hasPendingMessages() {
|
|
2590
|
+
return this.pendingStateManager.hasPendingMessages() || !this.emptyBatch;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2795
2593
|
private updateDocumentDirtyState(dirty: boolean) {
|
|
2594
|
+
if (this.attachState !== AttachState.Attached) {
|
|
2595
|
+
assert(dirty, 0x3d2 /* Non-attached container is dirty */);
|
|
2596
|
+
} else {
|
|
2597
|
+
// Other way is not true = see this.isContainerMessageDirtyable()
|
|
2598
|
+
assert(!dirty || this.hasPendingMessages(),
|
|
2599
|
+
0x3d3 /* if doc is dirty, there has to be pending ops */);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2796
2602
|
if (this.dirtyContainer === dirty) {
|
|
2797
2603
|
return;
|
|
2798
2604
|
}
|
|
@@ -2831,160 +2637,117 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2831
2637
|
|
|
2832
2638
|
private submit(
|
|
2833
2639
|
type: ContainerMessageType,
|
|
2834
|
-
|
|
2640
|
+
contents: any,
|
|
2835
2641
|
localOpMetadata: unknown = undefined,
|
|
2836
|
-
|
|
2642
|
+
metadata: Record<string, unknown> | undefined = undefined,
|
|
2837
2643
|
): void {
|
|
2838
2644
|
this.verifyNotClosed();
|
|
2839
2645
|
|
|
2840
2646
|
// There should be no ops in detached container state!
|
|
2841
2647
|
assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
|
|
2842
2648
|
|
|
2843
|
-
|
|
2844
|
-
|
|
2649
|
+
const deserializedContent: ContainerRuntimeMessage = { type, contents };
|
|
2650
|
+
const serializedContent = JSON.stringify(deserializedContent);
|
|
2845
2651
|
|
|
2846
|
-
if (this.
|
|
2847
|
-
|
|
2848
|
-
const maxOpSize = this.context.deltaManager.maxMessageSize;
|
|
2849
|
-
|
|
2850
|
-
// If in TurnBased flush mode we will trigger a flush at the next turn break
|
|
2851
|
-
if (this.flushMode === FlushMode.TurnBased && !this.needsFlush) {
|
|
2852
|
-
opMetadataInternal = {
|
|
2853
|
-
...opMetadata,
|
|
2854
|
-
batch: true,
|
|
2855
|
-
};
|
|
2856
|
-
this.needsFlush = true;
|
|
2857
|
-
|
|
2858
|
-
// Use Promise.resolve().then() to queue a microtask to detect the end of the turn and force a flush.
|
|
2859
|
-
if (!this.flushTrigger) {
|
|
2860
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
2861
|
-
Promise.resolve().then(() => {
|
|
2862
|
-
this.flushTrigger = false;
|
|
2863
|
-
this.flush();
|
|
2864
|
-
});
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
clientSequenceNumber = this.submitMaybeChunkedMessages(
|
|
2869
|
-
type,
|
|
2870
|
-
content,
|
|
2871
|
-
serializedContent,
|
|
2872
|
-
maxOpSize,
|
|
2873
|
-
this._flushMode === FlushMode.TurnBased,
|
|
2874
|
-
opMetadataInternal);
|
|
2652
|
+
if (this.deltaManager.readOnlyInfo.readonly) {
|
|
2653
|
+
this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
|
|
2875
2654
|
}
|
|
2876
2655
|
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
this.deltaManager.lastSequenceNumber,
|
|
2882
|
-
content,
|
|
2656
|
+
const message: BatchMessage = {
|
|
2657
|
+
contents: serializedContent,
|
|
2658
|
+
deserializedContent,
|
|
2659
|
+
metadata,
|
|
2883
2660
|
localOpMetadata,
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
if (this.isContainerMessageDirtyable(type, content)) {
|
|
2887
|
-
this.updateDocumentDirtyState(true);
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2661
|
+
referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
|
|
2662
|
+
};
|
|
2890
2663
|
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
//
|
|
2901
|
-
|
|
2902
|
-
|
|
2664
|
+
try {
|
|
2665
|
+
// If this is attach message for new data store, and we are in a batch, send this op out of order
|
|
2666
|
+
// Is it safe:
|
|
2667
|
+
// Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
|
|
2668
|
+
// They become visible only when aliased, or handle to some sub-element of newly created datastore
|
|
2669
|
+
// is stored in some DDS, i.e. only after some other op.
|
|
2670
|
+
// Why:
|
|
2671
|
+
// Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
|
|
2672
|
+
// stores are created, causing issues like relay service throttling (too many ops) and catastrophic
|
|
2673
|
+
// failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
|
|
2674
|
+
// these issues.
|
|
2675
|
+
// Cons:
|
|
2676
|
+
// 1. With large batches, relay service may throttle clients. Clients may disconnect while throttled.
|
|
2677
|
+
// This change creates new possibility of a lot of newly created data stores never being referenced
|
|
2678
|
+
// because client died before it had a change to submit the rest of the ops. This will create more
|
|
2679
|
+
// garbage that needs to be collected leveraging GC (Garbage Collection) feature.
|
|
2680
|
+
// 2. Sending ops out of order means they are excluded from rollback functionality. This is not an issue
|
|
2681
|
+
// today as rollback can't undo creation of data store. To some extent not sending them is a bigger
|
|
2682
|
+
// issue than sending.
|
|
2683
|
+
// Please note that this does not change file format, so it can be disabled in the future if this
|
|
2684
|
+
// optimization no longer makes sense (for example, batch compression may make it less appealing).
|
|
2685
|
+
if (type === ContainerMessageType.Attach &&
|
|
2686
|
+
this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
|
|
2687
|
+
if (!this.pendingAttachBatch.push(message)) {
|
|
2688
|
+
// BatchManager has two limits - soft limit & hard limit. Soft limit is only engaged
|
|
2689
|
+
// when queue is not empty.
|
|
2690
|
+
// Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
|
|
2691
|
+
this.flushBatch(this.pendingAttachBatch.popBatch());
|
|
2692
|
+
if (!this.pendingAttachBatch.push(message)) {
|
|
2693
|
+
throw new GenericError(
|
|
2694
|
+
"BatchTooLarge",
|
|
2695
|
+
/* error */ undefined,
|
|
2696
|
+
{
|
|
2697
|
+
opSize: message.contents.length,
|
|
2698
|
+
count: this.pendingAttachBatch.length,
|
|
2699
|
+
limit: this.pendingAttachBatch.limit,
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
} else {
|
|
2704
|
+
if (!this.pendingBatch.push(message)) {
|
|
2705
|
+
throw new GenericError(
|
|
2706
|
+
"BatchTooLarge",
|
|
2707
|
+
/* error */ undefined,
|
|
2708
|
+
{
|
|
2709
|
+
opSize: message.contents.length,
|
|
2710
|
+
count: this.pendingBatch.length,
|
|
2711
|
+
limit: this.pendingBatch.limit,
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2903
2714
|
}
|
|
2904
2715
|
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
{
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
},
|
|
2920
|
-
}));
|
|
2921
|
-
return -1;
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
// Chunking enabled, fallback on the server's max message size
|
|
2925
|
-
// and split the content accordingly
|
|
2926
|
-
if (!serializedContent || serializedContent.length <= serverMaxOpSize) {
|
|
2927
|
-
return this.submitRuntimeMessage(type, content, batch, opMetadataInternal);
|
|
2716
|
+
if (this._flushMode !== FlushMode.TurnBased) {
|
|
2717
|
+
this.flush();
|
|
2718
|
+
} else if (!this.flushTrigger) {
|
|
2719
|
+
this.flushTrigger = true;
|
|
2720
|
+
// Queue a microtask to detect the end of the turn and force a flush.
|
|
2721
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
2722
|
+
Promise.resolve().then(() => {
|
|
2723
|
+
this.flushTrigger = false;
|
|
2724
|
+
this.flush();
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
} catch (error) {
|
|
2728
|
+
this.closeFn(error as GenericError);
|
|
2729
|
+
throw error;
|
|
2928
2730
|
}
|
|
2929
2731
|
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
private submitChunkedMessage(type: ContainerMessageType, content: string, maxOpSize: number): number {
|
|
2934
|
-
const contentLength = content.length;
|
|
2935
|
-
const chunkN = Math.floor((contentLength - 1) / maxOpSize) + 1;
|
|
2936
|
-
let offset = 0;
|
|
2937
|
-
let clientSequenceNumber: number = 0;
|
|
2938
|
-
for (let i = 1; i <= chunkN; i = i + 1) {
|
|
2939
|
-
const chunkedOp: IChunkedOp = {
|
|
2940
|
-
chunkId: i,
|
|
2941
|
-
contents: content.substr(offset, maxOpSize),
|
|
2942
|
-
originalType: type,
|
|
2943
|
-
totalChunks: chunkN,
|
|
2944
|
-
};
|
|
2945
|
-
offset += maxOpSize;
|
|
2946
|
-
clientSequenceNumber = this.submitRuntimeMessage(
|
|
2947
|
-
ContainerMessageType.ChunkedOp,
|
|
2948
|
-
chunkedOp,
|
|
2949
|
-
false);
|
|
2732
|
+
if (this.isContainerMessageDirtyable(type, contents)) {
|
|
2733
|
+
this.updateDocumentDirtyState(true);
|
|
2950
2734
|
}
|
|
2951
|
-
return clientSequenceNumber;
|
|
2952
2735
|
}
|
|
2953
2736
|
|
|
2954
|
-
private
|
|
2955
|
-
type: MessageType,
|
|
2956
|
-
contents: any) {
|
|
2737
|
+
private submitSummaryMessage(contents: ISummaryContent) {
|
|
2957
2738
|
this.verifyNotClosed();
|
|
2958
2739
|
assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
|
|
2959
2740
|
|
|
2960
2741
|
// System message should not be sent in the middle of the batch.
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
this.
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
contents,
|
|
2971
|
-
middleOfBatch);
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
|
-
private submitRuntimeMessage(
|
|
2975
|
-
type: ContainerMessageType,
|
|
2976
|
-
contents: any,
|
|
2977
|
-
batch: boolean,
|
|
2978
|
-
appData?: any,
|
|
2979
|
-
) {
|
|
2980
|
-
this.verifyNotClosed();
|
|
2981
|
-
assert(this.connected, 0x259 /* "Container disconnected when trying to submit system message" */);
|
|
2982
|
-
const payload: ContainerRuntimeMessage = { type, contents };
|
|
2983
|
-
return this.context.submitFn(
|
|
2984
|
-
MessageType.Operation,
|
|
2985
|
-
payload,
|
|
2986
|
-
batch,
|
|
2987
|
-
appData);
|
|
2742
|
+
assert(this.emptyBatch, 0x3d4 /* System op in the middle of a batch */);
|
|
2743
|
+
|
|
2744
|
+
// back-compat: ADO #1385: Make this call unconditional in the future
|
|
2745
|
+
return this.context.submitSummaryFn !== undefined
|
|
2746
|
+
? this.context.submitSummaryFn(contents)
|
|
2747
|
+
: this.context.submitFn(
|
|
2748
|
+
MessageType.Summarize,
|
|
2749
|
+
contents,
|
|
2750
|
+
false);
|
|
2988
2751
|
}
|
|
2989
2752
|
|
|
2990
2753
|
/**
|
|
@@ -3022,7 +2785,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3022
2785
|
case ContainerMessageType.ChunkedOp:
|
|
3023
2786
|
throw new Error(`chunkedOp not expected here`);
|
|
3024
2787
|
case ContainerMessageType.BlobAttach:
|
|
3025
|
-
this.
|
|
2788
|
+
this.blobManager.reSubmit(opMetadata);
|
|
3026
2789
|
break;
|
|
3027
2790
|
case ContainerMessageType.Rejoin:
|
|
3028
2791
|
this.submit(type, content);
|
|
@@ -3060,25 +2823,25 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3060
2823
|
// It should only be done by the summarizerNode, if required.
|
|
3061
2824
|
const snapshotTreeFetcher = async () => {
|
|
3062
2825
|
const fetchResult = await this.fetchSnapshotFromStorage(
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
2826
|
+
ackHandle,
|
|
2827
|
+
summaryLogger,
|
|
2828
|
+
{
|
|
2829
|
+
eventName: "RefreshLatestSummaryGetSnapshot",
|
|
2830
|
+
ackHandle,
|
|
2831
|
+
summaryRefSeq,
|
|
2832
|
+
fetchLatest: false,
|
|
2833
|
+
});
|
|
2834
|
+
return fetchResult.snapshotTree;
|
|
2835
|
+
};
|
|
2836
|
+
const result = await this.summarizerNode.refreshLatestSummary(
|
|
2837
|
+
proposalHandle,
|
|
2838
|
+
summaryRefSeq,
|
|
2839
|
+
snapshotTreeFetcher,
|
|
2840
|
+
readAndParseBlob,
|
|
2841
|
+
summaryLogger,
|
|
2842
|
+
);
|
|
2843
|
+
|
|
2844
|
+
// Notify the garbage collector so it can update its latest summary state.
|
|
3082
2845
|
await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
|
|
3083
2846
|
}
|
|
3084
2847
|
|
|
@@ -3092,9 +2855,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3092
2855
|
summaryLogger: ITelemetryLogger,
|
|
3093
2856
|
): Promise<{ latestSnapshotRefSeq: number; latestSnapshotVersionId: string | undefined; }> {
|
|
3094
2857
|
const { snapshotTree, versionId } = await this.fetchSnapshotFromStorage(null, summaryLogger, {
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
2858
|
+
eventName: "RefreshLatestSummaryGetSnapshot",
|
|
2859
|
+
fetchLatest: true,
|
|
2860
|
+
},
|
|
2861
|
+
FetchSource.noCache,
|
|
3098
2862
|
);
|
|
3099
2863
|
|
|
3100
2864
|
const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
|
|
@@ -3118,6 +2882,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3118
2882
|
versionId: string | null,
|
|
3119
2883
|
logger: ITelemetryLogger,
|
|
3120
2884
|
event: ITelemetryGenericEvent,
|
|
2885
|
+
fetchSource?: FetchSource,
|
|
3121
2886
|
): Promise<{ snapshotTree: ISnapshotTree; versionId: string; }> {
|
|
3122
2887
|
return PerformanceEvent.timedExecAsync(
|
|
3123
2888
|
logger, event, async (perfEvent: {
|
|
@@ -3129,7 +2894,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3129
2894
|
const stats: { getVersionDuration?: number; getSnapshotDuration?: number; } = {};
|
|
3130
2895
|
const trace = Trace.start();
|
|
3131
2896
|
|
|
3132
|
-
const versions = await this.storage.getVersions(
|
|
2897
|
+
const versions = await this.storage.getVersions(
|
|
2898
|
+
versionId, 1, "refreshLatestSummaryAckFromServer", fetchSource);
|
|
3133
2899
|
assert(!!versions && !!versions[0], 0x137 /* "Failed to get version from storage" */);
|
|
3134
2900
|
stats.getVersionDuration = trace.trace().duration;
|
|
3135
2901
|
|
|
@@ -3157,15 +2923,21 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3157
2923
|
this.baseSnapshotBlobs = await SerializedSnapshotStorage.serializeTree(this.context.baseSnapshot, this.storage);
|
|
3158
2924
|
}
|
|
3159
2925
|
|
|
3160
|
-
public getPendingLocalState():
|
|
2926
|
+
public getPendingLocalState(): unknown {
|
|
3161
2927
|
if (!(this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad)) {
|
|
3162
2928
|
throw new UsageError("can't get state when offline load disabled");
|
|
3163
2929
|
}
|
|
3164
2930
|
|
|
2931
|
+
// Flush pending batch.
|
|
2932
|
+
// getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
|
|
2933
|
+
// to close current batch.
|
|
2934
|
+
this.flush();
|
|
2935
|
+
|
|
3165
2936
|
const previousPendingState = this.context.pendingLocalState as IPendingRuntimeState | undefined;
|
|
3166
2937
|
if (previousPendingState) {
|
|
3167
2938
|
return {
|
|
3168
2939
|
pending: this.pendingStateManager.getLocalState(),
|
|
2940
|
+
pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
|
|
3169
2941
|
snapshotBlobs: previousPendingState.snapshotBlobs,
|
|
3170
2942
|
baseSnapshot: previousPendingState.baseSnapshot,
|
|
3171
2943
|
savedOps: this.savedOps,
|
|
@@ -3175,6 +2947,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3175
2947
|
assert(!!this.baseSnapshotBlobs, 0x2e7 /* "Must serialize base snapshot blobs before getting runtime state" */);
|
|
3176
2948
|
return {
|
|
3177
2949
|
pending: this.pendingStateManager.getLocalState(),
|
|
2950
|
+
pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
|
|
3178
2951
|
snapshotBlobs: this.baseSnapshotBlobs,
|
|
3179
2952
|
baseSnapshot: this.context.baseSnapshot,
|
|
3180
2953
|
savedOps: this.savedOps,
|
|
@@ -3249,6 +3022,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3249
3022
|
// we may not have seen every sequence number (because of system ops) so apply everything once we
|
|
3250
3023
|
// don't have any more saved ops
|
|
3251
3024
|
await this.pendingStateManager.applyStashedOpsAt();
|
|
3025
|
+
|
|
3026
|
+
// If it's not the case, we should take it into account when calculating dirty state.
|
|
3027
|
+
assert(this.context.attachState === AttachState.Attached,
|
|
3028
|
+
0x3d5 /* this function is called for attached containers only */);
|
|
3029
|
+
if (!this.hasPendingMessages()) {
|
|
3030
|
+
this.updateDocumentDirtyState(false);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
private validateSummaryHeuristicConfiguration(configuration: ISummaryConfigurationHeuristics) {
|
|
3035
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
3036
|
+
for (const prop in configuration) {
|
|
3037
|
+
if (typeof configuration[prop] === "number" && configuration[prop] < 0) {
|
|
3038
|
+
throw new UsageError(`Summary heuristic configuration property "${prop}" cannot be less than 0`);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3252
3041
|
}
|
|
3253
3042
|
}
|
|
3254
3043
|
|