@fluidframework/container-runtime 0.59.4002 → 1.0.2
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 +1 -1
- package/dist/blobManager.d.ts +2 -2
- package/dist/blobManager.d.ts.map +1 -1
- package/dist/blobManager.js +12 -11
- package/dist/blobManager.js.map +1 -1
- package/dist/connectionTelemetry.js +3 -3
- package/dist/connectionTelemetry.js.map +1 -1
- package/dist/containerRuntime.d.ts +125 -29
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +242 -110
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts +4 -2
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +16 -5
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +4 -3
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +9 -3
- package/dist/dataStores.js.map +1 -1
- package/dist/garbageCollection.d.ts +3 -3
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +10 -8
- package/dist/garbageCollection.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/orderedClientElection.js +0 -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 +30 -29
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +72 -109
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runningSummarizer.d.ts +4 -3
- package/dist/runningSummarizer.d.ts.map +1 -1
- package/dist/runningSummarizer.js +11 -6
- package/dist/runningSummarizer.js.map +1 -1
- package/dist/serializedSnapshotStorage.d.ts +58 -0
- package/dist/serializedSnapshotStorage.d.ts.map +1 -0
- package/dist/serializedSnapshotStorage.js +108 -0
- package/dist/serializedSnapshotStorage.js.map +1 -0
- package/dist/summarizer.d.ts +11 -4
- package/dist/summarizer.d.ts.map +1 -1
- package/dist/summarizer.js +18 -9
- package/dist/summarizer.js.map +1 -1
- package/dist/summarizerHeuristics.d.ts +5 -3
- package/dist/summarizerHeuristics.d.ts.map +1 -1
- package/dist/summarizerHeuristics.js +10 -3
- package/dist/summarizerHeuristics.js.map +1 -1
- package/dist/summarizerTypes.d.ts +4 -2
- package/dist/summarizerTypes.d.ts.map +1 -1
- package/dist/summarizerTypes.js.map +1 -1
- package/dist/summaryManager.d.ts +3 -3
- package/dist/summaryManager.d.ts.map +1 -1
- package/dist/summaryManager.js +7 -7
- package/dist/summaryManager.js.map +1 -1
- package/garbageCollection.md +9 -1
- package/lib/blobManager.d.ts +2 -2
- package/lib/blobManager.d.ts.map +1 -1
- package/lib/blobManager.js +12 -11
- package/lib/blobManager.js.map +1 -1
- package/lib/connectionTelemetry.js +3 -3
- package/lib/connectionTelemetry.js.map +1 -1
- package/lib/containerRuntime.d.ts +125 -29
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +243 -111
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStoreContext.d.ts +4 -2
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +16 -5
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStores.d.ts +4 -3
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +9 -3
- package/lib/dataStores.js.map +1 -1
- package/lib/garbageCollection.d.ts +3 -3
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +10 -8
- package/lib/garbageCollection.js.map +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/orderedClientElection.js +0 -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 +30 -29
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +72 -109
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runningSummarizer.d.ts +4 -3
- package/lib/runningSummarizer.d.ts.map +1 -1
- package/lib/runningSummarizer.js +11 -6
- package/lib/runningSummarizer.js.map +1 -1
- package/lib/serializedSnapshotStorage.d.ts +58 -0
- package/lib/serializedSnapshotStorage.d.ts.map +1 -0
- package/lib/serializedSnapshotStorage.js +104 -0
- package/lib/serializedSnapshotStorage.js.map +1 -0
- package/lib/summarizer.d.ts +11 -4
- package/lib/summarizer.d.ts.map +1 -1
- package/lib/summarizer.js +18 -9
- package/lib/summarizer.js.map +1 -1
- package/lib/summarizerHeuristics.d.ts +5 -3
- package/lib/summarizerHeuristics.d.ts.map +1 -1
- package/lib/summarizerHeuristics.js +10 -3
- package/lib/summarizerHeuristics.js.map +1 -1
- package/lib/summarizerTypes.d.ts +4 -2
- package/lib/summarizerTypes.d.ts.map +1 -1
- package/lib/summarizerTypes.js.map +1 -1
- package/lib/summaryManager.d.ts +3 -3
- package/lib/summaryManager.d.ts.map +1 -1
- package/lib/summaryManager.js +7 -7
- package/lib/summaryManager.js.map +1 -1
- package/package.json +46 -31
- package/src/blobManager.ts +29 -15
- package/src/connectionTelemetry.ts +3 -3
- package/src/containerRuntime.ts +388 -135
- package/src/dataStoreContext.ts +27 -5
- package/src/dataStores.ts +15 -3
- package/src/garbageCollection.ts +21 -14
- package/src/index.ts +7 -1
- package/src/orderedClientElection.ts +1 -1
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +104 -123
- package/src/runningSummarizer.ts +20 -10
- package/src/serializedSnapshotStorage.ts +146 -0
- package/src/summarizer.ts +20 -16
- package/src/summarizerHeuristics.ts +21 -5
- package/src/summarizerTypes.ts +4 -2
- package/src/summaryManager.ts +5 -6
package/src/dataStoreContext.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
ISummarizeResult,
|
|
59
59
|
ISummarizerNodeWithGC,
|
|
60
60
|
SummarizeInternalFn,
|
|
61
|
+
ITelemetryContext,
|
|
61
62
|
} from "@fluidframework/runtime-definitions";
|
|
62
63
|
import { addBlobToSummary, convertSummaryTreeToITree } from "@fluidframework/runtime-utils";
|
|
63
64
|
import {
|
|
@@ -289,7 +290,8 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
289
290
|
};
|
|
290
291
|
|
|
291
292
|
const thisSummarizeInternal =
|
|
292
|
-
async (fullTree: boolean, trackState: boolean) =>
|
|
293
|
+
async (fullTree: boolean, trackState: boolean, telemetryContext?: ITelemetryContext) =>
|
|
294
|
+
this.summarizeInternal(fullTree, trackState, telemetryContext);
|
|
293
295
|
|
|
294
296
|
this.summarizerNode = props.createSummarizerNodeFn(
|
|
295
297
|
thisSummarizeInternal,
|
|
@@ -445,16 +447,25 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
445
447
|
* Returns a summary at the current sequence number.
|
|
446
448
|
* @param fullTree - true to bypass optimizations and force a full summary tree
|
|
447
449
|
* @param trackState - This tells whether we should track state from this summary.
|
|
450
|
+
* @param telemetryContext - summary data passed through the layers for telemetry purposes
|
|
448
451
|
*/
|
|
449
|
-
public async summarize(
|
|
450
|
-
|
|
452
|
+
public async summarize(
|
|
453
|
+
fullTree: boolean = false,
|
|
454
|
+
trackState: boolean = true,
|
|
455
|
+
telemetryContext?: ITelemetryContext,
|
|
456
|
+
): Promise<ISummarizeResult> {
|
|
457
|
+
return this.summarizerNode.summarize(fullTree, trackState, telemetryContext);
|
|
451
458
|
}
|
|
452
459
|
|
|
453
|
-
private async summarizeInternal(
|
|
460
|
+
private async summarizeInternal(
|
|
461
|
+
fullTree: boolean,
|
|
462
|
+
trackState: boolean,
|
|
463
|
+
telemetryContext?: ITelemetryContext,
|
|
464
|
+
): Promise<ISummarizeInternalResult> {
|
|
454
465
|
await this.realize();
|
|
455
466
|
|
|
456
467
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
457
|
-
const summarizeResult = await this.channel!.summarize(fullTree, trackState);
|
|
468
|
+
const summarizeResult = await this.channel!.summarize(fullTree, trackState, telemetryContext);
|
|
458
469
|
let pathPartsForChildren: string[] | undefined;
|
|
459
470
|
|
|
460
471
|
if (!this.disableIsolatedChannels) {
|
|
@@ -717,6 +728,17 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
717
728
|
this.channel.reSubmit(innerContents.type, innerContents.content, localOpMetadata);
|
|
718
729
|
}
|
|
719
730
|
|
|
731
|
+
public rollback(contents: any, localOpMetadata: unknown) {
|
|
732
|
+
if (!this.channel) {
|
|
733
|
+
throw new Error("Channel must exist when rolling back ops");
|
|
734
|
+
}
|
|
735
|
+
if (!this.channel.rollback) {
|
|
736
|
+
throw new Error("Channel doesn't support rollback");
|
|
737
|
+
}
|
|
738
|
+
const innerContents = contents as FluidDataStoreMessage;
|
|
739
|
+
this.channel.rollback(innerContents.type, innerContents.content, localOpMetadata);
|
|
740
|
+
}
|
|
741
|
+
|
|
720
742
|
public async applyStashedOp(contents: any): Promise<unknown> {
|
|
721
743
|
if (!this.channel) {
|
|
722
744
|
await this.realize();
|
package/src/dataStores.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
InboundAttachMessage,
|
|
26
26
|
ISummarizeResult,
|
|
27
27
|
ISummaryTreeWithStats,
|
|
28
|
+
ITelemetryContext,
|
|
28
29
|
} from "@fluidframework/runtime-definitions";
|
|
29
30
|
import {
|
|
30
31
|
convertSnapshotTreeToSummaryTree,
|
|
@@ -380,6 +381,13 @@ export class DataStores implements IDisposable {
|
|
|
380
381
|
context.reSubmit(envelope.contents, localOpMetadata);
|
|
381
382
|
}
|
|
382
383
|
|
|
384
|
+
public rollbackDataStoreOp(content: any, localOpMetadata: unknown) {
|
|
385
|
+
const envelope = content as IEnvelope;
|
|
386
|
+
const context = this.contexts.get(envelope.address);
|
|
387
|
+
assert(!!context, 0x2e8 /* "There should be a store context for the op" */);
|
|
388
|
+
context.rollback(envelope.contents, localOpMetadata);
|
|
389
|
+
}
|
|
390
|
+
|
|
383
391
|
public async applyStashedOp(content: any): Promise<unknown> {
|
|
384
392
|
const envelope = content as IEnvelope;
|
|
385
393
|
const context = this.contexts.get(envelope.address);
|
|
@@ -473,7 +481,11 @@ export class DataStores implements IDisposable {
|
|
|
473
481
|
return this.contexts.size;
|
|
474
482
|
}
|
|
475
483
|
|
|
476
|
-
public async summarize(
|
|
484
|
+
public async summarize(
|
|
485
|
+
fullTree: boolean,
|
|
486
|
+
trackState: boolean,
|
|
487
|
+
telemetryContext?: ITelemetryContext,
|
|
488
|
+
): Promise<ISummaryTreeWithStats> {
|
|
477
489
|
const summaryBuilder = new SummaryTreeBuilder();
|
|
478
490
|
|
|
479
491
|
// Iterate over each store and ask it to snapshot
|
|
@@ -484,14 +496,14 @@ export class DataStores implements IDisposable {
|
|
|
484
496
|
0x165 /* "Summarizer cannot work if client has local changes" */);
|
|
485
497
|
return context.attachState === AttachState.Attached;
|
|
486
498
|
}).map(async ([contextId, context]) => {
|
|
487
|
-
const contextSummary = await context.summarize(fullTree, trackState);
|
|
499
|
+
const contextSummary = await context.summarize(fullTree, trackState, telemetryContext);
|
|
488
500
|
summaryBuilder.addWithStats(contextId, contextSummary);
|
|
489
501
|
}));
|
|
490
502
|
|
|
491
503
|
return summaryBuilder.getSummaryTree();
|
|
492
504
|
}
|
|
493
505
|
|
|
494
|
-
public createSummary(): ISummaryTreeWithStats {
|
|
506
|
+
public createSummary(telemetryContext?: ITelemetryContext): ISummaryTreeWithStats {
|
|
495
507
|
const builder = new SummaryTreeBuilder();
|
|
496
508
|
// Attaching graph of some stores can cause other stores to get bound too.
|
|
497
509
|
// So keep taking summary until no new stores get bound.
|
package/src/garbageCollection.ts
CHANGED
|
@@ -22,8 +22,9 @@ import {
|
|
|
22
22
|
IGarbageCollectionData,
|
|
23
23
|
IGarbageCollectionState,
|
|
24
24
|
IGarbageCollectionDetailsBase,
|
|
25
|
-
IGarbageCollectionNodeData,
|
|
26
25
|
ISummarizeResult,
|
|
26
|
+
ITelemetryContext,
|
|
27
|
+
IGarbageCollectionNodeData,
|
|
27
28
|
} from "@fluidframework/runtime-definitions";
|
|
28
29
|
import {
|
|
29
30
|
mergeStats,
|
|
@@ -162,7 +163,11 @@ export interface IGarbageCollector {
|
|
|
162
163
|
options: { logger?: ITelemetryLogger; runGC?: boolean; runSweep?: boolean; fullGC?: boolean; },
|
|
163
164
|
): Promise<IGCStats>;
|
|
164
165
|
/** Summarizes the GC data and returns it as a summary tree. */
|
|
165
|
-
summarize(
|
|
166
|
+
summarize(
|
|
167
|
+
fullTree: boolean,
|
|
168
|
+
trackState: boolean,
|
|
169
|
+
telemetryContext?: ITelemetryContext,
|
|
170
|
+
): ISummarizeResult | undefined;
|
|
166
171
|
/** Returns the garbage collector specific metadata to be written into the summary. */
|
|
167
172
|
getMetadata(): IGCMetadata;
|
|
168
173
|
/** Returns a map of each node id to its base GC details in the base summary. */
|
|
@@ -330,7 +335,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
330
335
|
private _writeDataAtRoot: boolean = false;
|
|
331
336
|
public get writeDataAtRoot(): boolean {
|
|
332
337
|
return this._writeDataAtRoot;
|
|
333
|
-
|
|
338
|
+
}
|
|
334
339
|
|
|
335
340
|
/**
|
|
336
341
|
* Tells whether the initial GC state needs to be reset. This can happen under 2 conditions:
|
|
@@ -420,12 +425,14 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
420
425
|
} else {
|
|
421
426
|
// Sweep should not be enabled without enabling GC mark phase. We could silently disable sweep in this
|
|
422
427
|
// scenario but explicitly failing makes it clearer and promotes correct usage.
|
|
423
|
-
if (gcOptions.sweepAllowed &&
|
|
428
|
+
if (gcOptions.sweepAllowed && gcOptions.gcAllowed === false) {
|
|
424
429
|
throw new UsageError("GC sweep phase cannot be enabled without enabling GC mark phase");
|
|
425
430
|
}
|
|
426
431
|
|
|
427
|
-
// For new documents, GC
|
|
428
|
-
|
|
432
|
+
// For new documents, GC is enabled by default. It can be explicitly disabled by setting the gcAllowed
|
|
433
|
+
// flag in GC options to false.
|
|
434
|
+
this.gcEnabled = gcOptions.gcAllowed !== false;
|
|
435
|
+
// The sweep phase has to be explicitly enabled by setting the sweepAllowed flag in GC options to true.
|
|
429
436
|
this.sweepEnabled = gcOptions.sweepAllowed === true;
|
|
430
437
|
|
|
431
438
|
// Set the Session Expiry only if the flag is enabled or the test option is set.
|
|
@@ -588,7 +595,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
588
595
|
return;
|
|
589
596
|
}
|
|
590
597
|
|
|
591
|
-
const gcNodes: { [
|
|
598
|
+
const gcNodes: { [id: string]: string[]; } = {};
|
|
592
599
|
for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
|
|
593
600
|
if (nodeData.unreferencedTimestampMs !== undefined) {
|
|
594
601
|
this.unreferencedNodesState.set(
|
|
@@ -613,7 +620,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
613
620
|
return new Map();
|
|
614
621
|
}
|
|
615
622
|
|
|
616
|
-
const gcNodes: { [
|
|
623
|
+
const gcNodes: { [id: string]: string[]; } = {};
|
|
617
624
|
for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
|
|
618
625
|
gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
|
|
619
626
|
}
|
|
@@ -737,8 +744,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
737
744
|
this.completedRuns++;
|
|
738
745
|
|
|
739
746
|
return gcStats;
|
|
740
|
-
},
|
|
741
|
-
{ end: true, cancel: "error" });
|
|
747
|
+
}, { end: true, cancel: "error" });
|
|
742
748
|
}
|
|
743
749
|
|
|
744
750
|
/**
|
|
@@ -749,6 +755,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
749
755
|
public summarize(
|
|
750
756
|
fullTree: boolean,
|
|
751
757
|
trackState: boolean,
|
|
758
|
+
telemetryContext?: ITelemetryContext,
|
|
752
759
|
): ISummarizeResult | undefined {
|
|
753
760
|
if (!this.shouldRunGC || this.previousGCDataFromLastRun === undefined) {
|
|
754
761
|
return;
|
|
@@ -1338,16 +1345,16 @@ function meetsMinimumVersionRequirement(currentVersion: string, minimumVersion:
|
|
|
1338
1345
|
*/
|
|
1339
1346
|
export function semverCompare(currentVersion: string, minimumVersion: string): number {
|
|
1340
1347
|
const minimumValues = minimumVersion.split(".").map((value): number => {
|
|
1341
|
-
assert(isNaN(+value) === false,
|
|
1348
|
+
assert(isNaN(+value) === false, 0x2f8 /* Expected real numbers in minimum version! */);
|
|
1342
1349
|
return Number.parseInt(value, 10);
|
|
1343
1350
|
});
|
|
1344
|
-
assert(minimumValues.length === 3,
|
|
1351
|
+
assert(minimumValues.length === 3, 0x2f9 /* Expected minimumVersion to be [major].[minor].[patch] */);
|
|
1345
1352
|
const [minMajor, minMinor, minPatch] = minimumValues;
|
|
1346
1353
|
|
|
1347
1354
|
const currentValuesString = currentVersion.split(/\W/);
|
|
1348
|
-
assert(currentValuesString.length >= 3,
|
|
1355
|
+
assert(currentValuesString.length >= 3, 0x2fa /* Expected version to match semver rules! */);
|
|
1349
1356
|
const currentValues = currentValuesString.slice(0, 3).map((value) => {
|
|
1350
|
-
assert(isNaN(+value) === false,
|
|
1357
|
+
assert(isNaN(+value) === false, 0x2fb /* Expected real numbers in minimum version! */);
|
|
1351
1358
|
return Number.parseInt(value, 10);
|
|
1352
1359
|
});
|
|
1353
1360
|
const [cMajor, cMinor, cPatch] = currentValues;
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,12 @@ export {
|
|
|
9
9
|
ContainerRuntimeMessage,
|
|
10
10
|
IGCRuntimeOptions,
|
|
11
11
|
ISummaryRuntimeOptions,
|
|
12
|
+
ISummaryBaseConfiguration,
|
|
13
|
+
ISummaryConfigurationHeuristics,
|
|
14
|
+
ISummaryConfigurationDisableSummarizer,
|
|
15
|
+
ISummaryConfigurationDisableHeuristics,
|
|
12
16
|
IContainerRuntimeOptions,
|
|
17
|
+
IPendingRuntimeState,
|
|
13
18
|
IRootSummaryTreeWithStats,
|
|
14
19
|
isRuntimeMessage,
|
|
15
20
|
RuntimeMessage,
|
|
@@ -18,6 +23,8 @@ export {
|
|
|
18
23
|
agentSchedulerId,
|
|
19
24
|
ContainerRuntime,
|
|
20
25
|
RuntimeHeaders,
|
|
26
|
+
ISummaryConfiguration,
|
|
27
|
+
DefaultSummaryConfiguration,
|
|
21
28
|
} from "./containerRuntime";
|
|
22
29
|
export { DeltaScheduler } from "./deltaScheduler";
|
|
23
30
|
export { FluidDataStoreRegistry } from "./dataStoreRegistry";
|
|
@@ -55,7 +62,6 @@ export {
|
|
|
55
62
|
ISummarizer,
|
|
56
63
|
ISummarizerEvents,
|
|
57
64
|
ISummarizerInternalsProvider,
|
|
58
|
-
ISummarizerOptions,
|
|
59
65
|
ISummarizerRuntime,
|
|
60
66
|
ISummarizingWarning,
|
|
61
67
|
ISummaryCancellationToken,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
/* eslint-disable @rushstack/no-new-null */
|
|
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";
|
package/src/packageVersion.ts
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
import { IDisposable } from "@fluidframework/common-definitions";
|
|
7
7
|
import { assert, Lazy } from "@fluidframework/common-utils";
|
|
8
|
+
import { ICriticalContainerError } from "@fluidframework/container-definitions";
|
|
8
9
|
import { DataProcessingError } from "@fluidframework/container-utils";
|
|
9
10
|
import {
|
|
10
11
|
ISequencedDocumentMessage,
|
|
11
12
|
} from "@fluidframework/protocol-definitions";
|
|
12
13
|
import { FlushMode } from "@fluidframework/runtime-definitions";
|
|
14
|
+
import { wrapError } from "@fluidframework/telemetry-utils";
|
|
13
15
|
import Deque from "double-ended-queue";
|
|
14
|
-
import {
|
|
16
|
+
import { ContainerMessageType } from "./containerRuntime";
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* This represents a message that has been submitted and is added to the pending queue when `submit` is called on the
|
|
@@ -47,16 +49,31 @@ export interface IPendingFlush {
|
|
|
47
49
|
export type IPendingState = IPendingMessage | IPendingFlushMode | IPendingFlush;
|
|
48
50
|
|
|
49
51
|
export interface IPendingLocalState {
|
|
50
|
-
/**
|
|
51
|
-
* client ID we most recently connected with, or undefined if we never connected
|
|
52
|
-
*/
|
|
53
|
-
clientId?: string;
|
|
54
52
|
/**
|
|
55
53
|
* list of pending states, including ops and batch information
|
|
56
54
|
*/
|
|
57
55
|
pendingStates: IPendingState[];
|
|
58
56
|
}
|
|
59
57
|
|
|
58
|
+
export interface IRuntimeStateHandler{
|
|
59
|
+
connected(): boolean;
|
|
60
|
+
clientId(): string | undefined;
|
|
61
|
+
flushMode(): FlushMode;
|
|
62
|
+
setFlushMode(mode: FlushMode): void;
|
|
63
|
+
close(error?: ICriticalContainerError): void;
|
|
64
|
+
applyStashedOp: (type: ContainerMessageType, content: ISequencedDocumentMessage) => Promise<unknown>;
|
|
65
|
+
flush(): void;
|
|
66
|
+
reSubmit(
|
|
67
|
+
type: ContainerMessageType,
|
|
68
|
+
content: any,
|
|
69
|
+
localOpMetadata: unknown,
|
|
70
|
+
opMetadata: Record<string, unknown> | undefined): void;
|
|
71
|
+
rollback(
|
|
72
|
+
type: ContainerMessageType,
|
|
73
|
+
content: any,
|
|
74
|
+
localOpMetadata: unknown): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
/**
|
|
61
78
|
* PendingStateManager is responsible for maintaining the messages that have not been sent or have not yet been
|
|
62
79
|
* acknowledged by the server. It also maintains the batch information for both automatically and manually flushed
|
|
@@ -69,15 +86,16 @@ export interface IPendingLocalState {
|
|
|
69
86
|
export class PendingStateManager implements IDisposable {
|
|
70
87
|
private readonly pendingStates = new Deque<IPendingState>();
|
|
71
88
|
private readonly initialStates: Deque<IPendingState>;
|
|
72
|
-
private readonly previousClientIds = new Set<string>();
|
|
73
|
-
private readonly firstStashedCSN: number = -1;
|
|
74
89
|
private readonly disposeOnce = new Lazy<void>(() => {
|
|
75
90
|
this.initialStates.clear();
|
|
76
91
|
this.pendingStates.clear();
|
|
77
92
|
});
|
|
78
93
|
|
|
79
94
|
// Maintains the count of messages that are currently unacked.
|
|
80
|
-
private
|
|
95
|
+
private _pendingMessagesCount: number = 0;
|
|
96
|
+
public get pendingMessagesCount(): number {
|
|
97
|
+
return this._pendingMessagesCount;
|
|
98
|
+
}
|
|
81
99
|
|
|
82
100
|
// Indicates whether we are processing a batch.
|
|
83
101
|
private isProcessingBatch: boolean = false;
|
|
@@ -95,22 +113,18 @@ export class PendingStateManager implements IDisposable {
|
|
|
95
113
|
|
|
96
114
|
private clientId: string | undefined;
|
|
97
115
|
|
|
98
|
-
private get connected(): boolean {
|
|
99
|
-
return this.containerRuntime.connected;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
116
|
/**
|
|
103
117
|
* Called to check if there are any pending messages in the pending state queue.
|
|
104
118
|
* @returns A boolean indicating whether there are messages or not.
|
|
105
119
|
*/
|
|
106
120
|
public hasPendingMessages(): boolean {
|
|
107
|
-
return this.
|
|
121
|
+
return this._pendingMessagesCount !== 0 || !this.initialStates.isEmpty();
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
public getLocalState(): IPendingLocalState | undefined {
|
|
125
|
+
assert(this.initialStates.isEmpty(), 0x2e9 /* "Must call getLocalState() after applying initial states" */);
|
|
111
126
|
if (this.hasPendingMessages()) {
|
|
112
127
|
return {
|
|
113
|
-
clientId: this.clientId,
|
|
114
128
|
pendingStates: this.pendingStates.toArray().map(
|
|
115
129
|
// delete localOpMetadata since it may not be serializable
|
|
116
130
|
// and will be regenerated by applyStashedOp()
|
|
@@ -120,23 +134,12 @@ export class PendingStateManager implements IDisposable {
|
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
constructor(
|
|
123
|
-
private readonly
|
|
124
|
-
private readonly applyStashedOp: (type, content) => Promise<unknown>,
|
|
137
|
+
private readonly stateHandler: IRuntimeStateHandler,
|
|
125
138
|
initialFlushMode: FlushMode,
|
|
126
139
|
initialLocalState: IPendingLocalState | undefined,
|
|
127
140
|
) {
|
|
128
141
|
this.initialStates = new Deque<IPendingState>(initialLocalState?.pendingStates ?? []);
|
|
129
142
|
|
|
130
|
-
if (initialLocalState) {
|
|
131
|
-
if (initialLocalState?.clientId) {
|
|
132
|
-
this.previousClientIds.add(initialLocalState.clientId);
|
|
133
|
-
}
|
|
134
|
-
// get stashed op count and client sequence number of first op
|
|
135
|
-
const messages = initialLocalState.pendingStates
|
|
136
|
-
.filter((state) => state.type === "message") as IPendingMessage[];
|
|
137
|
-
this.firstStashedCSN = messages[0].clientSequenceNumber;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
143
|
this.flushModeForNextMessage = initialFlushMode;
|
|
141
144
|
this.onFlushModeUpdated(initialFlushMode);
|
|
142
145
|
}
|
|
@@ -172,7 +175,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
172
175
|
|
|
173
176
|
this.pendingStates.push(pendingMessage);
|
|
174
177
|
|
|
175
|
-
this.
|
|
178
|
+
this._pendingMessagesCount++;
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
/**
|
|
@@ -189,7 +192,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
189
192
|
public onFlush() {
|
|
190
193
|
// If the FlushMode is Immediate, we don't need to track an explicit flush call because every message is
|
|
191
194
|
// automatically flushed. So, flush is a no-op.
|
|
192
|
-
if (this.
|
|
195
|
+
if (this.stateHandler.flushMode() === FlushMode.Immediate) {
|
|
193
196
|
return;
|
|
194
197
|
}
|
|
195
198
|
|
|
@@ -205,21 +208,25 @@ export class PendingStateManager implements IDisposable {
|
|
|
205
208
|
|
|
206
209
|
/**
|
|
207
210
|
* Applies stashed ops at their reference sequence number so they are ready to be ACKed or resubmitted
|
|
211
|
+
* @param seqNum - Sequence number at which to apply ops. Will apply all ops if seqNum is undefined.
|
|
208
212
|
*/
|
|
209
|
-
public async applyStashedOpsAt(seqNum
|
|
213
|
+
public async applyStashedOpsAt(seqNum?: number) {
|
|
210
214
|
// apply stashed ops at sequence number
|
|
211
215
|
while (!this.initialStates.isEmpty()) {
|
|
212
216
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
213
217
|
const nextState = this.initialStates.peekFront()!;
|
|
214
218
|
if (nextState.type === "message") {
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
if (seqNum !== undefined) {
|
|
220
|
+
if (nextState.referenceSequenceNumber > seqNum) {
|
|
221
|
+
break; // nothing left to do at this sequence number
|
|
222
|
+
} else if (nextState.referenceSequenceNumber < seqNum) {
|
|
223
|
+
throw new Error("loaded from snapshot too recent to apply stashed ops");
|
|
224
|
+
}
|
|
219
225
|
}
|
|
220
226
|
|
|
221
227
|
// applyStashedOp will cause the DDS to behave as if it has sent the op but not actually send it
|
|
222
|
-
const localOpMetadata =
|
|
228
|
+
const localOpMetadata =
|
|
229
|
+
await this.stateHandler.applyStashedOp(nextState.messageType, nextState.content);
|
|
223
230
|
nextState.localOpMetadata = localOpMetadata;
|
|
224
231
|
}
|
|
225
232
|
|
|
@@ -229,88 +236,12 @@ export class PendingStateManager implements IDisposable {
|
|
|
229
236
|
}
|
|
230
237
|
}
|
|
231
238
|
|
|
232
|
-
/**
|
|
233
|
-
* Processes a local message once it's ack'd by the server to verify that there was no data corruption and that
|
|
234
|
-
* the batch information was preserved for batch messages. Also process remote messages that might have been
|
|
235
|
-
* sent from a previous container.
|
|
236
|
-
* @param message - The message that got ack'd and needs to be processed.
|
|
237
|
-
*/
|
|
238
|
-
public processMessage(message: ISequencedDocumentMessage, local: boolean) {
|
|
239
|
-
// Do not process chunked ops until all pieces are available.
|
|
240
|
-
if (message.type === ContainerMessageType.ChunkedOp) {
|
|
241
|
-
return { localAck: false, localOpMetadata: undefined };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (local) {
|
|
245
|
-
return { localAck: false, localOpMetadata: this.processPendingLocalMessage(message) };
|
|
246
|
-
} else {
|
|
247
|
-
return this.processRemoteMessage(message);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Listens for ACKs of stashed ops
|
|
253
|
-
*/
|
|
254
|
-
private processRemoteMessage(message: ISequencedDocumentMessage) {
|
|
255
|
-
if (!isRuntimeMessage(message)) {
|
|
256
|
-
return { localAck: false, localOpMetadata: undefined };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// this message was a pending op that was actually sent successfully
|
|
260
|
-
const isOriginalClientId = message.clientId === Array.from(this.previousClientIds)[0] &&
|
|
261
|
-
message.clientSequenceNumber >= this.firstStashedCSN;
|
|
262
|
-
// this message is a pending or stashed op that was resubmitted
|
|
263
|
-
const isNewClientId = Array.from(this.previousClientIds).indexOf(message.clientId) > 0;
|
|
264
|
-
|
|
265
|
-
// if this is an ack for a stashed op, dequeue one message.
|
|
266
|
-
// we should have seen its ref seq num by now and the DDS should be ready for it to be ACKed
|
|
267
|
-
if (isOriginalClientId || isNewClientId) {
|
|
268
|
-
assert(this.clientId === undefined, 0x28b /* "multiple clients connected with stashed ops" */);
|
|
269
|
-
while (!this.pendingStates.isEmpty()) {
|
|
270
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
271
|
-
const nextState = this.pendingStates.shift()!;
|
|
272
|
-
// if it's not a message just drop it and keep looking
|
|
273
|
-
if (nextState.type === "message") {
|
|
274
|
-
this.assertOpMatch(nextState, message, isOriginalClientId);
|
|
275
|
-
return { localAck: true, localOpMetadata: nextState.localOpMetadata };
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (message.type === ContainerMessageType.Rejoin && this.previousClientIds.has(message.contents?.clientId)) {
|
|
281
|
-
this.previousClientIds.add(message.clientId);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return { localAck: false, localOpMetadata: undefined };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private assertOpMatch(state: IPendingMessage, message: ISequencedDocumentMessage, isOriginalClientId: boolean) {
|
|
288
|
-
assert(message.type === state.messageType, 0x28c /* "different message type" */);
|
|
289
|
-
assert(message.clientSequenceNumber === state.clientSequenceNumber || !isOriginalClientId,
|
|
290
|
-
0x28d /* "client sequence number doesn't match" */);
|
|
291
|
-
switch (message.type) {
|
|
292
|
-
case ContainerMessageType.Attach:
|
|
293
|
-
assert(message.contents.id === state.content.id, 0x28e /* "datastore ID doesn't match" */);
|
|
294
|
-
break;
|
|
295
|
-
case ContainerMessageType.FluidDataStoreOp:
|
|
296
|
-
assert(message.contents.address === state.content.address, 0x28f /* "address doesn't match" */);
|
|
297
|
-
break;
|
|
298
|
-
case ContainerMessageType.BlobAttach:
|
|
299
|
-
// todo: assert we have blob storage, assert blob IDs match, remove blob from blob storage since it made
|
|
300
|
-
// it through successfully
|
|
301
|
-
break;
|
|
302
|
-
case ContainerMessageType.Rejoin:
|
|
303
|
-
default:
|
|
304
|
-
throw new Error(`${message.type} not expected`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
239
|
/**
|
|
309
240
|
* Processes a local message once its ack'd by the server. It verifies that there was no data corruption and that
|
|
310
241
|
* the batch information was preserved for batch messages.
|
|
311
242
|
* @param message - The message that got ack'd and needs to be processed.
|
|
312
243
|
*/
|
|
313
|
-
|
|
244
|
+
public processPendingLocalMessage(message: ISequencedDocumentMessage): unknown {
|
|
314
245
|
// Pre-processing part - This may be the start of a batch.
|
|
315
246
|
this.maybeProcessBatchBegin(message);
|
|
316
247
|
|
|
@@ -330,11 +261,11 @@ export class PendingStateManager implements IDisposable {
|
|
|
330
261
|
{ expectedClientSequenceNumber: pendingState.clientSequenceNumber },
|
|
331
262
|
);
|
|
332
263
|
|
|
333
|
-
this.
|
|
264
|
+
this.stateHandler.close(error);
|
|
334
265
|
return;
|
|
335
266
|
}
|
|
336
267
|
|
|
337
|
-
this.
|
|
268
|
+
this._pendingMessagesCount--;
|
|
338
269
|
|
|
339
270
|
// Post-processing part - If we are processing a batch then this could be the last message in the batch.
|
|
340
271
|
this.maybeProcessBatchEnd(message);
|
|
@@ -446,6 +377,31 @@ export class PendingStateManager implements IDisposable {
|
|
|
446
377
|
this.isProcessingBatch = false;
|
|
447
378
|
}
|
|
448
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Capture the pending state at this point
|
|
382
|
+
*/
|
|
383
|
+
public checkpoint() {
|
|
384
|
+
const checkpointHead = this.pendingStates.peekBack();
|
|
385
|
+
return {
|
|
386
|
+
rollback: () => {
|
|
387
|
+
try {
|
|
388
|
+
while (this.pendingStates.peekBack() !== checkpointHead) {
|
|
389
|
+
this.rollbackNextPendingState();
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const error = wrapError(err, (message) => {
|
|
393
|
+
return DataProcessingError.create(
|
|
394
|
+
`RollbackError: ${message}`,
|
|
395
|
+
"checkpointRollback",
|
|
396
|
+
undefined) as DataProcessingError;
|
|
397
|
+
});
|
|
398
|
+
this.stateHandler.close(error);
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
449
405
|
/**
|
|
450
406
|
* Returns the next pending state from the pending state queue.
|
|
451
407
|
*/
|
|
@@ -455,17 +411,42 @@ export class PendingStateManager implements IDisposable {
|
|
|
455
411
|
return nextPendingState;
|
|
456
412
|
}
|
|
457
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Undo the last pending state
|
|
416
|
+
*/
|
|
417
|
+
private rollbackNextPendingState() {
|
|
418
|
+
const pendingStatesCount = this.pendingStates.length;
|
|
419
|
+
if (pendingStatesCount === 0) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
this._pendingMessagesCount--;
|
|
424
|
+
|
|
425
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
426
|
+
const pendingState = this.pendingStates.pop()!;
|
|
427
|
+
switch (pendingState.type) {
|
|
428
|
+
case "message":
|
|
429
|
+
this.stateHandler.rollback(
|
|
430
|
+
pendingState.messageType,
|
|
431
|
+
pendingState.content,
|
|
432
|
+
pendingState.localOpMetadata);
|
|
433
|
+
break;
|
|
434
|
+
default:
|
|
435
|
+
throw new Error(`Can't rollback state ${pendingState.type}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
458
439
|
/**
|
|
459
440
|
* Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
|
|
460
441
|
* states in its queue. This includes setting the FlushMode and triggering resubmission of unacked ops.
|
|
461
442
|
*/
|
|
462
443
|
public replayPendingStates() {
|
|
463
|
-
assert(this.connected, 0x172 /* "The connection state is not consistent with the runtime" */);
|
|
444
|
+
assert(this.stateHandler.connected(), 0x172 /* "The connection state is not consistent with the runtime" */);
|
|
464
445
|
|
|
465
446
|
// This assert suggests we are about to send same ops twice, which will result in data loss.
|
|
466
|
-
assert(this.clientId !== this.
|
|
447
|
+
assert(this.clientId !== this.stateHandler.clientId(),
|
|
467
448
|
0x173 /* "replayPendingStates called twice for same clientId!" */);
|
|
468
|
-
this.clientId = this.
|
|
449
|
+
this.clientId = this.stateHandler.clientId();
|
|
469
450
|
|
|
470
451
|
assert(this.initialStates.isEmpty(), 0x174 /* "initial states should be empty before replaying pending" */);
|
|
471
452
|
|
|
@@ -475,14 +456,14 @@ export class PendingStateManager implements IDisposable {
|
|
|
475
456
|
}
|
|
476
457
|
|
|
477
458
|
// Reset the pending message count because all these messages will be removed from the queue.
|
|
478
|
-
this.
|
|
459
|
+
this._pendingMessagesCount = 0;
|
|
479
460
|
|
|
480
461
|
// Save the current FlushMode so that we can revert it back after replaying the states.
|
|
481
|
-
const savedFlushMode = this.
|
|
462
|
+
const savedFlushMode = this.stateHandler.flushMode();
|
|
482
463
|
|
|
483
464
|
// Set the flush mode for the next message. This step is important because the flush mode may have been changed
|
|
484
465
|
// after the next pending message was sent.
|
|
485
|
-
this.
|
|
466
|
+
this.stateHandler.setFlushMode(this.flushModeForNextMessage);
|
|
486
467
|
|
|
487
468
|
// Process exactly `pendingStatesCount` items in the queue as it represents the number of states that were
|
|
488
469
|
// pending when we connected. This is important because the `reSubmitFn` might add more items in the queue
|
|
@@ -492,17 +473,17 @@ export class PendingStateManager implements IDisposable {
|
|
|
492
473
|
const pendingState = this.pendingStates.shift()!;
|
|
493
474
|
switch (pendingState.type) {
|
|
494
475
|
case "message":
|
|
495
|
-
this.
|
|
476
|
+
this.stateHandler.reSubmit(
|
|
496
477
|
pendingState.messageType,
|
|
497
478
|
pendingState.content,
|
|
498
479
|
pendingState.localOpMetadata,
|
|
499
480
|
pendingState.opMetadata);
|
|
500
481
|
break;
|
|
501
482
|
case "flushMode":
|
|
502
|
-
this.
|
|
483
|
+
this.stateHandler.setFlushMode(pendingState.flushMode);
|
|
503
484
|
break;
|
|
504
485
|
case "flush":
|
|
505
|
-
this.
|
|
486
|
+
this.stateHandler.flush();
|
|
506
487
|
break;
|
|
507
488
|
default:
|
|
508
489
|
break;
|
|
@@ -511,6 +492,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
511
492
|
}
|
|
512
493
|
|
|
513
494
|
// Revert the FlushMode.
|
|
514
|
-
this.
|
|
495
|
+
this.stateHandler.setFlushMode(savedFlushMode);
|
|
515
496
|
}
|
|
516
497
|
}
|