@fluidframework/container-runtime 0.58.2002 → 0.58.3000-61081
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blobManager.d.ts +3 -2
- package/dist/blobManager.d.ts.map +1 -1
- package/dist/blobManager.js +13 -9
- package/dist/blobManager.js.map +1 -1
- package/dist/connectionTelemetry.d.ts.map +1 -1
- package/dist/connectionTelemetry.js +63 -23
- package/dist/connectionTelemetry.js.map +1 -1
- package/dist/containerRuntime.d.ts +12 -4
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +61 -15
- package/dist/containerRuntime.js.map +1 -1
- package/dist/opTelemetry.d.ts +22 -0
- package/dist/opTelemetry.d.ts.map +1 -0
- package/dist/opTelemetry.js +59 -0
- package/dist/opTelemetry.js.map +1 -0
- package/dist/orderedClientElection.d.ts +57 -6
- package/dist/orderedClientElection.d.ts.map +1 -1
- package/dist/orderedClientElection.js +140 -25
- 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/summarizerClientElection.d.ts +2 -0
- package/dist/summarizerClientElection.d.ts.map +1 -1
- package/dist/summarizerClientElection.js +7 -2
- package/dist/summarizerClientElection.js.map +1 -1
- package/dist/summarizerTypes.d.ts +9 -0
- package/dist/summarizerTypes.d.ts.map +1 -1
- package/dist/summarizerTypes.js.map +1 -1
- package/dist/summaryGenerator.d.ts.map +1 -1
- package/dist/summaryGenerator.js +1 -1
- package/dist/summaryGenerator.js.map +1 -1
- package/dist/summaryManager.d.ts.map +1 -1
- package/dist/summaryManager.js +14 -3
- package/dist/summaryManager.js.map +1 -1
- package/lib/blobManager.d.ts +3 -2
- package/lib/blobManager.d.ts.map +1 -1
- package/lib/blobManager.js +14 -10
- package/lib/blobManager.js.map +1 -1
- package/lib/connectionTelemetry.d.ts.map +1 -1
- package/lib/connectionTelemetry.js +63 -23
- package/lib/connectionTelemetry.js.map +1 -1
- package/lib/containerRuntime.d.ts +12 -4
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +62 -16
- package/lib/containerRuntime.js.map +1 -1
- package/lib/opTelemetry.d.ts +22 -0
- package/lib/opTelemetry.d.ts.map +1 -0
- package/lib/opTelemetry.js +55 -0
- package/lib/opTelemetry.js.map +1 -0
- package/lib/orderedClientElection.d.ts +57 -6
- package/lib/orderedClientElection.d.ts.map +1 -1
- package/lib/orderedClientElection.js +140 -25
- 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/summarizerClientElection.d.ts +2 -0
- package/lib/summarizerClientElection.d.ts.map +1 -1
- package/lib/summarizerClientElection.js +7 -2
- package/lib/summarizerClientElection.js.map +1 -1
- package/lib/summarizerTypes.d.ts +9 -0
- package/lib/summarizerTypes.d.ts.map +1 -1
- package/lib/summarizerTypes.js.map +1 -1
- package/lib/summaryGenerator.d.ts.map +1 -1
- package/lib/summaryGenerator.js +1 -1
- package/lib/summaryGenerator.js.map +1 -1
- package/lib/summaryManager.d.ts.map +1 -1
- package/lib/summaryManager.js +14 -3
- package/lib/summaryManager.js.map +1 -1
- package/package.json +47 -15
- package/src/blobManager.ts +19 -11
- package/src/connectionTelemetry.ts +110 -19
- package/src/containerRuntime.ts +85 -19
- package/src/opTelemetry.ts +71 -0
- package/src/orderedClientElection.ts +154 -25
- package/src/packageVersion.ts +1 -1
- package/src/summarizerClientElection.ts +7 -2
- package/src/summarizerTypes.ts +9 -0
- package/src/summaryGenerator.ts +9 -1
- package/src/summaryManager.ts +15 -4
package/src/containerRuntime.ts
CHANGED
|
@@ -91,7 +91,6 @@ import {
|
|
|
91
91
|
import {
|
|
92
92
|
addBlobToSummary,
|
|
93
93
|
addTreeToSummary,
|
|
94
|
-
convertToSummaryTree,
|
|
95
94
|
createRootSummarizerNodeWithGC,
|
|
96
95
|
IRootSummarizerNodeWithGC,
|
|
97
96
|
RequestParser,
|
|
@@ -100,6 +99,7 @@ import {
|
|
|
100
99
|
requestFluidObject,
|
|
101
100
|
responseToException,
|
|
102
101
|
seqFromTree,
|
|
102
|
+
calculateStats,
|
|
103
103
|
} from "@fluidframework/runtime-utils";
|
|
104
104
|
import { v4 as uuid } from "uuid";
|
|
105
105
|
import { ContainerFluidHandleContext } from "./containerHandleContext";
|
|
@@ -152,6 +152,7 @@ import {
|
|
|
152
152
|
isDataStoreAliasMessage,
|
|
153
153
|
} from "./dataStore";
|
|
154
154
|
import { BindBatchTracker } from "./batchTracker";
|
|
155
|
+
import { OpTracker } from "./opTelemetry";
|
|
155
156
|
|
|
156
157
|
export enum ContainerMessageType {
|
|
157
158
|
// An op to be delivered to store
|
|
@@ -277,8 +278,8 @@ export interface ISummaryRuntimeOptions {
|
|
|
277
278
|
* Options for container runtime.
|
|
278
279
|
*/
|
|
279
280
|
export interface IContainerRuntimeOptions {
|
|
280
|
-
summaryOptions?: ISummaryRuntimeOptions;
|
|
281
|
-
gcOptions?: IGCRuntimeOptions;
|
|
281
|
+
readonly summaryOptions?: ISummaryRuntimeOptions;
|
|
282
|
+
readonly gcOptions?: IGCRuntimeOptions;
|
|
282
283
|
/**
|
|
283
284
|
* Affects the behavior while loading the runtime when the data verification check which
|
|
284
285
|
* compares the DeltaManager sequence number (obtained from protocol in summary) to the
|
|
@@ -287,13 +288,20 @@ export interface IContainerRuntimeOptions {
|
|
|
287
288
|
* 2. "log" will log an error event to telemetry, but still continue to load.
|
|
288
289
|
* 3. "bypass" will skip the check entirely. This is not recommended.
|
|
289
290
|
*/
|
|
290
|
-
loadSequenceNumberVerification?: "close" | "log" | "bypass";
|
|
291
|
+
readonly loadSequenceNumberVerification?: "close" | "log" | "bypass";
|
|
291
292
|
/**
|
|
292
293
|
* Should the runtime use data store aliasing for creating root datastores.
|
|
293
294
|
* In case of aliasing conflicts, the runtime will raise an exception which does
|
|
294
295
|
* not effect the status of the container.
|
|
295
296
|
*/
|
|
296
|
-
useDataStoreAliasing?: boolean;
|
|
297
|
+
readonly useDataStoreAliasing?: boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Sets the flush mode for the runtime. In Immediate flush mode the runtime will immediately
|
|
300
|
+
* send all operations to the driver layer, while in TurnBased the operations will be buffered
|
|
301
|
+
* and then sent them as a single batch at the end of the turn.
|
|
302
|
+
* By default, flush mode is TurnBased.
|
|
303
|
+
*/
|
|
304
|
+
readonly flushMode?: FlushMode;
|
|
297
305
|
}
|
|
298
306
|
|
|
299
307
|
type IRuntimeMessageMetadata = undefined | {
|
|
@@ -347,6 +355,13 @@ const maxOpSizeInBytesKey = "Fluid.ContainerRuntime.MaxOpSizeInBytes";
|
|
|
347
355
|
// to not reach the 1MB limits in socket.io and Kafka.
|
|
348
356
|
const defaultMaxOpSizeInBytes = 768000;
|
|
349
357
|
|
|
358
|
+
// By default, the size of the contents for the incoming ops is tracked.
|
|
359
|
+
// However, in certain situations, this may incur a performance hit.
|
|
360
|
+
// The feature-gate below can be used to disable this feature.
|
|
361
|
+
const disableOpTrackingKey = "Fluid.ContainerRuntime.DisableOpTracking";
|
|
362
|
+
|
|
363
|
+
const defaultFlushMode = FlushMode.TurnBased;
|
|
364
|
+
|
|
350
365
|
export enum RuntimeMessage {
|
|
351
366
|
FluidDataStoreOp = "component",
|
|
352
367
|
Attach = "attach",
|
|
@@ -394,6 +409,7 @@ class ScheduleManagerCore {
|
|
|
394
409
|
private currentBatchClientId: string | undefined;
|
|
395
410
|
private localPaused = false;
|
|
396
411
|
private timePaused = 0;
|
|
412
|
+
private batchCount = 0;
|
|
397
413
|
|
|
398
414
|
constructor(
|
|
399
415
|
private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
|
|
@@ -482,14 +498,30 @@ class ScheduleManagerCore {
|
|
|
482
498
|
this.deltaManager.inbound.pause();
|
|
483
499
|
}
|
|
484
500
|
|
|
485
|
-
private resumeQueue(startBatch: number,
|
|
501
|
+
private resumeQueue(startBatch: number, messageEndBatch: ISequencedDocumentMessage) {
|
|
502
|
+
const endBatch = messageEndBatch.sequenceNumber;
|
|
503
|
+
const duration = performance.now() - this.timePaused;
|
|
504
|
+
|
|
505
|
+
this.batchCount++;
|
|
506
|
+
if (this.batchCount % 1000 === 1) {
|
|
507
|
+
this.logger.sendTelemetryEvent({
|
|
508
|
+
eventName: "BatchStats",
|
|
509
|
+
sequenceNumber: endBatch,
|
|
510
|
+
length: endBatch - startBatch + 1,
|
|
511
|
+
msnDistance: endBatch - messageEndBatch.minimumSequenceNumber,
|
|
512
|
+
duration,
|
|
513
|
+
batchCount: this.batchCount,
|
|
514
|
+
interrupted: this.localPaused,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
486
518
|
// Return early if no change in value
|
|
487
519
|
if (!this.localPaused) {
|
|
488
520
|
return;
|
|
489
521
|
}
|
|
490
522
|
|
|
491
523
|
this.localPaused = false;
|
|
492
|
-
|
|
524
|
+
|
|
493
525
|
// Random round number - we want to know when batch waiting paused op processing.
|
|
494
526
|
if (duration > latencyThreshold) {
|
|
495
527
|
this.logger.sendErrorEvent({
|
|
@@ -564,7 +596,7 @@ class ScheduleManagerCore {
|
|
|
564
596
|
} else if (batchMetadata === false) {
|
|
565
597
|
assert(this.pauseSequenceNumber !== undefined, 0x2a0 /* "batch presence was validated above" */);
|
|
566
598
|
// Batch is complete, we can process it!
|
|
567
|
-
this.resumeQueue(this.pauseSequenceNumber, message
|
|
599
|
+
this.resumeQueue(this.pauseSequenceNumber, message);
|
|
568
600
|
this.pauseSequenceNumber = undefined;
|
|
569
601
|
this.currentBatchClientId = undefined;
|
|
570
602
|
} else {
|
|
@@ -710,6 +742,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
710
742
|
gcOptions = {},
|
|
711
743
|
loadSequenceNumberVerification = "close",
|
|
712
744
|
useDataStoreAliasing = false,
|
|
745
|
+
flushMode = defaultFlushMode,
|
|
713
746
|
} = runtimeOptions;
|
|
714
747
|
|
|
715
748
|
// We pack at data store level only. If isolated channels are disabled,
|
|
@@ -805,6 +838,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
805
838
|
gcOptions,
|
|
806
839
|
loadSequenceNumberVerification,
|
|
807
840
|
useDataStoreAliasing,
|
|
841
|
+
flushMode,
|
|
808
842
|
},
|
|
809
843
|
containerScope,
|
|
810
844
|
logger,
|
|
@@ -909,7 +943,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
909
943
|
private readonly defaultMaxConsecutiveReconnects = 15;
|
|
910
944
|
|
|
911
945
|
private _orderSequentiallyCalls: number = 0;
|
|
912
|
-
private _flushMode: FlushMode
|
|
946
|
+
private _flushMode: FlushMode;
|
|
913
947
|
private needsFlush = false;
|
|
914
948
|
private flushTrigger = false;
|
|
915
949
|
|
|
@@ -983,6 +1017,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
983
1017
|
|
|
984
1018
|
private readonly createContainerMetadata: ICreateContainerMetadata;
|
|
985
1019
|
private summaryCount: number | undefined;
|
|
1020
|
+
private readonly opTracker: OpTracker;
|
|
986
1021
|
|
|
987
1022
|
private constructor(
|
|
988
1023
|
private readonly context: IContainerContext,
|
|
@@ -1037,6 +1072,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1037
1072
|
this.maxConsecutiveReconnects =
|
|
1038
1073
|
this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? this.defaultMaxConsecutiveReconnects;
|
|
1039
1074
|
|
|
1075
|
+
this._flushMode = runtimeOptions.flushMode;
|
|
1040
1076
|
this.garbageCollector = GarbageCollector.create(
|
|
1041
1077
|
this,
|
|
1042
1078
|
this.runtimeOptions.gcOptions,
|
|
@@ -1277,6 +1313,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1277
1313
|
|
|
1278
1314
|
ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
|
|
1279
1315
|
BindBatchTracker(this, this.logger);
|
|
1316
|
+
this.opTracker = new OpTracker(this.deltaManager, this.mc.config.getBoolean(disableOpTrackingKey) === true);
|
|
1280
1317
|
}
|
|
1281
1318
|
|
|
1282
1319
|
public dispose(error?: Error): void {
|
|
@@ -1446,13 +1483,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1446
1483
|
const electedSummarizerContent = JSON.stringify(this.summarizerClientElection?.serialize());
|
|
1447
1484
|
addBlobToSummary(summaryTree, electedSummarizerBlobName, electedSummarizerContent);
|
|
1448
1485
|
}
|
|
1449
|
-
const snapshot = this.blobManager.snapshot();
|
|
1450
1486
|
|
|
1487
|
+
const summary = this.blobManager.summarize();
|
|
1451
1488
|
// Some storage (like git) doesn't allow empty tree, so we can omit it.
|
|
1452
1489
|
// and the blob manager can handle the tree not existing when loading
|
|
1453
|
-
if (
|
|
1454
|
-
|
|
1455
|
-
addTreeToSummary(summaryTree, blobsTreeName, blobsTree);
|
|
1490
|
+
if (Object.keys(summary.summary.tree).length > 0) {
|
|
1491
|
+
addTreeToSummary(summaryTree, blobsTreeName, summary);
|
|
1456
1492
|
}
|
|
1457
1493
|
|
|
1458
1494
|
if (this.garbageCollector.writeDataAtRoot) {
|
|
@@ -1700,6 +1736,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1700
1736
|
return;
|
|
1701
1737
|
}
|
|
1702
1738
|
|
|
1739
|
+
this.mc.logger.sendTelemetryEvent({
|
|
1740
|
+
eventName: "FlushMode Updated",
|
|
1741
|
+
old: this._flushMode,
|
|
1742
|
+
new: mode,
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1703
1745
|
// Flush any pending batches if switching to immediate
|
|
1704
1746
|
if (mode === FlushMode.Immediate) {
|
|
1705
1747
|
this.flush();
|
|
@@ -1864,6 +1906,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1864
1906
|
Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props).realize();
|
|
1865
1907
|
if (isRoot) {
|
|
1866
1908
|
fluidDataStore.bindToContext();
|
|
1909
|
+
this.logger.sendTelemetryEvent({
|
|
1910
|
+
eventName: "Root datastore with props",
|
|
1911
|
+
hasProps: props !== undefined,
|
|
1912
|
+
});
|
|
1867
1913
|
}
|
|
1868
1914
|
return channelToDataStore(fluidDataStore, id, this, this.dataStores, this.mc.logger);
|
|
1869
1915
|
}
|
|
@@ -2041,11 +2087,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2041
2087
|
gcStats = await this.collectGarbage({ logger: summaryLogger, runSweep, fullGC });
|
|
2042
2088
|
}
|
|
2043
2089
|
|
|
2044
|
-
const
|
|
2045
|
-
|
|
2090
|
+
const { stats, summary } = await this.summarizerNode.summarize(fullTree, trackState);
|
|
2091
|
+
|
|
2092
|
+
assert(summary.type === SummaryType.Tree,
|
|
2046
2093
|
0x12f /* "Container Runtime's summarize should always return a tree" */);
|
|
2047
2094
|
|
|
2048
|
-
return {
|
|
2095
|
+
return { stats, summary, gcStats };
|
|
2049
2096
|
}
|
|
2050
2097
|
|
|
2051
2098
|
/**
|
|
@@ -2144,6 +2191,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2144
2191
|
await this.deltaManager.inbound.pause();
|
|
2145
2192
|
|
|
2146
2193
|
const summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
|
|
2194
|
+
const minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
|
|
2147
2195
|
const message = `Summary @${summaryRefSeqNum}:${this.deltaManager.minimumSequenceNumber}`;
|
|
2148
2196
|
|
|
2149
2197
|
// We should be here is we haven't processed be here. If we are of if the last message's sequence number
|
|
@@ -2188,7 +2236,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2188
2236
|
|
|
2189
2237
|
let continueResult = checkContinue();
|
|
2190
2238
|
if (!continueResult.continue) {
|
|
2191
|
-
return {
|
|
2239
|
+
return {
|
|
2240
|
+
stage: "base",
|
|
2241
|
+
referenceSequenceNumber: summaryRefSeqNum,
|
|
2242
|
+
minimumSequenceNumber,
|
|
2243
|
+
error: continueResult.error,
|
|
2244
|
+
};
|
|
2192
2245
|
}
|
|
2193
2246
|
|
|
2194
2247
|
// increment summary count
|
|
@@ -2211,7 +2264,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2211
2264
|
runGC: this.garbageCollector.shouldRunGC,
|
|
2212
2265
|
});
|
|
2213
2266
|
} catch (error) {
|
|
2214
|
-
return {
|
|
2267
|
+
return {
|
|
2268
|
+
stage: "base",
|
|
2269
|
+
referenceSequenceNumber: summaryRefSeqNum,
|
|
2270
|
+
minimumSequenceNumber,
|
|
2271
|
+
error,
|
|
2272
|
+
};
|
|
2215
2273
|
}
|
|
2216
2274
|
const { summary: summaryTree, stats: partialStats } = summarizeResult;
|
|
2217
2275
|
|
|
@@ -2226,15 +2284,23 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2226
2284
|
assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
|
|
2227
2285
|
const handleCount = Object.values(dataStoreTree.tree).filter(
|
|
2228
2286
|
(value) => value.type === SummaryType.Handle).length;
|
|
2287
|
+
const gcSummaryTreeStats = summaryTree.tree[gcTreeKey]
|
|
2288
|
+
? calculateStats((summaryTree.tree[gcTreeKey] as ISummaryTree))
|
|
2289
|
+
: undefined;
|
|
2229
2290
|
|
|
2230
2291
|
const summaryStats: IGeneratedSummaryStats = {
|
|
2231
2292
|
dataStoreCount: this.dataStores.size,
|
|
2232
2293
|
summarizedDataStoreCount: this.dataStores.size - handleCount,
|
|
2233
2294
|
gcStateUpdatedDataStoreCount: summarizeResult.gcStats?.updatedDataStoreCount,
|
|
2295
|
+
gcBlobNodeCount: gcSummaryTreeStats?.blobNodeCount,
|
|
2296
|
+
gcTotalBlobsSize: gcSummaryTreeStats?.totalBlobSize,
|
|
2297
|
+
opsSizesSinceLastSummary: this.opTracker.opsSizeAccumulator,
|
|
2298
|
+
nonSystemOpsSinceLastSummary: this.opTracker.nonSystemOpCount,
|
|
2234
2299
|
...partialStats,
|
|
2235
2300
|
};
|
|
2236
2301
|
const generateSummaryData = {
|
|
2237
2302
|
referenceSequenceNumber: summaryRefSeqNum,
|
|
2303
|
+
minimumSequenceNumber,
|
|
2238
2304
|
summaryTree,
|
|
2239
2305
|
summaryStats,
|
|
2240
2306
|
generateDuration: trace.trace().duration,
|
|
@@ -2301,7 +2367,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2301
2367
|
} as const;
|
|
2302
2368
|
|
|
2303
2369
|
this.summarizerNode.completeSummary(handle);
|
|
2304
|
-
|
|
2370
|
+
this.opTracker.reset();
|
|
2305
2371
|
return submitData;
|
|
2306
2372
|
} finally {
|
|
2307
2373
|
// Cleanup wip summary in case of failure
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IDeltaManager } from "@fluidframework/container-definitions";
|
|
7
|
+
import {
|
|
8
|
+
IDocumentMessage,
|
|
9
|
+
ISequencedDocumentMessage,
|
|
10
|
+
ISequencedDocumentSystemMessage,
|
|
11
|
+
} from "@fluidframework/protocol-definitions";
|
|
12
|
+
import { isSystemMessage } from "@fluidframework/protocol-base";
|
|
13
|
+
|
|
14
|
+
export class OpTracker {
|
|
15
|
+
/**
|
|
16
|
+
* Used for storing the message content size when
|
|
17
|
+
* the message is pushed onto the inbound queue.
|
|
18
|
+
*/
|
|
19
|
+
private readonly messageSize = new Map<number, number>();
|
|
20
|
+
private _nonSystemOpCount: number = 0;
|
|
21
|
+
public get nonSystemOpCount(): number {
|
|
22
|
+
return this._nonSystemOpCount;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private _opsSizeAccumulator: number = 0;
|
|
26
|
+
public get opsSizeAccumulator(): number {
|
|
27
|
+
return this._opsSizeAccumulator;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public constructor(
|
|
31
|
+
deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
|
|
32
|
+
disabled: boolean,
|
|
33
|
+
) {
|
|
34
|
+
if (disabled) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Record the message content size when we receive it.
|
|
39
|
+
// We should not log this value, as summarization can happen between the time the message
|
|
40
|
+
// is received and until it is processed (the 'op' event).
|
|
41
|
+
deltaManager.inbound.on("push", (message: ISequencedDocumentMessage) => {
|
|
42
|
+
// Some messages my already have string contents at this point,
|
|
43
|
+
// so stringifying them again will add inaccurate overhead.
|
|
44
|
+
const messageContent = typeof message.contents === "string" ?
|
|
45
|
+
message.contents :
|
|
46
|
+
JSON.stringify(message.contents);
|
|
47
|
+
const messageData = OpTracker.messageHasData(message) ? message.data : "";
|
|
48
|
+
this.messageSize[OpTracker.messageId(message)] = messageContent.length + messageData.length;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
deltaManager.on("op", (message: ISequencedDocumentMessage) => {
|
|
52
|
+
this._nonSystemOpCount += isSystemMessage(message) ? 0 : 1;
|
|
53
|
+
const id = OpTracker.messageId(message);
|
|
54
|
+
this._opsSizeAccumulator += this.messageSize[id] ?? 0;
|
|
55
|
+
this.messageSize.delete(id);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static messageId(message: ISequencedDocumentMessage): number {
|
|
60
|
+
return message.sequenceNumber;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private static messageHasData(message: ISequencedDocumentMessage): message is ISequencedDocumentSystemMessage {
|
|
64
|
+
return (message as ISequencedDocumentSystemMessage).data !== undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public reset() {
|
|
68
|
+
this._nonSystemOpCount = 0;
|
|
69
|
+
this._opsSizeAccumulator = 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
import { IEvent, IEventProvider, ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
7
7
|
import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
|
|
8
8
|
import { IDeltaManager } from "@fluidframework/container-definitions";
|
|
9
|
+
import { UsageError } from "@fluidframework/container-utils";
|
|
9
10
|
import { IClient, IQuorumClients, ISequencedClient } from "@fluidframework/protocol-definitions";
|
|
10
11
|
import { ChildLogger } from "@fluidframework/telemetry-utils";
|
|
12
|
+
import { summarizerClientType } from "./summarizerClientElection";
|
|
11
13
|
|
|
12
14
|
// helper types for recursive readonly.
|
|
13
15
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
@@ -206,16 +208,26 @@ export interface IOrderedClientElectionEvents extends IEvent {
|
|
|
206
208
|
export interface ISerializedElection {
|
|
207
209
|
/** Sequence number at the time of the latest election. */
|
|
208
210
|
readonly electionSequenceNumber: number;
|
|
209
|
-
/** Most recently elected client id.
|
|
211
|
+
/** Most recently elected client id. This is either:
|
|
212
|
+
* 1. the interactive elected parent client, in which case electedClientId === electedParentId,
|
|
213
|
+
* and the SummaryManager on the elected client will spawn a summarizer client, or
|
|
214
|
+
* 2. the non-interactive summarizer client itself. */
|
|
210
215
|
readonly electedClientId: string | undefined;
|
|
216
|
+
/** Most recently elected parent client id. This is always an interactive client. */
|
|
217
|
+
readonly electedParentId: string | undefined;
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
/** Contract for maintaining a deterministic client election based on eligibility. */
|
|
214
221
|
export interface IOrderedClientElection extends IEventProvider<IOrderedClientElectionEvents> {
|
|
215
222
|
/** Count of eligible clients in the collection. */
|
|
216
223
|
readonly eligibleCount: number;
|
|
217
|
-
/** Currently elected client.
|
|
224
|
+
/** Currently elected client. This is either:
|
|
225
|
+
* 1. the interactive elected parent client, in which case electedClientId === electedParentId,
|
|
226
|
+
* and the SummaryManager on the elected client will spawn a summarizer client, or
|
|
227
|
+
* 2. the non-interactive summarizer client itself. */
|
|
218
228
|
readonly electedClient: ITrackedClient | undefined;
|
|
229
|
+
/** Currently elected parent client. This is always an interactive client. */
|
|
230
|
+
readonly electedParent: ITrackedClient | undefined;
|
|
219
231
|
/** Sequence number of most recent election. */
|
|
220
232
|
readonly electionSequenceNumber: number;
|
|
221
233
|
/** Marks the currently elected client as invalid, and elects the next eligible client. */
|
|
@@ -241,16 +253,50 @@ export class OrderedClientElection
|
|
|
241
253
|
implements IOrderedClientElection {
|
|
242
254
|
private _eligibleCount: number = 0;
|
|
243
255
|
private _electedClient: ILinkedClient | undefined;
|
|
256
|
+
private _electedParent: ILinkedClient | undefined;
|
|
244
257
|
private _electionSequenceNumber: number;
|
|
245
258
|
|
|
246
259
|
public get eligibleCount() {
|
|
247
260
|
return this._eligibleCount;
|
|
248
261
|
}
|
|
262
|
+
public get electionSequenceNumber() {
|
|
263
|
+
return this._electionSequenceNumber;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* OrderedClientCollection tracks electedClient and electedParent separately. This allows us to handle the case
|
|
268
|
+
* where a new interactive parent client has been elected, but the summarizer is still doing work, so
|
|
269
|
+
* a new summarizer should not yet be spawned. In this case, changing electedParent will cause SummaryManager
|
|
270
|
+
* to stop the current summarizer, but a new summarizer will not be spawned until the old summarizer client has
|
|
271
|
+
* left the quorum.
|
|
272
|
+
*
|
|
273
|
+
* Details:
|
|
274
|
+
*
|
|
275
|
+
* electedParent is the interactive client that has been elected to spawn a summarizer. It is typically the oldest
|
|
276
|
+
* eligible interactive client in the quorum. Only the electedParent is permitted to spawn a summarizer.
|
|
277
|
+
* Once elected, this client will remain the electedParent until it leaves the quorum or the summarizer that
|
|
278
|
+
* it spawned stops producing summaries, at which point a new electedParent will be chosen.
|
|
279
|
+
*
|
|
280
|
+
* electedClient is the non-interactive summarizer client if one exists. If not, then electedClient is equal to
|
|
281
|
+
* electedParent. If electedParent === electedClient, this is the signal for electedParent to spawn a new
|
|
282
|
+
* electedClient. Once a summarizer client becomes electedClient, a new summarizer will not be spawned until
|
|
283
|
+
* electedClient leaves the quorum.
|
|
284
|
+
*
|
|
285
|
+
* A typical sequence looks like this:
|
|
286
|
+
* i. Begin by electing A. electedParent === A, electedClient === A.
|
|
287
|
+
* ii. SummaryManager running on A spawns a summarizer client, A'. electedParent === A, electedClient === A'
|
|
288
|
+
* iii. A' stops producing summaries. A new parent client, B, is elected. electedParent === B, electedClient === A'
|
|
289
|
+
* iv. SummaryManager running on A detects the change to electedParent and tells the summarizer to stop, but A'
|
|
290
|
+
* is in mid-summarization. No new summarizer is spawned, as electedParent !== electedClient.
|
|
291
|
+
* v. A' completes its summary, and the summarizer and backing client are torn down.
|
|
292
|
+
* vi. A' leaves the quorum, and B takes its place as electedClient. electedParent === B, electedClient === B
|
|
293
|
+
* vii. SummaryManager running on B spawns a summarizer client, B'. electedParent === B, electedClient === B'
|
|
294
|
+
*/
|
|
249
295
|
public get electedClient() {
|
|
250
296
|
return this._electedClient;
|
|
251
297
|
}
|
|
252
|
-
public get
|
|
253
|
-
return this.
|
|
298
|
+
public get electedParent() {
|
|
299
|
+
return this._electedParent;
|
|
254
300
|
}
|
|
255
301
|
|
|
256
302
|
constructor(
|
|
@@ -262,11 +308,20 @@ export class OrderedClientElection
|
|
|
262
308
|
) {
|
|
263
309
|
super();
|
|
264
310
|
let initialClient: ILinkedClient | undefined;
|
|
311
|
+
let initialParent: ILinkedClient | undefined;
|
|
265
312
|
for (const client of orderedClientCollection.getAllClients()) {
|
|
266
313
|
this.addClient(client, 0);
|
|
267
314
|
if (typeof initialState !== "number") {
|
|
268
315
|
if (client.clientId === initialState.electedClientId) {
|
|
269
316
|
initialClient = client;
|
|
317
|
+
if (initialState.electedParentId === undefined &&
|
|
318
|
+
client.client.details.type !== summarizerClientType) {
|
|
319
|
+
// If there was no elected parent in the serialized data, use this one.
|
|
320
|
+
initialParent = client;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (client.clientId === initialState.electedParentId) {
|
|
324
|
+
initialParent = client;
|
|
270
325
|
}
|
|
271
326
|
}
|
|
272
327
|
}
|
|
@@ -288,7 +343,7 @@ export class OrderedClientElection
|
|
|
288
343
|
});
|
|
289
344
|
} else if (initialClient !== undefined && !isEligibleFn(initialClient)) {
|
|
290
345
|
// Initially elected client is ineligible, so elect next eligible client.
|
|
291
|
-
initialClient = this.
|
|
346
|
+
initialClient = initialParent = this.findFirstEligibleParent(initialParent);
|
|
292
347
|
logger.sendErrorEvent({
|
|
293
348
|
eventName: "InitialElectedClientIneligible",
|
|
294
349
|
electionSequenceNumber: initialState.electionSequenceNumber,
|
|
@@ -296,31 +351,53 @@ export class OrderedClientElection
|
|
|
296
351
|
electedClientId: initialClient?.clientId,
|
|
297
352
|
});
|
|
298
353
|
}
|
|
354
|
+
this._electedParent = initialParent;
|
|
299
355
|
this._electedClient = initialClient;
|
|
300
356
|
this._electionSequenceNumber = initialState.electionSequenceNumber;
|
|
301
357
|
}
|
|
302
358
|
}
|
|
303
359
|
|
|
304
|
-
/** Tries changing the elected client, raising an event if it is different.
|
|
360
|
+
/** Tries changing the elected client, raising an event if it is different.
|
|
361
|
+
* Note that this function does no eligibility or suitability checks. If we get here, then
|
|
362
|
+
* we will set _electedClient, and we will set _electedParent if this is an interactive client.
|
|
363
|
+
*/
|
|
305
364
|
private tryElectingClient(client: ILinkedClient | undefined, sequenceNumber: number): void {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
365
|
+
let change = false;
|
|
366
|
+
const isSummarizerClient = client?.client.details.type === summarizerClientType;
|
|
310
367
|
const prevClient = this._electedClient;
|
|
311
|
-
this._electedClient
|
|
312
|
-
|
|
368
|
+
if (this._electedClient !== client) {
|
|
369
|
+
// Changing the elected client. Record the sequence number and note that we have to fire an event.
|
|
370
|
+
this._electionSequenceNumber = sequenceNumber;
|
|
371
|
+
this._electedClient = client;
|
|
372
|
+
change = true;
|
|
373
|
+
}
|
|
374
|
+
if (this._electedParent !== client && !isSummarizerClient) {
|
|
375
|
+
// Changing the elected parent as well.
|
|
376
|
+
this._electedParent = client;
|
|
377
|
+
change = true;
|
|
378
|
+
}
|
|
379
|
+
if (change) {
|
|
380
|
+
this.emit("election", client, sequenceNumber, prevClient);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private tryElectingParent(client: ILinkedClient | undefined, sequenceNumber: number): void {
|
|
385
|
+
if (this._electedParent !== client) {
|
|
386
|
+
this._electedParent = client;
|
|
387
|
+
this.emit("election", this._electedClient, sequenceNumber, this._electedClient);
|
|
388
|
+
}
|
|
313
389
|
}
|
|
314
390
|
|
|
315
391
|
/**
|
|
316
|
-
* Helper function to find the first eligible client starting with the passed in client,
|
|
392
|
+
* Helper function to find the first eligible parent client starting with the passed in client,
|
|
317
393
|
* or undefined if none are eligible.
|
|
318
394
|
* @param client - client to start checking
|
|
319
395
|
* @returns oldest eligible client starting with passed in client or undefined if none.
|
|
320
396
|
*/
|
|
321
|
-
private
|
|
397
|
+
private findFirstEligibleParent(client: ILinkedClient | undefined): ILinkedClient | undefined {
|
|
322
398
|
let candidateClient = client;
|
|
323
|
-
while (candidateClient !== undefined &&
|
|
399
|
+
while (candidateClient !== undefined &&
|
|
400
|
+
(!this.isEligibleFn(candidateClient) || candidateClient.client.details.type === summarizerClientType)) {
|
|
324
401
|
candidateClient = candidateClient.youngerClient;
|
|
325
402
|
}
|
|
326
403
|
return candidateClient;
|
|
@@ -335,10 +412,16 @@ export class OrderedClientElection
|
|
|
335
412
|
private addClient(client: ILinkedClient, sequenceNumber: number): void {
|
|
336
413
|
if (this.isEligibleFn(client)) {
|
|
337
414
|
this._eligibleCount++;
|
|
338
|
-
|
|
339
|
-
|
|
415
|
+
const newClientIsSummarizer = client.client.details.type === summarizerClientType;
|
|
416
|
+
const electedClientIsSummarizer = this._electedClient?.client.details.type === summarizerClientType;
|
|
417
|
+
// Note that we allow a summarizer client to supercede an interactive client as elected client.
|
|
418
|
+
if (this._electedClient === undefined || (!electedClientIsSummarizer && newClientIsSummarizer)) {
|
|
340
419
|
this.tryElectingClient(client, sequenceNumber);
|
|
341
420
|
}
|
|
421
|
+
else if (this._electedParent === undefined && !newClientIsSummarizer) {
|
|
422
|
+
// This is an odd case. If the _electedClient is set, the _electedParent should be as well.
|
|
423
|
+
this.tryElectingParent(client, sequenceNumber);
|
|
424
|
+
}
|
|
342
425
|
}
|
|
343
426
|
}
|
|
344
427
|
|
|
@@ -352,9 +435,33 @@ export class OrderedClientElection
|
|
|
352
435
|
if (this.isEligibleFn(client)) {
|
|
353
436
|
this._eligibleCount--;
|
|
354
437
|
if (this._electedClient === client) {
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
438
|
+
// Removing the _electedClient. There are 2 possible cases:
|
|
439
|
+
if (this._electedParent !== client) {
|
|
440
|
+
// 1. The _electedClient is a summarizer that we've been allowing to finish its work.
|
|
441
|
+
// Let the _electedParent become the _electedClient so that it can start its own summarizer.
|
|
442
|
+
if (this._electedClient.client.details.type !== summarizerClientType) {
|
|
443
|
+
throw new UsageError("Elected client should be a summarizer client 1");
|
|
444
|
+
}
|
|
445
|
+
this.tryElectingClient(this._electedParent, sequenceNumber);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
// 2. The _electedClient is an interactive client that has left the quorum.
|
|
449
|
+
// Automatically shift to next oldest client.
|
|
450
|
+
const nextClient = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
|
|
451
|
+
this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
|
|
452
|
+
this.tryElectingClient(nextClient, sequenceNumber);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
else if (this._electedParent === client) {
|
|
456
|
+
// Removing the _electedParent (but not _electedClient).
|
|
457
|
+
// Shift to the next oldest parent, but do not replace the _electedClient,
|
|
458
|
+
// which is a summarizer that is still doing work.
|
|
459
|
+
if (this._electedClient?.client.details.type !== summarizerClientType) {
|
|
460
|
+
throw new UsageError("Elected client should be a summarizer client 2");
|
|
461
|
+
}
|
|
462
|
+
const nextParent = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
|
|
463
|
+
this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
|
|
464
|
+
this.tryElectingParent(nextParent, sequenceNumber);
|
|
358
465
|
}
|
|
359
466
|
}
|
|
360
467
|
}
|
|
@@ -363,24 +470,46 @@ export class OrderedClientElection
|
|
|
363
470
|
return this.orderedClientCollection.getAllClients().filter(this.isEligibleFn);
|
|
364
471
|
}
|
|
365
472
|
|
|
473
|
+
/** Advance election to the next-oldest client. This is called if the current parent is leaving the quorum,
|
|
474
|
+
* or if the current summarizer is not responsive and we want to stop it and spawn a new one.
|
|
475
|
+
*/
|
|
366
476
|
public incrementElectedClient(sequenceNumber: number): void {
|
|
367
|
-
const nextClient = this.
|
|
368
|
-
|
|
477
|
+
const nextClient = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
|
|
478
|
+
this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
|
|
479
|
+
if (this._electedClient === undefined || this._electedClient === this._electedParent) {
|
|
480
|
+
this.tryElectingClient(nextClient, sequenceNumber);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// The _electedClient is a summarizer and should not be replaced until it leaves the quorum.
|
|
484
|
+
// Changing the _electedParent will stop the summarizer.
|
|
485
|
+
this.tryElectingParent(nextClient, sequenceNumber);
|
|
486
|
+
}
|
|
369
487
|
}
|
|
370
488
|
|
|
489
|
+
/** (Re-)start election with the oldest client in the quorum. This is called if we need to summarize
|
|
490
|
+
* and no client has been elected.
|
|
491
|
+
*/
|
|
371
492
|
public resetElectedClient(sequenceNumber: number): void {
|
|
372
|
-
const firstClient = this.
|
|
373
|
-
this.
|
|
493
|
+
const firstClient = this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
|
|
494
|
+
if (this._electedClient === undefined || this._electedClient === this._electedParent) {
|
|
495
|
+
this.tryElectingClient(firstClient, sequenceNumber);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
// The _electedClient is a summarizer and should not be replaced until it leaves the quorum.
|
|
499
|
+
// Changing the _electedParent will stop the summarizer.
|
|
500
|
+
this.tryElectingParent(firstClient, sequenceNumber);
|
|
501
|
+
}
|
|
374
502
|
}
|
|
375
503
|
|
|
376
504
|
public peekNextElectedClient(): ITrackedClient | undefined {
|
|
377
|
-
return this.
|
|
505
|
+
return this.findFirstEligibleParent(this._electedParent?.youngerClient);
|
|
378
506
|
}
|
|
379
507
|
|
|
380
508
|
public serialize(): ISerializedElection {
|
|
381
509
|
return {
|
|
382
510
|
electionSequenceNumber: this.electionSequenceNumber,
|
|
383
511
|
electedClientId: this.electedClient?.clientId,
|
|
512
|
+
electedParentId: this.electedParent?.clientId,
|
|
384
513
|
};
|
|
385
514
|
}
|
|
386
515
|
}
|
package/src/packageVersion.ts
CHANGED