@fluidframework/container-runtime 1.3.0 → 2.0.0-dev.1.4.5.105745
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +8 -22
- package/.mocharc.js +12 -0
- package/dist/batchManager.d.ts +37 -0
- package/dist/batchManager.d.ts.map +1 -0
- package/dist/batchManager.js +73 -0
- package/dist/batchManager.js.map +1 -0
- package/dist/batchTracker.d.ts +1 -2
- package/dist/batchTracker.d.ts.map +1 -1
- package/dist/batchTracker.js +2 -3
- package/dist/batchTracker.js.map +1 -1
- package/dist/blobManager.d.ts +87 -25
- package/dist/blobManager.d.ts.map +1 -1
- package/dist/blobManager.js +317 -99
- package/dist/blobManager.js.map +1 -1
- package/dist/containerRuntime.d.ts +110 -125
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +360 -549
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.js +29 -24
- package/dist/dataStore.js.map +1 -1
- package/dist/dataStoreContext.d.ts +20 -14
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +49 -58
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +12 -5
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +21 -20
- package/dist/dataStores.js.map +1 -1
- package/dist/deltaScheduler.d.ts +6 -4
- package/dist/deltaScheduler.d.ts.map +1 -1
- package/dist/deltaScheduler.js +6 -4
- package/dist/deltaScheduler.js.map +1 -1
- package/dist/garbageCollection.d.ts +74 -14
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +248 -169
- package/dist/garbageCollection.js.map +1 -1
- package/dist/gcSweepReadyUsageDetection.d.ts +53 -0
- package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -0
- package/dist/gcSweepReadyUsageDetection.js +135 -0
- package/dist/gcSweepReadyUsageDetection.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/opProperties.d.ts +7 -0
- package/dist/opProperties.d.ts.map +1 -0
- package/dist/opProperties.js +20 -0
- package/dist/opProperties.js.map +1 -0
- package/dist/orderedClientElection.d.ts +28 -10
- package/dist/orderedClientElection.d.ts.map +1 -1
- package/dist/orderedClientElection.js +14 -4
- package/dist/orderedClientElection.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +0 -11
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +24 -46
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runningSummarizer.d.ts +14 -4
- package/dist/runningSummarizer.d.ts.map +1 -1
- package/dist/runningSummarizer.js +69 -27
- package/dist/runningSummarizer.js.map +1 -1
- package/dist/scheduleManager.d.ts +31 -0
- package/dist/scheduleManager.d.ts.map +1 -0
- package/dist/scheduleManager.js +243 -0
- package/dist/scheduleManager.js.map +1 -0
- package/dist/summarizer.d.ts +0 -2
- package/dist/summarizer.d.ts.map +1 -1
- package/dist/summarizer.js +1 -12
- package/dist/summarizer.js.map +1 -1
- package/dist/summarizerHeuristics.d.ts +26 -4
- package/dist/summarizerHeuristics.d.ts.map +1 -1
- package/dist/summarizerHeuristics.js +98 -18
- package/dist/summarizerHeuristics.js.map +1 -1
- package/dist/summarizerTypes.d.ts +45 -18
- package/dist/summarizerTypes.d.ts.map +1 -1
- package/dist/summarizerTypes.js +1 -1
- package/dist/summarizerTypes.js.map +1 -1
- package/dist/summaryCollection.d.ts +1 -0
- package/dist/summaryCollection.d.ts.map +1 -1
- package/dist/summaryCollection.js +31 -15
- package/dist/summaryCollection.js.map +1 -1
- package/dist/summaryFormat.d.ts +0 -5
- package/dist/summaryFormat.d.ts.map +1 -1
- package/dist/summaryFormat.js.map +1 -1
- package/dist/summaryGenerator.d.ts +1 -0
- package/dist/summaryGenerator.d.ts.map +1 -1
- package/dist/summaryGenerator.js +11 -9
- package/dist/summaryGenerator.js.map +1 -1
- package/dist/summaryManager.d.ts +2 -2
- package/dist/summaryManager.d.ts.map +1 -1
- package/dist/summaryManager.js +22 -7
- package/dist/summaryManager.js.map +1 -1
- package/lib/batchManager.d.ts +37 -0
- package/lib/batchManager.d.ts.map +1 -0
- package/lib/batchManager.js +69 -0
- package/lib/batchManager.js.map +1 -0
- package/lib/batchTracker.d.ts +1 -2
- package/lib/batchTracker.d.ts.map +1 -1
- package/lib/batchTracker.js +2 -3
- package/lib/batchTracker.js.map +1 -1
- package/lib/blobManager.d.ts +87 -25
- package/lib/blobManager.d.ts.map +1 -1
- package/lib/blobManager.js +319 -101
- package/lib/blobManager.js.map +1 -1
- package/lib/containerRuntime.d.ts +110 -125
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +366 -554
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.js +29 -24
- package/lib/dataStore.js.map +1 -1
- package/lib/dataStoreContext.d.ts +20 -14
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +46 -55
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStores.d.ts +12 -5
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +21 -20
- package/lib/dataStores.js.map +1 -1
- package/lib/deltaScheduler.d.ts +6 -4
- package/lib/deltaScheduler.d.ts.map +1 -1
- package/lib/deltaScheduler.js +6 -4
- package/lib/deltaScheduler.js.map +1 -1
- package/lib/garbageCollection.d.ts +74 -14
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +237 -159
- package/lib/garbageCollection.js.map +1 -1
- package/lib/gcSweepReadyUsageDetection.d.ts +53 -0
- package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -0
- package/lib/gcSweepReadyUsageDetection.js +130 -0
- package/lib/gcSweepReadyUsageDetection.js.map +1 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/opProperties.d.ts +7 -0
- package/lib/opProperties.d.ts.map +1 -0
- package/lib/opProperties.js +16 -0
- package/lib/opProperties.js.map +1 -0
- package/lib/orderedClientElection.d.ts +28 -10
- package/lib/orderedClientElection.d.ts.map +1 -1
- package/lib/orderedClientElection.js +14 -4
- package/lib/orderedClientElection.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +0 -11
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +24 -46
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runningSummarizer.d.ts +14 -4
- package/lib/runningSummarizer.d.ts.map +1 -1
- package/lib/runningSummarizer.js +69 -27
- package/lib/runningSummarizer.js.map +1 -1
- package/lib/scheduleManager.d.ts +31 -0
- package/lib/scheduleManager.d.ts.map +1 -0
- package/lib/scheduleManager.js +239 -0
- package/lib/scheduleManager.js.map +1 -0
- package/lib/summarizer.d.ts +0 -2
- package/lib/summarizer.d.ts.map +1 -1
- package/lib/summarizer.js +1 -12
- package/lib/summarizer.js.map +1 -1
- package/lib/summarizerHeuristics.d.ts +26 -4
- package/lib/summarizerHeuristics.d.ts.map +1 -1
- package/lib/summarizerHeuristics.js +98 -18
- package/lib/summarizerHeuristics.js.map +1 -1
- package/lib/summarizerTypes.d.ts +45 -18
- package/lib/summarizerTypes.d.ts.map +1 -1
- package/lib/summarizerTypes.js +1 -1
- package/lib/summarizerTypes.js.map +1 -1
- package/lib/summaryCollection.d.ts +1 -0
- package/lib/summaryCollection.d.ts.map +1 -1
- package/lib/summaryCollection.js +31 -15
- package/lib/summaryCollection.js.map +1 -1
- package/lib/summaryFormat.d.ts +0 -5
- package/lib/summaryFormat.d.ts.map +1 -1
- package/lib/summaryFormat.js.map +1 -1
- package/lib/summaryGenerator.d.ts +1 -0
- package/lib/summaryGenerator.d.ts.map +1 -1
- package/lib/summaryGenerator.js +11 -9
- package/lib/summaryGenerator.js.map +1 -1
- package/lib/summaryManager.d.ts +2 -2
- package/lib/summaryManager.d.ts.map +1 -1
- package/lib/summaryManager.js +22 -7
- package/lib/summaryManager.js.map +1 -1
- package/package.json +68 -25
- package/src/batchManager.ts +91 -0
- package/src/batchTracker.ts +2 -3
- package/src/blobManager.ts +385 -118
- package/src/containerRuntime.ts +523 -732
- package/src/dataStore.ts +49 -37
- package/src/dataStoreContext.ts +44 -56
- package/src/dataStores.ts +34 -30
- package/src/deltaScheduler.ts +6 -4
- package/src/garbageCollection.ts +296 -205
- package/src/gcSweepReadyUsageDetection.ts +147 -0
- package/src/index.ts +1 -2
- package/src/opProperties.ts +19 -0
- package/src/orderedClientElection.ts +31 -10
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +27 -59
- package/src/runningSummarizer.ts +76 -23
- package/src/scheduleManager.ts +314 -0
- package/src/summarizer.ts +1 -18
- package/src/summarizerHeuristics.ts +136 -19
- package/src/summarizerTypes.ts +53 -18
- package/src/summaryCollection.ts +33 -18
- package/src/summaryFormat.ts +0 -6
- package/src/summaryGenerator.ts +40 -22
- package/src/summaryManager.ts +22 -7
- package/dist/opTelemetry.d.ts +0 -22
- package/dist/opTelemetry.d.ts.map +0 -1
- package/dist/opTelemetry.js +0 -59
- package/dist/opTelemetry.js.map +0 -1
- package/lib/opTelemetry.d.ts +0 -22
- package/lib/opTelemetry.d.ts.map +0 -1
- package/lib/opTelemetry.js +0 -55
- package/lib/opTelemetry.js.map +0 -1
- package/src/opTelemetry.ts +0 -71
package/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
|
-
idleTime
|
|
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,55 @@ export type ISummaryConfiguration =
|
|
|
260
297
|
export const DefaultSummaryConfiguration: ISummaryConfiguration = {
|
|
261
298
|
state: "enabled",
|
|
262
299
|
|
|
263
|
-
|
|
300
|
+
minIdleTime: 0,
|
|
264
301
|
|
|
265
|
-
|
|
302
|
+
maxIdleTime: 30 * 1000, // 30 secs.
|
|
266
303
|
|
|
267
|
-
|
|
304
|
+
maxTime: 60 * 1000, // 1 min.
|
|
305
|
+
|
|
306
|
+
maxOps: 100, // Summarize if 100 weighted ops received since last snapshot.
|
|
268
307
|
|
|
269
308
|
minOpsForLastSummaryAttempt: 10,
|
|
270
309
|
|
|
271
|
-
maxAckWaitTime:
|
|
310
|
+
maxAckWaitTime: 10 * 60 * 1000, // 10 mins.
|
|
272
311
|
|
|
273
312
|
maxOpsSinceLastSummary: 7000,
|
|
274
313
|
|
|
275
|
-
initialSummarizerDelayMs:
|
|
314
|
+
initialSummarizerDelayMs: 5 * 1000, // 5 secs.
|
|
276
315
|
|
|
277
316
|
summarizerClientElection: false,
|
|
317
|
+
|
|
318
|
+
nonRuntimeOpWeight: 0.1,
|
|
319
|
+
|
|
320
|
+
runtimeOpWeight: 1.0,
|
|
278
321
|
};
|
|
279
322
|
|
|
280
323
|
export interface IGCRuntimeOptions {
|
|
281
324
|
/**
|
|
282
|
-
* Flag that if true, will enable running garbage collection (GC)
|
|
283
|
-
*
|
|
284
|
-
* mark phase.
|
|
325
|
+
* Flag that if true, will enable running garbage collection (GC) for a new container.
|
|
326
|
+
*
|
|
327
|
+
* GC has mark phase and sweep phase. In mark phase, unreferenced objects are identified
|
|
328
|
+
* and marked as such in the summary. This option enables the mark phase.
|
|
285
329
|
* In sweep phase, unreferenced objects are eventually deleted from the container if they meet certain conditions.
|
|
286
330
|
* Sweep phase can be enabled via the "sweepAllowed" option.
|
|
287
|
-
*
|
|
331
|
+
*
|
|
332
|
+
* Note: This setting is persisted in the container's summary and cannot be changed.
|
|
288
333
|
*/
|
|
289
334
|
gcAllowed?: boolean;
|
|
290
335
|
|
|
291
336
|
/**
|
|
292
|
-
* Flag that if true, enables GC's sweep phase
|
|
337
|
+
* Flag that if true, enables GC's sweep phase for a new container.
|
|
338
|
+
*
|
|
339
|
+
* This will allow GC to eventually delete unreferenced objects from the container.
|
|
293
340
|
* This flag should only be set to true if "gcAllowed" is true.
|
|
294
|
-
*
|
|
341
|
+
*
|
|
342
|
+
* Note: This setting is persisted in the container's summary and cannot be changed.
|
|
295
343
|
*/
|
|
296
344
|
sweepAllowed?: boolean;
|
|
297
345
|
|
|
298
346
|
/**
|
|
299
|
-
* Flag that will disable garbage collection
|
|
300
|
-
* is allowed via the gcAllowed option.
|
|
347
|
+
* Flag that if true, will disable garbage collection for the session.
|
|
348
|
+
* Can be used to disable running GC on containers where it is allowed via the gcAllowed option.
|
|
301
349
|
*/
|
|
302
350
|
disableGC?: boolean;
|
|
303
351
|
|
|
@@ -307,6 +355,13 @@ export interface IGCRuntimeOptions {
|
|
|
307
355
|
*/
|
|
308
356
|
runFullGC?: boolean;
|
|
309
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Maximum session duration for a new container. If not present, a default value will be used.
|
|
360
|
+
*
|
|
361
|
+
* Note: This setting is persisted in the container's summary and cannot be changed.
|
|
362
|
+
*/
|
|
363
|
+
sessionExpiryTimeoutMs?: number;
|
|
364
|
+
|
|
310
365
|
/**
|
|
311
366
|
* Allows additional GC options to be passed.
|
|
312
367
|
*/
|
|
@@ -318,39 +373,46 @@ export interface ISummaryRuntimeOptions {
|
|
|
318
373
|
/** Override summary configurations set by the server. */
|
|
319
374
|
summaryConfigOverrides?: ISummaryConfiguration;
|
|
320
375
|
|
|
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
376
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
|
|
377
|
+
* Delay before first attempt to spawn summarizing container.
|
|
378
|
+
*
|
|
379
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
380
|
+
* {@link ISummaryBaseConfiguration.initialSummarizerDelayMs} instead.
|
|
381
|
+
*/
|
|
330
382
|
initialSummarizerDelayMs?: number;
|
|
331
383
|
|
|
332
384
|
/**
|
|
333
|
-
* @deprecated - use `summaryConfigOverrides.disableSummaries` instead.
|
|
334
385
|
* Flag that disables summaries if it is set to true.
|
|
386
|
+
*
|
|
387
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
388
|
+
* {@link ISummaryConfigurationDisableSummarizer.state} instead.
|
|
335
389
|
*/
|
|
336
390
|
disableSummaries?: boolean;
|
|
337
391
|
|
|
338
392
|
/**
|
|
339
|
-
* @
|
|
340
|
-
*
|
|
393
|
+
* @defaultValue 7000 operations (ops)
|
|
394
|
+
*
|
|
395
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
396
|
+
* {@link ISummaryBaseConfiguration.maxOpsSinceLastSummary} instead.
|
|
341
397
|
*/
|
|
342
398
|
maxOpsSinceLastSummary?: number;
|
|
343
399
|
|
|
344
400
|
/**
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
401
|
+
* Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
|
|
402
|
+
*
|
|
403
|
+
* @defaultValue `false` (disabled) and must be explicitly set to true to enable.
|
|
404
|
+
*
|
|
405
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
406
|
+
* {@link ISummaryBaseConfiguration.summarizerClientElection} instead.
|
|
407
|
+
*/
|
|
349
408
|
summarizerClientElection?: boolean;
|
|
350
409
|
|
|
351
410
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
411
|
+
* Options that control the running summarizer behavior.
|
|
412
|
+
*
|
|
413
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
414
|
+
* `{@link ISummaryConfiguration.state} = "DisableHeuristics"` instead.
|
|
415
|
+
* */
|
|
354
416
|
summarizerOptions?: Readonly<Partial<ISummarizerOptions>>;
|
|
355
417
|
}
|
|
356
418
|
|
|
@@ -369,12 +431,6 @@ export interface IContainerRuntimeOptions {
|
|
|
369
431
|
* 3. "bypass" will skip the check entirely. This is not recommended.
|
|
370
432
|
*/
|
|
371
433
|
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
434
|
/**
|
|
379
435
|
* Sets the flush mode for the runtime. In Immediate flush mode the runtime will immediately
|
|
380
436
|
* send all operations to the driver layer, while in TurnBased the operations will be buffered
|
|
@@ -388,10 +444,6 @@ export interface IContainerRuntimeOptions {
|
|
|
388
444
|
readonly enableOfflineLoad?: boolean;
|
|
389
445
|
}
|
|
390
446
|
|
|
391
|
-
type IRuntimeMessageMetadata = undefined | {
|
|
392
|
-
batch?: boolean;
|
|
393
|
-
};
|
|
394
|
-
|
|
395
447
|
/**
|
|
396
448
|
* The summary tree returned by the root node. It adds state relevant to the root of the tree.
|
|
397
449
|
*/
|
|
@@ -431,11 +483,15 @@ interface OldContainerContextWithLogger extends Omit<IContainerContext, "taggedL
|
|
|
431
483
|
* instantiated runtime in a new instance of the container, so it can load to the
|
|
432
484
|
* same state
|
|
433
485
|
*/
|
|
434
|
-
|
|
486
|
+
interface IPendingRuntimeState {
|
|
435
487
|
/**
|
|
436
488
|
* Pending ops from PendingStateManager
|
|
437
489
|
*/
|
|
438
490
|
pending?: IPendingLocalState;
|
|
491
|
+
/**
|
|
492
|
+
* Pending blobs from BlobManager
|
|
493
|
+
*/
|
|
494
|
+
pendingAttachmentBlobs?: IPendingBlobs;
|
|
439
495
|
/**
|
|
440
496
|
* A base snapshot at a sequence number prior to the first pending op
|
|
441
497
|
*/
|
|
@@ -453,26 +509,13 @@ export interface IPendingRuntimeState {
|
|
|
453
509
|
savedOps: ISequencedDocumentMessage[];
|
|
454
510
|
}
|
|
455
511
|
|
|
456
|
-
const useDataStoreAliasingKey = "Fluid.ContainerRuntime.UseDataStoreAliasing";
|
|
457
512
|
const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
|
|
458
513
|
|
|
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
514
|
const defaultFlushMode = FlushMode.TurnBased;
|
|
475
515
|
|
|
516
|
+
/**
|
|
517
|
+
* @deprecated - use ContainerRuntimeMessage instead
|
|
518
|
+
*/
|
|
476
519
|
export enum RuntimeMessage {
|
|
477
520
|
FluidDataStoreOp = "component",
|
|
478
521
|
Attach = "attach",
|
|
@@ -483,6 +526,9 @@ export enum RuntimeMessage {
|
|
|
483
526
|
Operation = "op",
|
|
484
527
|
}
|
|
485
528
|
|
|
529
|
+
/**
|
|
530
|
+
* @deprecated - please use version in driver-utils
|
|
531
|
+
*/
|
|
486
532
|
export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
|
|
487
533
|
if ((Object.values(RuntimeMessage) as string[]).includes(message.type)) {
|
|
488
534
|
return true;
|
|
@@ -490,6 +536,15 @@ export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
|
|
|
490
536
|
return false;
|
|
491
537
|
}
|
|
492
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Unpacks runtime messages
|
|
541
|
+
*
|
|
542
|
+
* @remarks This API makes no promises regarding backward-compatability. This is internal API.
|
|
543
|
+
* @param message - message (as it observed in storage / service)
|
|
544
|
+
* @returns unpacked runtime message
|
|
545
|
+
*
|
|
546
|
+
* @internal
|
|
547
|
+
*/
|
|
493
548
|
export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
|
|
494
549
|
if (message.type === MessageType.Operation) {
|
|
495
550
|
// legacy op format?
|
|
@@ -502,287 +557,13 @@ export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
|
|
|
502
557
|
message.type = innerContents.type;
|
|
503
558
|
message.contents = innerContents.contents;
|
|
504
559
|
}
|
|
505
|
-
|
|
560
|
+
return true;
|
|
506
561
|
} else {
|
|
507
562
|
// Legacy format, but it's already "unpacked",
|
|
508
563
|
// i.e. message.type is actually ContainerMessageType.
|
|
564
|
+
// Or it's non-runtime message.
|
|
509
565
|
// 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
|
-
}
|
|
566
|
+
return false;
|
|
786
567
|
}
|
|
787
568
|
}
|
|
788
569
|
|
|
@@ -852,7 +633,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
852
633
|
summaryOptions = {},
|
|
853
634
|
gcOptions = {},
|
|
854
635
|
loadSequenceNumberVerification = "close",
|
|
855
|
-
useDataStoreAliasing = false,
|
|
856
636
|
flushMode = defaultFlushMode,
|
|
857
637
|
enableOfflineLoad = false,
|
|
858
638
|
} = runtimeOptions;
|
|
@@ -928,7 +708,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
928
708
|
summaryOptions,
|
|
929
709
|
gcOptions,
|
|
930
710
|
loadSequenceNumberVerification,
|
|
931
|
-
useDataStoreAliasing,
|
|
932
711
|
flushMode,
|
|
933
712
|
enableOfflineLoad,
|
|
934
713
|
},
|
|
@@ -1018,15 +797,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1018
797
|
private readonly summaryCollection: SummaryCollection;
|
|
1019
798
|
|
|
1020
799
|
private readonly summarizerNode: IRootSummarizerNodeWithGC;
|
|
1021
|
-
private readonly _aliasingEnabled: boolean;
|
|
1022
|
-
private readonly _maxOpSizeInBytes: number;
|
|
1023
800
|
|
|
1024
801
|
private readonly maxConsecutiveReconnects: number;
|
|
1025
|
-
private readonly defaultMaxConsecutiveReconnects =
|
|
802
|
+
private readonly defaultMaxConsecutiveReconnects = 7;
|
|
1026
803
|
|
|
1027
804
|
private _orderSequentiallyCalls: number = 0;
|
|
1028
805
|
private _flushMode: FlushMode;
|
|
1029
|
-
private needsFlush = false;
|
|
1030
806
|
private flushTrigger = false;
|
|
1031
807
|
|
|
1032
808
|
private _connected: boolean;
|
|
@@ -1036,6 +812,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1036
812
|
|
|
1037
813
|
private consecutiveReconnects = 0;
|
|
1038
814
|
|
|
815
|
+
/**
|
|
816
|
+
* Used to delay transition to "connected" state while we upload
|
|
817
|
+
* attachment blobs that were added while disconnected
|
|
818
|
+
*/
|
|
819
|
+
private delayConnectClientId?: string;
|
|
820
|
+
|
|
1039
821
|
public get connected(): boolean {
|
|
1040
822
|
return this._connected;
|
|
1041
823
|
}
|
|
@@ -1069,6 +851,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1069
851
|
private readonly scheduleManager: ScheduleManager;
|
|
1070
852
|
private readonly blobManager: BlobManager;
|
|
1071
853
|
private readonly pendingStateManager: PendingStateManager;
|
|
854
|
+
|
|
855
|
+
// Provide lower soft limit - we want to have some number of ops to get efficiency in compression & bandwidth usage,
|
|
856
|
+
// but at the same time we want to send these ops sooner, to reduce overall latency of processing a batch.
|
|
857
|
+
// So there is some ballance here, that depends on compression algorithm and its efficiency working with smaller
|
|
858
|
+
// payloads. That number represents final (compressed) bits (once compression is implemented).
|
|
859
|
+
private readonly pendingAttachBatch = new BatchManager(64 * 1024);
|
|
860
|
+
private readonly pendingBatch = new BatchManager();
|
|
861
|
+
|
|
1072
862
|
private readonly garbageCollector: IGarbageCollector;
|
|
1073
863
|
|
|
1074
864
|
// Local copy of incomplete received chunks.
|
|
@@ -1076,15 +866,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1076
866
|
|
|
1077
867
|
private readonly dataStores: DataStores;
|
|
1078
868
|
|
|
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
869
|
/** The last message processed at the time of the last summary. */
|
|
1086
870
|
private messageAtLastSummary: ISummaryMetadataMessage | undefined;
|
|
1087
871
|
|
|
872
|
+
private get emptyBatch() {
|
|
873
|
+
return this.pendingBatch.empty && this.pendingAttachBatch.empty;
|
|
874
|
+
}
|
|
875
|
+
|
|
1088
876
|
private get summarizer(): Summarizer {
|
|
1089
877
|
assert(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
|
|
1090
878
|
return this._summarizer;
|
|
@@ -1120,11 +908,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1120
908
|
if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
|
|
1121
909
|
return true;
|
|
1122
910
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
return false;
|
|
1127
|
-
}
|
|
911
|
+
return this.summaryConfiguration.state !== "disabled"
|
|
912
|
+
? this.summaryConfiguration.summarizerClientElection === true
|
|
913
|
+
: false;
|
|
1128
914
|
}
|
|
1129
915
|
private readonly maxOpsSinceLastSummary: number;
|
|
1130
916
|
private getMaxOpsSinceLastSummary(): number {
|
|
@@ -1133,11 +919,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1133
919
|
if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
|
|
1134
920
|
return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
|
|
1135
921
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
return 0;
|
|
1140
|
-
}
|
|
922
|
+
return this.summaryConfiguration.state !== "disabled"
|
|
923
|
+
? this.summaryConfiguration.maxOpsSinceLastSummary
|
|
924
|
+
: 0;
|
|
1141
925
|
}
|
|
1142
926
|
|
|
1143
927
|
private readonly initialSummarizerDelayMs: number;
|
|
@@ -1147,11 +931,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1147
931
|
if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
|
|
1148
932
|
return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
|
|
1149
933
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
return 0;
|
|
1154
|
-
}
|
|
934
|
+
return this.summaryConfiguration.state !== "disabled"
|
|
935
|
+
? this.summaryConfiguration.initialSummarizerDelayMs
|
|
936
|
+
: 0;
|
|
1155
937
|
}
|
|
1156
938
|
|
|
1157
939
|
private readonly createContainerMetadata: ICreateContainerMetadata;
|
|
@@ -1160,7 +942,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1160
942
|
* a summary is generated.
|
|
1161
943
|
*/
|
|
1162
944
|
private nextSummaryNumber: number;
|
|
1163
|
-
private readonly opTracker: OpTracker;
|
|
1164
945
|
|
|
1165
946
|
private constructor(
|
|
1166
947
|
private readonly context: IContainerContext,
|
|
@@ -1186,9 +967,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1186
967
|
super();
|
|
1187
968
|
this.messageAtLastSummary = metadata?.message;
|
|
1188
969
|
|
|
1189
|
-
// Default to false (enabled).
|
|
1190
|
-
this.disableIsolatedChannels = this.runtimeOptions.summaryOptions.disableIsolatedChannels ?? false;
|
|
1191
|
-
|
|
1192
970
|
this._connected = this.context.connected;
|
|
1193
971
|
this.chunkMap = new Map<string, string[]>(chunks);
|
|
1194
972
|
|
|
@@ -1197,17 +975,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1197
975
|
this.mc = loggerToMonitoringContext(
|
|
1198
976
|
ChildLogger.create(this.logger, "ContainerRuntime"));
|
|
1199
977
|
|
|
978
|
+
if (this.summaryConfiguration.state === "enabled") {
|
|
979
|
+
this.validateSummaryHeuristicConfiguration(this.summaryConfiguration);
|
|
980
|
+
}
|
|
981
|
+
|
|
1200
982
|
this.summariesDisabled = this.isSummariesDisabled();
|
|
1201
983
|
this.heuristicsDisabled = this.isHeuristicsDisabled();
|
|
1202
984
|
this.summarizerClientElectionEnabled = this.isSummarizerClientElectionEnabled();
|
|
1203
985
|
this.maxOpsSinceLastSummary = this.getMaxOpsSinceLastSummary();
|
|
1204
986
|
this.initialSummarizerDelayMs = this.getInitialSummarizerDelayMs();
|
|
1205
987
|
|
|
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
988
|
this.maxConsecutiveReconnects =
|
|
1212
989
|
this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? this.defaultMaxConsecutiveReconnects;
|
|
1213
990
|
|
|
@@ -1227,6 +1004,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1227
1004
|
getNodePackagePath: async (nodePath: string) => this.getGCNodePackagePath(nodePath),
|
|
1228
1005
|
getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
|
|
1229
1006
|
readAndParseBlob: async <T>(id: string) => readAndParse<T>(this.storage, id),
|
|
1007
|
+
getContainerDiagnosticId: () => this.context.id,
|
|
1008
|
+
activeConnection: () => this.deltaManager.active,
|
|
1230
1009
|
});
|
|
1231
1010
|
|
|
1232
1011
|
const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
|
|
@@ -1288,15 +1067,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1288
1067
|
this.handleContext,
|
|
1289
1068
|
blobManagerSnapshot,
|
|
1290
1069
|
() => this.storage,
|
|
1291
|
-
(blobId
|
|
1070
|
+
(blobId, localId) => {
|
|
1071
|
+
if (!this.disposed) {
|
|
1072
|
+
this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId, localId });
|
|
1073
|
+
}
|
|
1074
|
+
},
|
|
1292
1075
|
(blobPath: string) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"),
|
|
1293
1076
|
this,
|
|
1294
|
-
|
|
1077
|
+
pendingRuntimeState?.pendingAttachmentBlobs,
|
|
1295
1078
|
);
|
|
1296
1079
|
|
|
1297
1080
|
this.scheduleManager = new ScheduleManager(
|
|
1298
1081
|
context.deltaManager,
|
|
1299
1082
|
this,
|
|
1083
|
+
() => this.clientId,
|
|
1300
1084
|
ChildLogger.create(this.logger, "ScheduleManager"),
|
|
1301
1085
|
);
|
|
1302
1086
|
|
|
@@ -1311,7 +1095,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1311
1095
|
flush: this.flush.bind(this),
|
|
1312
1096
|
flushMode: () => this.flushMode,
|
|
1313
1097
|
reSubmit: this.reSubmit.bind(this),
|
|
1314
|
-
rollback: this.rollback.bind(this),
|
|
1315
1098
|
setFlushMode: (mode) => this.setFlushMode(mode),
|
|
1316
1099
|
},
|
|
1317
1100
|
this._flushMode,
|
|
@@ -1442,9 +1225,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1442
1225
|
createContainerRuntimeVersion: metadata?.createContainerRuntimeVersion,
|
|
1443
1226
|
createContainerTimestamp: metadata?.createContainerTimestamp,
|
|
1444
1227
|
};
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1447
|
-
loadSummaryNumber = metadata?.summaryNumber ??
|
|
1228
|
+
// summaryNumber was renamed from summaryCount. For older docs that haven't been opened for a long time,
|
|
1229
|
+
// the count is reset to 0.
|
|
1230
|
+
loadSummaryNumber = metadata?.summaryNumber ?? 0;
|
|
1448
1231
|
} else {
|
|
1449
1232
|
this.createContainerMetadata = {
|
|
1450
1233
|
createContainerRuntimeVersion: pkgVersion,
|
|
@@ -1466,7 +1249,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1466
1249
|
|
|
1467
1250
|
ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
|
|
1468
1251
|
BindBatchTracker(this, this.logger);
|
|
1469
|
-
this.opTracker = new OpTracker(this.deltaManager, this.mc.config.getBoolean(disableOpTrackingKey) === true);
|
|
1470
1252
|
}
|
|
1471
1253
|
|
|
1472
1254
|
public dispose(error?: Error): void {
|
|
@@ -1546,16 +1328,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1546
1328
|
}
|
|
1547
1329
|
|
|
1548
1330
|
if (id === BlobManager.basePath && requestParser.isLeaf(2)) {
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
|
|
1331
|
+
const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
|
|
1332
|
+
return blob
|
|
1333
|
+
? {
|
|
1552
1334
|
status: 200,
|
|
1553
1335
|
mimeType: "fluid/object",
|
|
1554
|
-
value:
|
|
1555
|
-
};
|
|
1556
|
-
} else {
|
|
1557
|
-
return create404Response(request);
|
|
1558
|
-
}
|
|
1336
|
+
value: blob,
|
|
1337
|
+
} : create404Response(request);
|
|
1559
1338
|
} else if (requestParser.pathParts.length > 0) {
|
|
1560
1339
|
const dataStore = await this.getDataStoreFromRequest(id, request);
|
|
1561
1340
|
const subRequest = requestParser.createSubRequest(1);
|
|
@@ -1573,7 +1352,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1573
1352
|
}
|
|
1574
1353
|
|
|
1575
1354
|
private internalId(maybeAlias: string): string {
|
|
1576
|
-
return this.dataStores.aliases
|
|
1355
|
+
return this.dataStores.aliases.get(maybeAlias) ?? maybeAlias;
|
|
1577
1356
|
}
|
|
1578
1357
|
|
|
1579
1358
|
private async getDataStoreFromRequest(id: string, request: IRequest): Promise<IFluidRouter> {
|
|
@@ -1581,6 +1360,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1581
1360
|
? request.headers?.[RuntimeHeaders.wait]
|
|
1582
1361
|
: true;
|
|
1583
1362
|
|
|
1363
|
+
await this.dataStores.waitIfPendingAlias(id);
|
|
1584
1364
|
const internalId = this.internalId(id);
|
|
1585
1365
|
const dataStoreContext = await this.dataStores.getDataStore(internalId, wait);
|
|
1586
1366
|
|
|
@@ -1620,12 +1400,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1620
1400
|
private addMetadataToSummary(summaryTree: ISummaryTreeWithStats) {
|
|
1621
1401
|
const metadata: IContainerRuntimeMetadata = {
|
|
1622
1402
|
...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
1403
|
// Increment the summary number for the next summary that will be generated.
|
|
1626
1404
|
summaryNumber: this.nextSummaryNumber++,
|
|
1627
1405
|
summaryFormatVersion: 1,
|
|
1628
|
-
disableIsolatedChannels: this.disableIsolatedChannels || undefined,
|
|
1629
1406
|
...this.garbageCollector.getMetadata(),
|
|
1630
1407
|
// The last message processed at the time of summary. If there are no new messages, use the message from the
|
|
1631
1408
|
// last summary.
|
|
@@ -1647,7 +1424,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1647
1424
|
addBlobToSummary(summaryTree, chunksBlobName, content);
|
|
1648
1425
|
}
|
|
1649
1426
|
|
|
1650
|
-
const dataStoreAliases = this.dataStores.aliases
|
|
1427
|
+
const dataStoreAliases = this.dataStores.aliases;
|
|
1651
1428
|
if (dataStoreAliases.size > 0) {
|
|
1652
1429
|
addBlobToSummary(summaryTree, aliasBlobName, JSON.stringify([...dataStoreAliases]));
|
|
1653
1430
|
}
|
|
@@ -1683,7 +1460,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1683
1460
|
return true;
|
|
1684
1461
|
}
|
|
1685
1462
|
|
|
1686
|
-
if (!this.
|
|
1463
|
+
if (!this.hasPendingMessages()) {
|
|
1687
1464
|
// If there are no pending messages, we can always reconnect
|
|
1688
1465
|
this.resetReconnectCount();
|
|
1689
1466
|
return true;
|
|
@@ -1757,34 +1534,72 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1757
1534
|
}
|
|
1758
1535
|
|
|
1759
1536
|
public setConnectionState(connected: boolean, clientId?: string) {
|
|
1537
|
+
if (connected === false && this.delayConnectClientId !== undefined) {
|
|
1538
|
+
this.delayConnectClientId = undefined;
|
|
1539
|
+
this.mc.logger.sendTelemetryEvent({
|
|
1540
|
+
eventName: "UnsuccessfulConnectedTransition",
|
|
1541
|
+
});
|
|
1542
|
+
// Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// If attachment blobs were added while disconnected, we need to delay
|
|
1547
|
+
// propagation of the "connected" event until we have uploaded them to
|
|
1548
|
+
// ensure we don't submit ops referencing a blob that has not been uploaded
|
|
1549
|
+
const connecting = connected && !this._connected && !this.deltaManager.readOnlyInfo.readonly;
|
|
1550
|
+
if (connecting && this.blobManager.hasPendingOfflineUploads) {
|
|
1551
|
+
assert(!this.delayConnectClientId,
|
|
1552
|
+
0x392 /* Connect event delay must be canceled before subsequent connect event */);
|
|
1553
|
+
assert(!!clientId, 0x393 /* Must have clientId when connecting */);
|
|
1554
|
+
this.delayConnectClientId = clientId;
|
|
1555
|
+
this.blobManager.onConnected().then(() => {
|
|
1556
|
+
// make sure we didn't reconnect before the promise resolved
|
|
1557
|
+
if (this.delayConnectClientId === clientId && !this.disposed) {
|
|
1558
|
+
this.delayConnectClientId = undefined;
|
|
1559
|
+
this.setConnectionStateCore(connected, clientId);
|
|
1560
|
+
}
|
|
1561
|
+
}, (error) => this.closeFn(error));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
this.setConnectionStateCore(connected, clientId);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
private setConnectionStateCore(connected: boolean, clientId?: string) {
|
|
1569
|
+
assert(!this.delayConnectClientId,
|
|
1570
|
+
0x394 /* connect event delay must be cleared before propagating connect event */);
|
|
1760
1571
|
this.verifyNotClosed();
|
|
1761
1572
|
|
|
1762
1573
|
// There might be no change of state due to Container calling this API after loading runtime.
|
|
1763
1574
|
const changeOfState = this._connected !== connected;
|
|
1764
|
-
const reconnection = changeOfState && connected;
|
|
1575
|
+
const reconnection = changeOfState && !connected;
|
|
1765
1576
|
this._connected = connected;
|
|
1766
1577
|
|
|
1767
1578
|
if (!connected) {
|
|
1768
1579
|
this._perfSignalData.signalsLost = 0;
|
|
1769
1580
|
this._perfSignalData.signalTimestamp = 0;
|
|
1770
1581
|
this._perfSignalData.trackingSignalSequenceNumber = undefined;
|
|
1582
|
+
} else {
|
|
1583
|
+
assert(this.attachState === AttachState.Attached,
|
|
1584
|
+
0x3cd /* Connection is possible only if container exists in storage */);
|
|
1771
1585
|
}
|
|
1772
1586
|
|
|
1587
|
+
// Fail while disconnected
|
|
1773
1588
|
if (reconnection) {
|
|
1774
1589
|
this.consecutiveReconnects++;
|
|
1775
1590
|
|
|
1776
1591
|
if (!this.shouldContinueReconnecting()) {
|
|
1777
1592
|
this.closeFn(
|
|
1778
|
-
// pre-0.58 error message: MaxReconnectsWithNoProgress
|
|
1779
1593
|
DataProcessingError.create(
|
|
1780
|
-
|
|
1594
|
+
// eslint-disable-next-line max-len
|
|
1595
|
+
"Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)",
|
|
1781
1596
|
"setConnectionState",
|
|
1782
1597
|
undefined,
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1598
|
+
{
|
|
1599
|
+
dataLoss: 1,
|
|
1600
|
+
attempts: this.consecutiveReconnects,
|
|
1601
|
+
pendingMessages: this.pendingStateManager.pendingMessagesCount,
|
|
1602
|
+
}));
|
|
1788
1603
|
return;
|
|
1789
1604
|
}
|
|
1790
1605
|
}
|
|
@@ -1794,6 +1609,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1794
1609
|
}
|
|
1795
1610
|
|
|
1796
1611
|
this.dataStores.setConnectionState(connected, clientId);
|
|
1612
|
+
this.garbageCollector.setConnectionState(connected, clientId);
|
|
1797
1613
|
|
|
1798
1614
|
raiseConnectedEvent(this.mc.logger, this, connected, clientId);
|
|
1799
1615
|
}
|
|
@@ -1801,49 +1617,50 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1801
1617
|
public process(messageArg: ISequencedDocumentMessage, local: boolean) {
|
|
1802
1618
|
this.verifyNotClosed();
|
|
1803
1619
|
|
|
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
1620
|
// Do shallow copy of message, as methods below will modify it.
|
|
1814
1621
|
// There might be multiple container instances receiving same message
|
|
1815
1622
|
// We do not need to make deep copy, as each layer will just replace message.content itself,
|
|
1816
1623
|
// but would not modify contents details
|
|
1817
1624
|
let message = { ...messageArg };
|
|
1818
1625
|
|
|
1626
|
+
// back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
|
|
1627
|
+
// System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
|
|
1628
|
+
// Old ops may contain empty string (I assume noops).
|
|
1629
|
+
if (typeof message.contents === "string" && message.contents !== "") {
|
|
1630
|
+
message.contents = JSON.parse(message.contents);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
|
|
1634
|
+
// This format was not shipped to production workflows.
|
|
1635
|
+
const runtimeMessage = unpackRuntimeMessage(message);
|
|
1636
|
+
|
|
1637
|
+
if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
|
|
1638
|
+
this.savedOps.push(messageArg);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1819
1641
|
// Surround the actual processing of the operation with messages to the schedule manager indicating
|
|
1820
1642
|
// the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
|
|
1821
1643
|
// messages once a batch has been fully processed.
|
|
1822
1644
|
this.scheduleManager.beforeOpProcessing(message);
|
|
1823
1645
|
|
|
1824
1646
|
try {
|
|
1825
|
-
message = unpackRuntimeMessage(message);
|
|
1826
|
-
|
|
1827
1647
|
// Chunk processing must come first given that we will transform the message to the unchunked version
|
|
1828
1648
|
// once all pieces are available
|
|
1829
1649
|
message = this.processRemoteChunkedMessage(message);
|
|
1830
1650
|
|
|
1831
1651
|
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
|
-
}
|
|
1652
|
+
if (local && runtimeMessage) {
|
|
1653
|
+
localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
|
|
1838
1654
|
}
|
|
1839
1655
|
|
|
1840
1656
|
// If there are no more pending messages after processing a local message,
|
|
1841
1657
|
// the document is no longer dirty.
|
|
1842
|
-
if (!this.
|
|
1658
|
+
if (!this.hasPendingMessages()) {
|
|
1843
1659
|
this.updateDocumentDirtyState(false);
|
|
1844
1660
|
}
|
|
1845
1661
|
|
|
1846
|
-
|
|
1662
|
+
const type = message.type as ContainerMessageType;
|
|
1663
|
+
switch (type) {
|
|
1847
1664
|
case ContainerMessageType.Attach:
|
|
1848
1665
|
this.dataStores.processAttachMessage(message, local);
|
|
1849
1666
|
break;
|
|
@@ -1854,13 +1671,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1854
1671
|
this.dataStores.processFluidDataStoreOp(message, local, localOpMetadata);
|
|
1855
1672
|
break;
|
|
1856
1673
|
case ContainerMessageType.BlobAttach:
|
|
1857
|
-
|
|
1858
|
-
|
|
1674
|
+
this.blobManager.processBlobAttachOp(message, local);
|
|
1675
|
+
break;
|
|
1676
|
+
case ContainerMessageType.ChunkedOp:
|
|
1677
|
+
case ContainerMessageType.Rejoin:
|
|
1859
1678
|
break;
|
|
1860
1679
|
default:
|
|
1680
|
+
assert(!runtimeMessage, 0x3ce /* Runtime message of unknown type */);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// For back-compat, notify only about runtime messages for now.
|
|
1684
|
+
if (runtimeMessage) {
|
|
1685
|
+
this.emit("op", message, runtimeMessage);
|
|
1861
1686
|
}
|
|
1862
1687
|
|
|
1863
|
-
this.emit("op", message);
|
|
1864
1688
|
this.scheduleManager.afterOpProcessing(undefined, message);
|
|
1865
1689
|
|
|
1866
1690
|
if (local) {
|
|
@@ -1937,6 +1761,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1937
1761
|
}
|
|
1938
1762
|
|
|
1939
1763
|
public async getRootDataStore(id: string, wait = true): Promise<IFluidRouter> {
|
|
1764
|
+
return this.getRootDataStoreChannel(id, wait);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
private async getRootDataStoreChannel(id: string, wait = true): Promise<IFluidDataStoreChannel> {
|
|
1768
|
+
await this.dataStores.waitIfPendingAlias(id);
|
|
1940
1769
|
const internalId = this.internalId(id);
|
|
1941
1770
|
const context = await this.dataStores.getDataStore(internalId, wait);
|
|
1942
1771
|
assert(await context.isRoot(), 0x12b /* "did not get root data store" */);
|
|
@@ -1969,30 +1798,76 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1969
1798
|
assert(this._orderSequentiallyCalls === 0,
|
|
1970
1799
|
0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
|
|
1971
1800
|
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
}
|
|
1801
|
+
this.flushBatch(this.pendingAttachBatch.popBatch());
|
|
1802
|
+
this.flushBatch(this.pendingBatch.popBatch());
|
|
1975
1803
|
|
|
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();
|
|
1804
|
+
assert(this.emptyBatch, 0x3cf /* reentrancy */);
|
|
1805
|
+
}
|
|
1981
1806
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1807
|
+
protected flushBatch(batch: BatchMessage[]): void {
|
|
1808
|
+
const length = batch.length;
|
|
1809
|
+
|
|
1810
|
+
if (length > 1) {
|
|
1811
|
+
batch[0].metadata = { ...batch[0].metadata, batch: true };
|
|
1812
|
+
batch[length - 1].metadata = { ...batch[length - 1].metadata, batch: false };
|
|
1813
|
+
|
|
1814
|
+
// This assert fires for the following reason (there might be more cases like that):
|
|
1815
|
+
// AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
|
|
1816
|
+
// i.e. in the middle of op processing!
|
|
1817
|
+
// Sending ops while processing ops is not good idea - it's not defined when
|
|
1818
|
+
// referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
|
|
1819
|
+
// If we send ops in response to processing multiple ops, then we for sure hit this assert!
|
|
1820
|
+
// Tracked via ADO #1834
|
|
1821
|
+
// assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
|
|
1822
|
+
// "Batch should be generated synchronously, without processing ops in the middle!");
|
|
1985
1823
|
}
|
|
1986
1824
|
|
|
1987
|
-
|
|
1825
|
+
let clientSequenceNumber: number = -1;
|
|
1988
1826
|
|
|
1989
1827
|
// Did we disconnect in the middle of turn-based batch?
|
|
1990
1828
|
// If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
|
|
1991
|
-
if (
|
|
1992
|
-
|
|
1829
|
+
if (this.canSendOps()) {
|
|
1830
|
+
if (this.context.submitBatchFn !== undefined) {
|
|
1831
|
+
const batchToSend: IBatchMessage[] = [];
|
|
1832
|
+
for (const message of batch) {
|
|
1833
|
+
batchToSend.push({ contents: message.contents, metadata: message.metadata });
|
|
1834
|
+
}
|
|
1835
|
+
// returns clientSequenceNumber of last message in a batch
|
|
1836
|
+
clientSequenceNumber = this.context.submitBatchFn(batchToSend);
|
|
1837
|
+
} else {
|
|
1838
|
+
// Legacy path - supporting old loader versions. Can be removed only when LTS moves above
|
|
1839
|
+
// version that has support for batches (submitBatchFn)
|
|
1840
|
+
for (const message of batch) {
|
|
1841
|
+
clientSequenceNumber = this.context.submitFn(
|
|
1842
|
+
MessageType.Operation,
|
|
1843
|
+
message.deserializedContent,
|
|
1844
|
+
true, // batch
|
|
1845
|
+
message.metadata);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
this.deltaSender.flush();
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
|
|
1852
|
+
clientSequenceNumber -= batch.length - 1;
|
|
1853
|
+
assert(clientSequenceNumber >= 0, 0x3d0 /* clientSequenceNumber can't be negative */);
|
|
1993
1854
|
}
|
|
1994
1855
|
|
|
1995
|
-
|
|
1856
|
+
// Let the PendingStateManager know that a message was submitted.
|
|
1857
|
+
// In future, need to shift toward keeping batch as a whole!
|
|
1858
|
+
for (const message of batch) {
|
|
1859
|
+
this.pendingStateManager.onSubmitMessage(
|
|
1860
|
+
message.deserializedContent.type,
|
|
1861
|
+
clientSequenceNumber,
|
|
1862
|
+
message.referenceSequenceNumber,
|
|
1863
|
+
message.deserializedContent.contents,
|
|
1864
|
+
message.localOpMetadata,
|
|
1865
|
+
message.metadata,
|
|
1866
|
+
);
|
|
1867
|
+
clientSequenceNumber++;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
this.pendingStateManager.onFlush();
|
|
1996
1871
|
}
|
|
1997
1872
|
|
|
1998
1873
|
public orderSequentially(callback: () => void): void {
|
|
@@ -2018,9 +1893,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2018
1893
|
}
|
|
2019
1894
|
|
|
2020
1895
|
private trackOrderSequentiallyCalls(callback: () => void): void {
|
|
2021
|
-
let checkpoint: { rollback: () => void; } | undefined;
|
|
1896
|
+
let checkpoint: { rollback: (action: (message: BatchMessage) => void) => void; } | undefined;
|
|
2022
1897
|
if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
|
|
2023
|
-
|
|
1898
|
+
// Note: we are not touching this.pendingAttachBatch here, for two reasons:
|
|
1899
|
+
// 1. It would not help, as we flush attach ops as they become available.
|
|
1900
|
+
// 2. There is no way to undo process of data store creation.
|
|
1901
|
+
checkpoint = this.pendingBatch.checkpoint();
|
|
2024
1902
|
}
|
|
2025
1903
|
|
|
2026
1904
|
try {
|
|
@@ -2029,7 +1907,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2029
1907
|
} catch (error) {
|
|
2030
1908
|
if (checkpoint) {
|
|
2031
1909
|
// This will throw and close the container if rollback fails
|
|
2032
|
-
|
|
1910
|
+
try {
|
|
1911
|
+
checkpoint.rollback((message: BatchMessage) =>
|
|
1912
|
+
this.rollback(
|
|
1913
|
+
message.deserializedContent.type,
|
|
1914
|
+
message.deserializedContent.contents,
|
|
1915
|
+
message.localOpMetadata));
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
const error2 = wrapError(err, (message) => {
|
|
1918
|
+
return DataProcessingError.create(
|
|
1919
|
+
`RollbackError: ${message}`,
|
|
1920
|
+
"checkpointRollback",
|
|
1921
|
+
undefined) as DataProcessingError;
|
|
1922
|
+
});
|
|
1923
|
+
this.closeFn(error2);
|
|
1924
|
+
throw error2;
|
|
1925
|
+
}
|
|
2033
1926
|
} else {
|
|
2034
1927
|
// pre-0.58 error message: orderSequentiallyCallbackException
|
|
2035
1928
|
this.closeFn(new GenericError("orderSequentially callback exception", error));
|
|
@@ -2043,79 +1936,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2043
1936
|
public async createDataStore(pkg: string | string[]): Promise<IDataStore> {
|
|
2044
1937
|
const internalId = uuid();
|
|
2045
1938
|
return channelToDataStore(
|
|
2046
|
-
await this._createDataStore(pkg,
|
|
1939
|
+
await this._createDataStore(pkg, internalId),
|
|
2047
1940
|
internalId,
|
|
2048
1941
|
this,
|
|
2049
1942
|
this.dataStores,
|
|
2050
1943
|
this.mc.logger);
|
|
2051
1944
|
}
|
|
2052
1945
|
|
|
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
1946
|
public createDetachedRootDataStore(
|
|
2120
1947
|
pkg: Readonly<string[]>,
|
|
2121
1948
|
rootDataStoreId: string): IFluidDataStoreContextDetached {
|
|
@@ -2129,55 +1956,23 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2129
1956
|
return this.dataStores.createDetachedDataStoreCore(pkg, false);
|
|
2130
1957
|
}
|
|
2131
1958
|
|
|
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(
|
|
1959
|
+
public async _createDataStoreWithProps(
|
|
2139
1960
|
pkg: string | string[],
|
|
2140
1961
|
props?: any,
|
|
2141
1962
|
id = uuid(),
|
|
2142
|
-
isRoot = false,
|
|
2143
1963
|
): Promise<IDataStore> {
|
|
2144
1964
|
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
|
-
}
|
|
1965
|
+
Array.isArray(pkg) ? pkg : [pkg], id, props).realize();
|
|
2159
1966
|
return channelToDataStore(fluidDataStore, id, this, this.dataStores, this.mc.logger);
|
|
2160
1967
|
}
|
|
2161
1968
|
|
|
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
1969
|
private async _createDataStore(
|
|
2174
1970
|
pkg: string | string[],
|
|
2175
|
-
isRoot: boolean,
|
|
2176
1971
|
id = uuid(),
|
|
2177
1972
|
props?: any,
|
|
2178
1973
|
): Promise<IFluidDataStoreChannel> {
|
|
2179
1974
|
return this.dataStores
|
|
2180
|
-
._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id,
|
|
1975
|
+
._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, props)
|
|
2181
1976
|
.realize();
|
|
2182
1977
|
}
|
|
2183
1978
|
|
|
@@ -2263,7 +2058,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2263
2058
|
this.emit("attached");
|
|
2264
2059
|
}
|
|
2265
2060
|
|
|
2266
|
-
if (attachState === AttachState.Attached && !this.
|
|
2061
|
+
if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
|
|
2267
2062
|
this.updateDocumentDirtyState(false);
|
|
2268
2063
|
}
|
|
2269
2064
|
this.dataStores.setAttachState(attachState);
|
|
@@ -2283,10 +2078,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2283
2078
|
}
|
|
2284
2079
|
|
|
2285
2080
|
const summarizeResult = this.dataStores.createSummary(telemetryContext);
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
}
|
|
2081
|
+
// Wrap data store summaries in .channels subtree.
|
|
2082
|
+
wrapSummaryInChannelsTree(summarizeResult);
|
|
2083
|
+
|
|
2290
2084
|
this.addContainerStateToSummary(
|
|
2291
2085
|
summarizeResult,
|
|
2292
2086
|
true /* fullTree */,
|
|
@@ -2312,13 +2106,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2312
2106
|
telemetryContext?: ITelemetryContext,
|
|
2313
2107
|
): Promise<ISummarizeInternalResult> {
|
|
2314
2108
|
const summarizeResult = await this.dataStores.summarize(fullTree, trackState, telemetryContext);
|
|
2315
|
-
let pathPartsForChildren: string[] | undefined;
|
|
2316
2109
|
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
}
|
|
2110
|
+
// Wrap data store summaries in .channels subtree.
|
|
2111
|
+
wrapSummaryInChannelsTree(summarizeResult);
|
|
2112
|
+
const pathPartsForChildren = [channelsTreeName];
|
|
2113
|
+
|
|
2322
2114
|
this.addContainerStateToSummary(summarizeResult, fullTree, trackState, telemetryContext);
|
|
2323
2115
|
return {
|
|
2324
2116
|
...summarizeResult,
|
|
@@ -2488,7 +2280,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2488
2280
|
|
|
2489
2281
|
/**
|
|
2490
2282
|
* Runs garbage collection and updates the reference / used state of the nodes in the container.
|
|
2491
|
-
* @returns the statistics of the garbage collection run.
|
|
2283
|
+
* @returns the statistics of the garbage collection run; undefined if GC did not run.
|
|
2492
2284
|
*/
|
|
2493
2285
|
public async collectGarbage(
|
|
2494
2286
|
options: {
|
|
@@ -2499,7 +2291,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2499
2291
|
/** True to generate full GC data */
|
|
2500
2292
|
fullGC?: boolean;
|
|
2501
2293
|
},
|
|
2502
|
-
): Promise<IGCStats> {
|
|
2294
|
+
): Promise<IGCStats | undefined> {
|
|
2503
2295
|
return this.garbageCollector.collectGarbage(options);
|
|
2504
2296
|
}
|
|
2505
2297
|
|
|
@@ -2534,6 +2326,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2534
2326
|
},
|
|
2535
2327
|
);
|
|
2536
2328
|
|
|
2329
|
+
assert(this.emptyBatch, 0x3d1 /* Can't trigger summary in the middle of a batch */);
|
|
2330
|
+
|
|
2537
2331
|
let latestSnapshotVersionId: string | undefined;
|
|
2538
2332
|
if (refreshLatestAck) {
|
|
2539
2333
|
const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(
|
|
@@ -2563,15 +2357,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2563
2357
|
const summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
|
|
2564
2358
|
const minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
|
|
2565
2359
|
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
|
-
}
|
|
2360
|
+
const lastAck = this.summaryCollection.latestAck;
|
|
2575
2361
|
|
|
2576
2362
|
this.summarizerNode.startSummary(summaryRefSeqNum, summaryNumberLogger);
|
|
2577
2363
|
|
|
@@ -2601,6 +2387,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2601
2387
|
error: `lastSequenceNumber changed before uploading to storage. ${this.deltaManager.lastSequenceNumber} !== ${summaryRefSeqNum}`,
|
|
2602
2388
|
};
|
|
2603
2389
|
}
|
|
2390
|
+
assert(summaryRefSeqNum === this.deltaManager.lastMessage?.sequenceNumber,
|
|
2391
|
+
0x395 /* it's one and the same thing */);
|
|
2392
|
+
|
|
2393
|
+
if (lastAck !== this.summaryCollection.latestAck) {
|
|
2394
|
+
return {
|
|
2395
|
+
continue: false,
|
|
2396
|
+
// eslint-disable-next-line max-len
|
|
2397
|
+
error: `Last summary changed while summarizing. ${this.summaryCollection.latestAck} !== ${lastAck}`,
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2604
2400
|
return { continue: true };
|
|
2605
2401
|
};
|
|
2606
2402
|
|
|
@@ -2621,7 +2417,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2621
2417
|
const forcedFullTree = this.garbageCollector.summaryStateNeedsReset;
|
|
2622
2418
|
try {
|
|
2623
2419
|
summarizeResult = await this.summarize({
|
|
2624
|
-
fullTree: fullTree
|
|
2420
|
+
fullTree: fullTree ?? forcedFullTree,
|
|
2625
2421
|
trackState: true,
|
|
2626
2422
|
summaryLogger: summaryNumberLogger,
|
|
2627
2423
|
runGC: this.garbageCollector.shouldRunGC,
|
|
@@ -2642,7 +2438,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2642
2438
|
// Counting dataStores and handles
|
|
2643
2439
|
// Because handles are unchanged dataStores in the current logic,
|
|
2644
2440
|
// summarized dataStore count is total dataStore count minus handle count
|
|
2645
|
-
const dataStoreTree =
|
|
2441
|
+
const dataStoreTree = summaryTree.tree[channelsTreeName];
|
|
2646
2442
|
|
|
2647
2443
|
assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
|
|
2648
2444
|
const handleCount = Object.values(dataStoreTree.tree).filter(
|
|
@@ -2657,8 +2453,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2657
2453
|
gcStateUpdatedDataStoreCount: summarizeResult.gcStats?.updatedDataStoreCount,
|
|
2658
2454
|
gcBlobNodeCount: gcSummaryTreeStats?.blobNodeCount,
|
|
2659
2455
|
gcTotalBlobsSize: gcSummaryTreeStats?.totalBlobSize,
|
|
2660
|
-
opsSizesSinceLastSummary: this.opTracker.opsSizeAccumulator,
|
|
2661
|
-
nonSystemOpsSinceLastSummary: this.opTracker.nonSystemOpCount,
|
|
2662
2456
|
summaryNumber,
|
|
2663
2457
|
...partialStats,
|
|
2664
2458
|
};
|
|
@@ -2681,7 +2475,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2681
2475
|
// submitting the summaryOp then we can't rely on summaryAck. So in case we have
|
|
2682
2476
|
// latestSnapshotVersionId from storage and it does not match with the lastAck ackHandle, then use
|
|
2683
2477
|
// the one fetched from storage as parent as that is the latest.
|
|
2684
|
-
const lastAck = this.summaryCollection.latestAck;
|
|
2685
2478
|
let summaryContext: ISummaryContext;
|
|
2686
2479
|
if (lastAck?.summaryAck.contents.handle !== latestSnapshotVersionId
|
|
2687
2480
|
&& latestSnapshotVersionId !== undefined) {
|
|
@@ -2732,7 +2525,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2732
2525
|
|
|
2733
2526
|
let clientSequenceNumber: number;
|
|
2734
2527
|
try {
|
|
2735
|
-
clientSequenceNumber = this.
|
|
2528
|
+
clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
|
|
2736
2529
|
} catch (error) {
|
|
2737
2530
|
return { stage: "upload", ...uploadData, error };
|
|
2738
2531
|
}
|
|
@@ -2745,7 +2538,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2745
2538
|
} as const;
|
|
2746
2539
|
|
|
2747
2540
|
this.summarizerNode.completeSummary(handle);
|
|
2748
|
-
this.opTracker.reset();
|
|
2749
2541
|
return submitData;
|
|
2750
2542
|
} finally {
|
|
2751
2543
|
// Cleanup wip summary in case of failure
|
|
@@ -2792,7 +2584,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2792
2584
|
}
|
|
2793
2585
|
}
|
|
2794
2586
|
|
|
2587
|
+
private hasPendingMessages() {
|
|
2588
|
+
return this.pendingStateManager.hasPendingMessages() || !this.emptyBatch;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2795
2591
|
private updateDocumentDirtyState(dirty: boolean) {
|
|
2592
|
+
if (this.attachState !== AttachState.Attached) {
|
|
2593
|
+
assert(dirty, 0x3d2 /* Non-attached container is dirty */);
|
|
2594
|
+
} else {
|
|
2595
|
+
// Other way is not true = see this.isContainerMessageDirtyable()
|
|
2596
|
+
assert(!dirty || this.hasPendingMessages(),
|
|
2597
|
+
0x3d3 /* if doc is dirty, there has to be pending ops */);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2796
2600
|
if (this.dirtyContainer === dirty) {
|
|
2797
2601
|
return;
|
|
2798
2602
|
}
|
|
@@ -2831,160 +2635,117 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2831
2635
|
|
|
2832
2636
|
private submit(
|
|
2833
2637
|
type: ContainerMessageType,
|
|
2834
|
-
|
|
2638
|
+
contents: any,
|
|
2835
2639
|
localOpMetadata: unknown = undefined,
|
|
2836
|
-
|
|
2640
|
+
metadata: Record<string, unknown> | undefined = undefined,
|
|
2837
2641
|
): void {
|
|
2838
2642
|
this.verifyNotClosed();
|
|
2839
2643
|
|
|
2840
2644
|
// There should be no ops in detached container state!
|
|
2841
2645
|
assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
|
|
2842
2646
|
|
|
2843
|
-
|
|
2844
|
-
|
|
2647
|
+
const deserializedContent: ContainerRuntimeMessage = { type, contents };
|
|
2648
|
+
const serializedContent = JSON.stringify(deserializedContent);
|
|
2845
2649
|
|
|
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);
|
|
2650
|
+
if (this.deltaManager.readOnlyInfo.readonly) {
|
|
2651
|
+
this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
|
|
2875
2652
|
}
|
|
2876
2653
|
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
this.deltaManager.lastSequenceNumber,
|
|
2882
|
-
content,
|
|
2654
|
+
const message: BatchMessage = {
|
|
2655
|
+
contents: serializedContent,
|
|
2656
|
+
deserializedContent,
|
|
2657
|
+
metadata,
|
|
2883
2658
|
localOpMetadata,
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
if (this.isContainerMessageDirtyable(type, content)) {
|
|
2887
|
-
this.updateDocumentDirtyState(true);
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2659
|
+
referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
|
|
2660
|
+
};
|
|
2890
2661
|
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
//
|
|
2901
|
-
|
|
2902
|
-
|
|
2662
|
+
try {
|
|
2663
|
+
// If this is attach message for new data store, and we are in a batch, send this op out of order
|
|
2664
|
+
// Is it safe:
|
|
2665
|
+
// Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
|
|
2666
|
+
// They become visible only when aliased, or handle to some sub-element of newly created datastore
|
|
2667
|
+
// is stored in some DDS, i.e. only after some other op.
|
|
2668
|
+
// Why:
|
|
2669
|
+
// Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
|
|
2670
|
+
// stores are created, causing issues like relay service throttling (too many ops) and catastrophic
|
|
2671
|
+
// failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
|
|
2672
|
+
// these issues.
|
|
2673
|
+
// Cons:
|
|
2674
|
+
// 1. With large batches, relay service may throttle clients. Clients may disconnect while throttled.
|
|
2675
|
+
// This change creates new possibility of a lot of newly created data stores never being referenced
|
|
2676
|
+
// because client died before it had a change to submit the rest of the ops. This will create more
|
|
2677
|
+
// garbage that needs to be collected leveraging GC (Garbage Collection) feature.
|
|
2678
|
+
// 2. Sending ops out of order means they are excluded from rollback functionality. This is not an issue
|
|
2679
|
+
// today as rollback can't undo creation of data store. To some extent not sending them is a bigger
|
|
2680
|
+
// issue than sending.
|
|
2681
|
+
// Please note that this does not change file format, so it can be disabled in the future if this
|
|
2682
|
+
// optimization no longer makes sense (for example, batch compression may make it less appealing).
|
|
2683
|
+
if (type === ContainerMessageType.Attach &&
|
|
2684
|
+
this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
|
|
2685
|
+
if (!this.pendingAttachBatch.push(message)) {
|
|
2686
|
+
// BatchManager has two limits - soft limit & hard limit. Soft limit is only engaged
|
|
2687
|
+
// when queue is not empty.
|
|
2688
|
+
// Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
|
|
2689
|
+
this.flushBatch(this.pendingAttachBatch.popBatch());
|
|
2690
|
+
if (!this.pendingAttachBatch.push(message)) {
|
|
2691
|
+
throw new GenericError(
|
|
2692
|
+
"BatchTooLarge",
|
|
2693
|
+
/* error */ undefined,
|
|
2694
|
+
{
|
|
2695
|
+
opSize: message.contents.length,
|
|
2696
|
+
count: this.pendingAttachBatch.length,
|
|
2697
|
+
limit: this.pendingAttachBatch.limit,
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
} else {
|
|
2702
|
+
if (!this.pendingBatch.push(message)) {
|
|
2703
|
+
throw new GenericError(
|
|
2704
|
+
"BatchTooLarge",
|
|
2705
|
+
/* error */ undefined,
|
|
2706
|
+
{
|
|
2707
|
+
opSize: message.contents.length,
|
|
2708
|
+
count: this.pendingBatch.length,
|
|
2709
|
+
limit: this.pendingBatch.limit,
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2903
2712
|
}
|
|
2904
2713
|
|
|
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);
|
|
2714
|
+
if (this._flushMode !== FlushMode.TurnBased) {
|
|
2715
|
+
this.flush();
|
|
2716
|
+
} else if (!this.flushTrigger) {
|
|
2717
|
+
this.flushTrigger = true;
|
|
2718
|
+
// Queue a microtask to detect the end of the turn and force a flush.
|
|
2719
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
2720
|
+
Promise.resolve().then(() => {
|
|
2721
|
+
this.flushTrigger = false;
|
|
2722
|
+
this.flush();
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
this.closeFn(error as GenericError);
|
|
2727
|
+
throw error;
|
|
2928
2728
|
}
|
|
2929
2729
|
|
|
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);
|
|
2730
|
+
if (this.isContainerMessageDirtyable(type, contents)) {
|
|
2731
|
+
this.updateDocumentDirtyState(true);
|
|
2950
2732
|
}
|
|
2951
|
-
return clientSequenceNumber;
|
|
2952
2733
|
}
|
|
2953
2734
|
|
|
2954
|
-
private
|
|
2955
|
-
type: MessageType,
|
|
2956
|
-
contents: any) {
|
|
2735
|
+
private submitSummaryMessage(contents: ISummaryContent) {
|
|
2957
2736
|
this.verifyNotClosed();
|
|
2958
2737
|
assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
|
|
2959
2738
|
|
|
2960
2739
|
// 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);
|
|
2740
|
+
assert(this.emptyBatch, 0x3d4 /* System op in the middle of a batch */);
|
|
2741
|
+
|
|
2742
|
+
// back-compat: ADO #1385: Make this call unconditional in the future
|
|
2743
|
+
return this.context.submitSummaryFn !== undefined
|
|
2744
|
+
? this.context.submitSummaryFn(contents)
|
|
2745
|
+
: this.context.submitFn(
|
|
2746
|
+
MessageType.Summarize,
|
|
2747
|
+
contents,
|
|
2748
|
+
false);
|
|
2988
2749
|
}
|
|
2989
2750
|
|
|
2990
2751
|
/**
|
|
@@ -3022,7 +2783,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3022
2783
|
case ContainerMessageType.ChunkedOp:
|
|
3023
2784
|
throw new Error(`chunkedOp not expected here`);
|
|
3024
2785
|
case ContainerMessageType.BlobAttach:
|
|
3025
|
-
this.
|
|
2786
|
+
this.blobManager.reSubmit(opMetadata);
|
|
3026
2787
|
break;
|
|
3027
2788
|
case ContainerMessageType.Rejoin:
|
|
3028
2789
|
this.submit(type, content);
|
|
@@ -3056,20 +2817,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3056
2817
|
summaryLogger: ITelemetryLogger,
|
|
3057
2818
|
) {
|
|
3058
2819
|
const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
eventName: "RefreshLatestSummaryGetSnapshot",
|
|
2820
|
+
// The call to fetch the snapshot is very expensive and not always needed.
|
|
2821
|
+
// It should only be done by the summarizerNode, if required.
|
|
2822
|
+
const snapshotTreeFetcher = async () => {
|
|
2823
|
+
const fetchResult = await this.fetchSnapshotFromStorage(
|
|
3064
2824
|
ackHandle,
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
2825
|
+
summaryLogger,
|
|
2826
|
+
{
|
|
2827
|
+
eventName: "RefreshLatestSummaryGetSnapshot",
|
|
2828
|
+
ackHandle,
|
|
2829
|
+
summaryRefSeq,
|
|
2830
|
+
fetchLatest: false,
|
|
2831
|
+
});
|
|
2832
|
+
return fetchResult.snapshotTree;
|
|
2833
|
+
};
|
|
3069
2834
|
const result = await this.summarizerNode.refreshLatestSummary(
|
|
3070
2835
|
proposalHandle,
|
|
3071
2836
|
summaryRefSeq,
|
|
3072
|
-
|
|
2837
|
+
snapshotTreeFetcher,
|
|
3073
2838
|
readAndParseBlob,
|
|
3074
2839
|
summaryLogger,
|
|
3075
2840
|
);
|
|
@@ -3088,9 +2853,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3088
2853
|
summaryLogger: ITelemetryLogger,
|
|
3089
2854
|
): Promise<{ latestSnapshotRefSeq: number; latestSnapshotVersionId: string | undefined; }> {
|
|
3090
2855
|
const { snapshotTree, versionId } = await this.fetchSnapshotFromStorage(null, summaryLogger, {
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
2856
|
+
eventName: "RefreshLatestSummaryGetSnapshot",
|
|
2857
|
+
fetchLatest: true,
|
|
2858
|
+
},
|
|
2859
|
+
FetchSource.noCache,
|
|
3094
2860
|
);
|
|
3095
2861
|
|
|
3096
2862
|
const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
|
|
@@ -3114,6 +2880,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3114
2880
|
versionId: string | null,
|
|
3115
2881
|
logger: ITelemetryLogger,
|
|
3116
2882
|
event: ITelemetryGenericEvent,
|
|
2883
|
+
fetchSource?: FetchSource,
|
|
3117
2884
|
): Promise<{ snapshotTree: ISnapshotTree; versionId: string; }> {
|
|
3118
2885
|
return PerformanceEvent.timedExecAsync(
|
|
3119
2886
|
logger, event, async (perfEvent: {
|
|
@@ -3125,7 +2892,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3125
2892
|
const stats: { getVersionDuration?: number; getSnapshotDuration?: number; } = {};
|
|
3126
2893
|
const trace = Trace.start();
|
|
3127
2894
|
|
|
3128
|
-
const versions = await this.storage.getVersions(
|
|
2895
|
+
const versions = await this.storage.getVersions(
|
|
2896
|
+
versionId, 1, "refreshLatestSummaryAckFromServer", fetchSource);
|
|
3129
2897
|
assert(!!versions && !!versions[0], 0x137 /* "Failed to get version from storage" */);
|
|
3130
2898
|
stats.getVersionDuration = trace.trace().duration;
|
|
3131
2899
|
|
|
@@ -3153,15 +2921,21 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3153
2921
|
this.baseSnapshotBlobs = await SerializedSnapshotStorage.serializeTree(this.context.baseSnapshot, this.storage);
|
|
3154
2922
|
}
|
|
3155
2923
|
|
|
3156
|
-
public getPendingLocalState():
|
|
2924
|
+
public getPendingLocalState(): unknown {
|
|
3157
2925
|
if (!(this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad)) {
|
|
3158
2926
|
throw new UsageError("can't get state when offline load disabled");
|
|
3159
2927
|
}
|
|
3160
2928
|
|
|
2929
|
+
// Flush pending batch.
|
|
2930
|
+
// getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
|
|
2931
|
+
// to close current batch.
|
|
2932
|
+
this.flush();
|
|
2933
|
+
|
|
3161
2934
|
const previousPendingState = this.context.pendingLocalState as IPendingRuntimeState | undefined;
|
|
3162
2935
|
if (previousPendingState) {
|
|
3163
2936
|
return {
|
|
3164
2937
|
pending: this.pendingStateManager.getLocalState(),
|
|
2938
|
+
pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
|
|
3165
2939
|
snapshotBlobs: previousPendingState.snapshotBlobs,
|
|
3166
2940
|
baseSnapshot: previousPendingState.baseSnapshot,
|
|
3167
2941
|
savedOps: this.savedOps,
|
|
@@ -3171,6 +2945,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3171
2945
|
assert(!!this.baseSnapshotBlobs, 0x2e7 /* "Must serialize base snapshot blobs before getting runtime state" */);
|
|
3172
2946
|
return {
|
|
3173
2947
|
pending: this.pendingStateManager.getLocalState(),
|
|
2948
|
+
pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
|
|
3174
2949
|
snapshotBlobs: this.baseSnapshotBlobs,
|
|
3175
2950
|
baseSnapshot: this.context.baseSnapshot,
|
|
3176
2951
|
savedOps: this.savedOps,
|
|
@@ -3245,6 +3020,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
3245
3020
|
// we may not have seen every sequence number (because of system ops) so apply everything once we
|
|
3246
3021
|
// don't have any more saved ops
|
|
3247
3022
|
await this.pendingStateManager.applyStashedOpsAt();
|
|
3023
|
+
|
|
3024
|
+
// If it's not the case, we should take it into account when calculating dirty state.
|
|
3025
|
+
assert(this.context.attachState === AttachState.Attached,
|
|
3026
|
+
0x3d5 /* this function is called for attached containers only */);
|
|
3027
|
+
if (!this.hasPendingMessages()) {
|
|
3028
|
+
this.updateDocumentDirtyState(false);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
private validateSummaryHeuristicConfiguration(configuration: ISummaryConfigurationHeuristics) {
|
|
3033
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
3034
|
+
for (const prop in configuration) {
|
|
3035
|
+
if (typeof configuration[prop] === "number" && configuration[prop] < 0) {
|
|
3036
|
+
throw new UsageError(`Summary heuristic configuration property "${prop}" cannot be less than 0`);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3248
3039
|
}
|
|
3249
3040
|
}
|
|
3250
3041
|
|