@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ITelemetryProperties } from "@fluidframework/common-definitions";
|
|
7
|
+
import { ICriticalContainerError } from "@fluidframework/container-definitions";
|
|
8
|
+
import {
|
|
9
|
+
IConfigProvider,
|
|
10
|
+
IFluidErrorBase,
|
|
11
|
+
LoggingError,
|
|
12
|
+
MonitoringContext,
|
|
13
|
+
} from "@fluidframework/telemetry-utils";
|
|
14
|
+
import { oneDayMs } from "./garbageCollection";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Feature Gate Key -
|
|
18
|
+
* How many days between closing the container from this error (avoids locking user out of their file altogether)
|
|
19
|
+
*/
|
|
20
|
+
export const skipClosureForXDaysKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.SkipClosureForXDays";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* LocalStorage key (NOT via feature gate / monitoring context)
|
|
24
|
+
* A map from docId to info about the last time we closed due to this error
|
|
25
|
+
*/
|
|
26
|
+
export const closuresMapLocalStorageKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.Closures";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Feature gate key to enable closing the container if SweepReady objects are used.
|
|
30
|
+
* Value should contain keywords "interactiveClient" and/or "summarizer" to enable detection in each container type
|
|
31
|
+
*/
|
|
32
|
+
const sweepReadyUsageDetectionSetting = {
|
|
33
|
+
read(config: IConfigProvider) {
|
|
34
|
+
const sweepReadyUsageDetectionKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection";
|
|
35
|
+
const value = config.getString(sweepReadyUsageDetectionKey);
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
return { interactiveClient: false, summarizer: false };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
interactiveClient: value.includes("interactiveClient"),
|
|
41
|
+
summarizer: value.includes("summarizer"),
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Error class raised when a SweepReady object is used, indicating a bug in how
|
|
48
|
+
* references are managed in the container by the application, or a bug in how
|
|
49
|
+
* GC tracks those references.
|
|
50
|
+
*
|
|
51
|
+
* There's a chance for false positives when this error is raised by an Interactive Container,
|
|
52
|
+
* since only the Summarizer has the latest truth about unreferenced node tracking
|
|
53
|
+
*/
|
|
54
|
+
export class SweepReadyUsageError extends LoggingError implements IFluidErrorBase {
|
|
55
|
+
/** This errorType will be in temporary use (until Sweep is fully implemented) so don't add to any errorType type */
|
|
56
|
+
public errorType: string = "unreferencedObjectUsedAfterGarbageCollected";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* This class encapsulates the logic around what to do when a SweepReady object is used.
|
|
61
|
+
* There are several tactics we plan to use in Dogfood environments to aid diagnosis of these cases:
|
|
62
|
+
* - Closing the interactive container when either the interactive or summarizer client detects this kind of violation
|
|
63
|
+
* (via sweepReadyUsageDetectionSetting above)
|
|
64
|
+
* - Throttling the frequency of these crashes via a "Skip Closure Period" per container per device
|
|
65
|
+
* (via skipClosureForXDaysKey above. Uses localStorage and closuresMapLocalStorageKey to implement this behavior)
|
|
66
|
+
*/
|
|
67
|
+
export class SweepReadyUsageDetectionHandler {
|
|
68
|
+
private readonly localStorage: Pick<Storage, "getItem" | "setItem">;
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly uniqueContainerKey: string,
|
|
72
|
+
private readonly mc: MonitoringContext,
|
|
73
|
+
private readonly closeFn: (error?: ICriticalContainerError) => void,
|
|
74
|
+
localStorageOverride?: Pick<Storage, "getItem" | "setItem">,
|
|
75
|
+
) {
|
|
76
|
+
const noopStorage = { getItem: () => null, setItem: () => {} };
|
|
77
|
+
if (localStorageOverride !== undefined) {
|
|
78
|
+
this.localStorage = localStorageOverride;
|
|
79
|
+
} else {
|
|
80
|
+
try {
|
|
81
|
+
// localStorage is not defined in Node environment so this throws
|
|
82
|
+
this.localStorage = localStorage ?? noopStorage;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.localStorage = noopStorage;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.localStorage === noopStorage) {
|
|
89
|
+
// This means the Skip Closure Period logic will not work.
|
|
90
|
+
this.mc.logger.sendTelemetryEvent({ eventName: "SweepReadyUsageDetectionHandlerNoopStorage" });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* If SweepReady Usage Detection is enabled, close the interactive container.
|
|
96
|
+
* If the SkipClosureForXDays setting is set, don't close the container more than once in that period.
|
|
97
|
+
*
|
|
98
|
+
* Once Sweep is fully implemented, this will be removed since the objects will be gone
|
|
99
|
+
* and errors will arise elsewhere in the runtime
|
|
100
|
+
*/
|
|
101
|
+
public usageDetectedInInteractiveClient(errorProps: ITelemetryProperties) {
|
|
102
|
+
if (!sweepReadyUsageDetectionSetting.read(this.mc.config).interactiveClient) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Default stance is we close every time - this reflects the severity of SweepReady Object Usage.
|
|
107
|
+
// However, we may choose to "throttle" the closures by setting the SkipClosureForXDays setting,
|
|
108
|
+
// which will only allow the container to close once during that period, to avoid locking users out.
|
|
109
|
+
let shouldClose: boolean = true;
|
|
110
|
+
let pastClosuresMap: Record<string, { lastCloseTime: number; } | undefined> = {};
|
|
111
|
+
let lastCloseTime: number | undefined;
|
|
112
|
+
const skipClosureForXDays = this.mc.config.getNumber(skipClosureForXDaysKey);
|
|
113
|
+
if (skipClosureForXDays !== undefined) {
|
|
114
|
+
// Read pastClosuresMap from localStorage then extract the lastCloseTime from the map
|
|
115
|
+
try {
|
|
116
|
+
const rawValue = this.localStorage.getItem(closuresMapLocalStorageKey);
|
|
117
|
+
const parsedValue = rawValue === null ? {} : JSON.parse(rawValue);
|
|
118
|
+
if (typeof parsedValue === "object") {
|
|
119
|
+
pastClosuresMap = parsedValue;
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
}
|
|
123
|
+
lastCloseTime = pastClosuresMap[this.uniqueContainerKey]?.lastCloseTime;
|
|
124
|
+
|
|
125
|
+
// Don't close if we did already within the Skip Closure Period
|
|
126
|
+
if (lastCloseTime !== undefined && Date.now() < lastCloseTime + skipClosureForXDays * oneDayMs) {
|
|
127
|
+
shouldClose = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const error = new SweepReadyUsageError(
|
|
132
|
+
"SweepReady object used in Non-Summarizer Client",
|
|
133
|
+
{ errorDetails: JSON.stringify({ ...errorProps, lastCloseTime, skipClosureForXDays }) },
|
|
134
|
+
);
|
|
135
|
+
if (shouldClose) {
|
|
136
|
+
// Update closures map in localStorage before closing
|
|
137
|
+
// Note there is a race condition between different tabs updating localStorage and overwriting
|
|
138
|
+
// each others' updates. If so, some tab will crash again. Just reload one at a time to get unstuck
|
|
139
|
+
pastClosuresMap[this.uniqueContainerKey] = { lastCloseTime: Date.now() };
|
|
140
|
+
this.localStorage.setItem(closuresMapLocalStorageKey, JSON.stringify(pastClosuresMap));
|
|
141
|
+
|
|
142
|
+
this.closeFn(error);
|
|
143
|
+
} else {
|
|
144
|
+
this.mc.logger.sendErrorEvent({ eventName: "SweepReadyObject_UsageAllowed" }, error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,12 +14,10 @@ export {
|
|
|
14
14
|
ISummaryConfigurationDisableSummarizer,
|
|
15
15
|
ISummaryConfigurationDisableHeuristics,
|
|
16
16
|
IContainerRuntimeOptions,
|
|
17
|
-
IPendingRuntimeState,
|
|
18
17
|
IRootSummaryTreeWithStats,
|
|
19
18
|
isRuntimeMessage,
|
|
20
19
|
RuntimeMessage,
|
|
21
20
|
unpackRuntimeMessage,
|
|
22
|
-
ScheduleManager,
|
|
23
21
|
agentSchedulerId,
|
|
24
22
|
ContainerRuntime,
|
|
25
23
|
RuntimeHeaders,
|
|
@@ -41,6 +39,7 @@ export {
|
|
|
41
39
|
IPendingMessage,
|
|
42
40
|
IPendingState,
|
|
43
41
|
} from "./pendingStateManager";
|
|
42
|
+
export { ScheduleManager } from "./scheduleManager";
|
|
44
43
|
export { Summarizer } from "./summarizer";
|
|
45
44
|
export {
|
|
46
45
|
EnqueueSummarizeResult,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ISequencedDocumentMessage, ISequencedDocumentSystemMessage } from "@fluidframework/protocol-definitions";
|
|
7
|
+
|
|
8
|
+
export const opSize = (op: ISequencedDocumentMessage): number => {
|
|
9
|
+
// Some messages may already have string contents,
|
|
10
|
+
// so stringifying them again will add inaccurate overhead.
|
|
11
|
+
const content = typeof op.contents === "string" ?
|
|
12
|
+
op.contents :
|
|
13
|
+
JSON.stringify(op.contents) ?? "";
|
|
14
|
+
const data = opHasData(op) ? op.data : "";
|
|
15
|
+
return content.length + data.length;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const opHasData = (op: ISequencedDocumentMessage): op is ISequencedDocumentSystemMessage =>
|
|
19
|
+
(op as ISequencedDocumentSystemMessage).data !== undefined;
|
|
@@ -208,11 +208,17 @@ export interface IOrderedClientElectionEvents extends IEvent {
|
|
|
208
208
|
export interface ISerializedElection {
|
|
209
209
|
/** Sequence number at the time of the latest election. */
|
|
210
210
|
readonly electionSequenceNumber: number;
|
|
211
|
-
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Most recently elected client id. This is either:
|
|
214
|
+
*
|
|
212
215
|
* 1. the interactive elected parent client, in which case electedClientId === electedParentId,
|
|
213
|
-
*
|
|
214
|
-
*
|
|
216
|
+
* and the SummaryManager on the elected client will spawn a summarizer client, or
|
|
217
|
+
*
|
|
218
|
+
* 2. the non-interactive summarizer client itself.
|
|
219
|
+
*/
|
|
215
220
|
readonly electedClientId: string | undefined;
|
|
221
|
+
|
|
216
222
|
/** Most recently elected parent client id. This is always an interactive client. */
|
|
217
223
|
readonly electedParentId: string | undefined;
|
|
218
224
|
}
|
|
@@ -221,10 +227,15 @@ export interface ISerializedElection {
|
|
|
221
227
|
export interface IOrderedClientElection extends IEventProvider<IOrderedClientElectionEvents> {
|
|
222
228
|
/** Count of eligible clients in the collection. */
|
|
223
229
|
readonly eligibleCount: number;
|
|
224
|
-
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Currently elected client. This is either:
|
|
233
|
+
*
|
|
225
234
|
* 1. the interactive elected parent client, in which case electedClientId === electedParentId,
|
|
226
|
-
*
|
|
227
|
-
*
|
|
235
|
+
* and the SummaryManager on the elected client will spawn a summarizer client, or
|
|
236
|
+
*
|
|
237
|
+
* 2. the non-interactive summarizer client itself.
|
|
238
|
+
*/
|
|
228
239
|
readonly electedClient: ITrackedClient | undefined;
|
|
229
240
|
/** Currently elected parent client. This is always an interactive client. */
|
|
230
241
|
readonly electedParent: ITrackedClient | undefined;
|
|
@@ -283,13 +294,20 @@ export class OrderedClientElection
|
|
|
283
294
|
* electedClient leaves the quorum.
|
|
284
295
|
*
|
|
285
296
|
* A typical sequence looks like this:
|
|
297
|
+
*
|
|
286
298
|
* i. Begin by electing A. electedParent === A, electedClient === A.
|
|
299
|
+
*
|
|
287
300
|
* ii. SummaryManager running on A spawns a summarizer client, A'. electedParent === A, electedClient === A'
|
|
301
|
+
*
|
|
288
302
|
* iii. A' stops producing summaries. A new parent client, B, is elected. electedParent === B, electedClient === A'
|
|
303
|
+
*
|
|
289
304
|
* iv. SummaryManager running on A detects the change to electedParent and tells the summarizer to stop, but A'
|
|
290
|
-
*
|
|
305
|
+
* is in mid-summarization. No new summarizer is spawned, as electedParent !== electedClient.
|
|
306
|
+
*
|
|
291
307
|
* v. A' completes its summary, and the summarizer and backing client are torn down.
|
|
308
|
+
*
|
|
292
309
|
* vi. A' leaves the quorum, and B takes its place as electedClient. electedParent === B, electedClient === B
|
|
310
|
+
*
|
|
293
311
|
* vii. SummaryManager running on B spawns a summarizer client, B'. electedParent === B, electedClient === B'
|
|
294
312
|
*/
|
|
295
313
|
public get electedClient() {
|
|
@@ -357,7 +375,8 @@ export class OrderedClientElection
|
|
|
357
375
|
}
|
|
358
376
|
}
|
|
359
377
|
|
|
360
|
-
/**
|
|
378
|
+
/**
|
|
379
|
+
* Tries changing the elected client, raising an event if it is different.
|
|
361
380
|
* Note that this function does no eligibility or suitability checks. If we get here, then
|
|
362
381
|
* we will set _electedClient, and we will set _electedParent if this is an interactive client.
|
|
363
382
|
*/
|
|
@@ -467,7 +486,8 @@ export class OrderedClientElection
|
|
|
467
486
|
return this.orderedClientCollection.getAllClients().filter(this.isEligibleFn);
|
|
468
487
|
}
|
|
469
488
|
|
|
470
|
-
/**
|
|
489
|
+
/**
|
|
490
|
+
* Advance election to the next-oldest client. This is called if the current parent is leaving the quorum,
|
|
471
491
|
* or if the current summarizer is not responsive and we want to stop it and spawn a new one.
|
|
472
492
|
*/
|
|
473
493
|
public incrementElectedClient(sequenceNumber: number): void {
|
|
@@ -482,7 +502,8 @@ export class OrderedClientElection
|
|
|
482
502
|
}
|
|
483
503
|
}
|
|
484
504
|
|
|
485
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* (Re-)start election with the oldest client in the quorum. This is called if we need to summarize
|
|
486
507
|
* and no client has been elected.
|
|
487
508
|
*/
|
|
488
509
|
public resetElectedClient(sequenceNumber: number): void {
|
package/src/packageVersion.ts
CHANGED
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
ISequencedDocumentMessage,
|
|
12
12
|
} from "@fluidframework/protocol-definitions";
|
|
13
13
|
import { FlushMode } from "@fluidframework/runtime-definitions";
|
|
14
|
-
import { wrapError } from "@fluidframework/telemetry-utils";
|
|
15
14
|
import Deque from "double-ended-queue";
|
|
16
15
|
import { ContainerMessageType } from "./containerRuntime";
|
|
16
|
+
import { pkgVersion } from "./packageVersion";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* This represents a message that has been submitted and is added to the pending queue when `submit` is called on the
|
|
@@ -68,10 +68,6 @@ export interface IRuntimeStateHandler{
|
|
|
68
68
|
content: any,
|
|
69
69
|
localOpMetadata: unknown,
|
|
70
70
|
opMetadata: Record<string, unknown> | undefined): void;
|
|
71
|
-
rollback(
|
|
72
|
-
type: ContainerMessageType,
|
|
73
|
-
content: any,
|
|
74
|
-
localOpMetadata: unknown): void;
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
/**
|
|
@@ -232,7 +228,11 @@ export class PendingStateManager implements IDisposable {
|
|
|
232
228
|
|
|
233
229
|
// then we push onto pendingStates which will cause PendingStateManager to resubmit when we connect
|
|
234
230
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
235
|
-
this.
|
|
231
|
+
const firstPendingState = this.initialStates.shift()!;
|
|
232
|
+
this.pendingStates.push(firstPendingState);
|
|
233
|
+
if (firstPendingState.type === "message") {
|
|
234
|
+
this._pendingMessagesCount++;
|
|
235
|
+
}
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
@@ -266,6 +266,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
this._pendingMessagesCount--;
|
|
269
|
+
assert(this._pendingMessagesCount >= 0, 0x3d6 /* positive */);
|
|
269
270
|
|
|
270
271
|
// Post-processing part - If we are processing a batch then this could be the last message in the batch.
|
|
271
272
|
this.maybeProcessBatchEnd(message);
|
|
@@ -285,9 +286,11 @@ export class PendingStateManager implements IDisposable {
|
|
|
285
286
|
|
|
286
287
|
/**
|
|
287
288
|
* We are checking if the next message is the start of a batch. It can happen in the following scenarios:
|
|
289
|
+
*
|
|
288
290
|
* 1. The FlushMode was set to TurnBased before this message was sent.
|
|
291
|
+
*
|
|
289
292
|
* 2. The FlushMode was already TurnBased and a flush was called before this message was sent. This essentially
|
|
290
|
-
*
|
|
293
|
+
* means that the flush marked the end of a previous batch and beginning of a new batch.
|
|
291
294
|
*
|
|
292
295
|
* Keep reading pending states from the queue until we encounter a message. It's possible that the FlushMode was
|
|
293
296
|
* updated a bunch of times without sending any messages.
|
|
@@ -368,8 +371,23 @@ export class PendingStateManager implements IDisposable {
|
|
|
368
371
|
} else {
|
|
369
372
|
// Get the batch metadata from the last message in the batch.
|
|
370
373
|
const batchEndMetadata = message.metadata?.batch;
|
|
371
|
-
|
|
372
|
-
|
|
374
|
+
if (batchBeginMetadata !== true || batchEndMetadata !== false) {
|
|
375
|
+
this.stateHandler.close(DataProcessingError.create(
|
|
376
|
+
"Pending batch inconsistency", // Formerly known as asserts 0x16f and 0x170
|
|
377
|
+
"processPendingLocalMessage",
|
|
378
|
+
message,
|
|
379
|
+
{
|
|
380
|
+
runtimeVersion: pkgVersion,
|
|
381
|
+
batchClientId: this.pendingBatchBeginMessage.clientId,
|
|
382
|
+
clientId: this.stateHandler.clientId(),
|
|
383
|
+
hasBatchStart: batchBeginMetadata === true,
|
|
384
|
+
hasBatchEnd: batchEndMetadata === false,
|
|
385
|
+
messageType: message.type,
|
|
386
|
+
batchStartSequenceNumber: this.pendingBatchBeginMessage.clientSequenceNumber,
|
|
387
|
+
pendingMessagesCount: this.pendingMessagesCount,
|
|
388
|
+
nextPendingState: nextPendingState.type,
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
373
391
|
}
|
|
374
392
|
|
|
375
393
|
// Clear the pending batch state now that we have processed the entire batch.
|
|
@@ -377,31 +395,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
377
395
|
this.isProcessingBatch = false;
|
|
378
396
|
}
|
|
379
397
|
|
|
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
|
-
|
|
405
398
|
/**
|
|
406
399
|
* Returns the next pending state from the pending state queue.
|
|
407
400
|
*/
|
|
@@ -411,31 +404,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
411
404
|
return nextPendingState;
|
|
412
405
|
}
|
|
413
406
|
|
|
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
|
-
|
|
439
407
|
/**
|
|
440
408
|
* Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
|
|
441
409
|
* states in its queue. This includes setting the FlushMode and triggering resubmission of unacked ops.
|
package/src/runningSummarizer.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { IDisposable, ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
7
7
|
import { assert, delay, Deferred, PromiseTimer } from "@fluidframework/common-utils";
|
|
8
8
|
import { UsageError } from "@fluidframework/container-utils";
|
|
9
|
+
import { isRuntimeMessage } from "@fluidframework/driver-utils";
|
|
9
10
|
import {
|
|
10
11
|
ISequencedDocumentMessage,
|
|
11
12
|
MessageType,
|
|
@@ -14,6 +15,7 @@ import { ChildLogger } from "@fluidframework/telemetry-utils";
|
|
|
14
15
|
import {
|
|
15
16
|
ISummaryConfiguration,
|
|
16
17
|
} from "./containerRuntime";
|
|
18
|
+
import { opSize } from "./opProperties";
|
|
17
19
|
import { SummarizeHeuristicRunner } from "./summarizerHeuristics";
|
|
18
20
|
import {
|
|
19
21
|
IEnqueueSummarizeOptions,
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
ISummaryCancellationToken,
|
|
29
31
|
ISummarizeResults,
|
|
30
32
|
ISummarizeTelemetryProperties,
|
|
33
|
+
ISummarizerRuntime,
|
|
31
34
|
ISummarizeRunnerTelemetry,
|
|
32
35
|
} from "./summarizerTypes";
|
|
33
36
|
import { IClientSummaryWatcher, SummaryCollection } from "./summaryCollection";
|
|
@@ -58,6 +61,7 @@ export class RunningSummarizer implements IDisposable {
|
|
|
58
61
|
summaryCollection: SummaryCollection,
|
|
59
62
|
cancellationToken: ISummaryCancellationToken,
|
|
60
63
|
stopSummarizerCallback: (reason: SummarizerStopReason) => void,
|
|
64
|
+
runtime: ISummarizerRuntime,
|
|
61
65
|
): Promise<RunningSummarizer> {
|
|
62
66
|
const summarizer = new RunningSummarizer(
|
|
63
67
|
logger,
|
|
@@ -68,12 +72,36 @@ export class RunningSummarizer implements IDisposable {
|
|
|
68
72
|
raiseSummarizingError,
|
|
69
73
|
summaryCollection,
|
|
70
74
|
cancellationToken,
|
|
71
|
-
stopSummarizerCallback
|
|
75
|
+
stopSummarizerCallback,
|
|
76
|
+
runtime);
|
|
72
77
|
|
|
73
78
|
await summarizer.waitStart();
|
|
74
79
|
|
|
75
|
-
//
|
|
80
|
+
// Update heuristic counts
|
|
81
|
+
// By the time we get here, there are potentially ops missing from the heuristic summary counts
|
|
82
|
+
// Examples of where this could happen:
|
|
83
|
+
// 1. Op is processed during the time that we are initiating the RunningSummarizer instance but before we
|
|
84
|
+
// listen for the op events (will get missed by the handlers in the current workflow)
|
|
85
|
+
// 2. Op was sequenced after the last time we summarized (op sequence number > summarize ref sequence number)
|
|
86
|
+
const diff = runtime.deltaManager.lastSequenceNumber - (
|
|
87
|
+
heuristicData.lastSuccessfulSummary.refSequenceNumber
|
|
88
|
+
+ heuristicData.numNonRuntimeOps
|
|
89
|
+
+ heuristicData.numRuntimeOps);
|
|
90
|
+
heuristicData.hasMissingOpData = diff > 0;
|
|
91
|
+
|
|
92
|
+
if (heuristicData.hasMissingOpData) {
|
|
93
|
+
// Split the diff 50-50 and increment the counts appropriately
|
|
94
|
+
heuristicData.numNonRuntimeOps += Math.ceil(diff / 2);
|
|
95
|
+
heuristicData.numRuntimeOps += Math.floor(diff / 2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Update last seq number (in case the handlers haven't processed anything yet)
|
|
99
|
+
heuristicData.lastOpSequenceNumber = runtime.deltaManager.lastSequenceNumber;
|
|
100
|
+
|
|
101
|
+
// Start heuristics
|
|
102
|
+
summarizer.heuristicRunner?.start();
|
|
76
103
|
summarizer.heuristicRunner?.run();
|
|
104
|
+
|
|
77
105
|
return summarizer;
|
|
78
106
|
}
|
|
79
107
|
|
|
@@ -95,6 +123,7 @@ export class RunningSummarizer implements IDisposable {
|
|
|
95
123
|
} | undefined;
|
|
96
124
|
private summarizeCount = 0;
|
|
97
125
|
private totalSuccessfulAttempts = 0;
|
|
126
|
+
private initialized = false;
|
|
98
127
|
|
|
99
128
|
private constructor(
|
|
100
129
|
baseLogger: ITelemetryLogger,
|
|
@@ -106,6 +135,7 @@ export class RunningSummarizer implements IDisposable {
|
|
|
106
135
|
private readonly summaryCollection: SummaryCollection,
|
|
107
136
|
private readonly cancellationToken: ISummaryCancellationToken,
|
|
108
137
|
private readonly stopSummarizerCallback: (reason: SummarizerStopReason) => void,
|
|
138
|
+
private readonly runtime: ISummarizerRuntime,
|
|
109
139
|
) {
|
|
110
140
|
const telemetryProps: ISummarizeRunnerTelemetry = {
|
|
111
141
|
summarizeCount: () => this.summarizeCount,
|
|
@@ -175,9 +205,13 @@ export class RunningSummarizer implements IDisposable {
|
|
|
175
205
|
this.summaryWatcher,
|
|
176
206
|
this.logger,
|
|
177
207
|
);
|
|
208
|
+
|
|
209
|
+
// Listen for ops
|
|
210
|
+
this.runtime.deltaManager.on("op", (op) => { this.handleOp(op); });
|
|
178
211
|
}
|
|
179
212
|
|
|
180
213
|
public dispose(): void {
|
|
214
|
+
this.runtime.deltaManager.off("op", (op) => { this.handleOp(op); });
|
|
181
215
|
this.summaryWatcher.dispose();
|
|
182
216
|
this.heuristicRunner?.dispose();
|
|
183
217
|
this.heuristicRunner = undefined;
|
|
@@ -199,30 +233,48 @@ export class RunningSummarizer implements IDisposable {
|
|
|
199
233
|
? this.logger
|
|
200
234
|
: undefined;
|
|
201
235
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
case MessageType.ClientLeave:
|
|
205
|
-
case MessageType.ClientJoin:
|
|
206
|
-
case MessageType.Propose: {
|
|
207
|
-
// Synchronously handle quorum ops like regular ops
|
|
208
|
-
this.handleOp(undefined, op);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
default: {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
236
|
+
/** We only want a single heuristic runner micro-task (will provide better optimized grouping of ops) */
|
|
237
|
+
private heuristicRunnerMicroTaskExists = false;
|
|
216
238
|
|
|
217
|
-
public handleOp(
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
public handleOp(op: ISequencedDocumentMessage) {
|
|
240
|
+
this.heuristicData.lastOpSequenceNumber = op.sequenceNumber;
|
|
241
|
+
|
|
242
|
+
if (isRuntimeMessage(op)) {
|
|
243
|
+
this.heuristicData.numRuntimeOps++;
|
|
244
|
+
} else {
|
|
245
|
+
this.heuristicData.numNonRuntimeOps++;
|
|
220
246
|
}
|
|
221
|
-
|
|
247
|
+
|
|
248
|
+
this.heuristicData.totalOpsSize += opSize(op);
|
|
222
249
|
|
|
223
250
|
// Check for enqueued on-demand summaries; Intentionally do nothing otherwise
|
|
224
|
-
if (
|
|
225
|
-
this.
|
|
251
|
+
if (this.initialized
|
|
252
|
+
&& this.opCanTriggerSummary(op)
|
|
253
|
+
&& !this.tryRunEnqueuedSummary()
|
|
254
|
+
&& !this.heuristicRunnerMicroTaskExists) {
|
|
255
|
+
this.heuristicRunnerMicroTaskExists = true;
|
|
256
|
+
Promise.resolve().then(() => {
|
|
257
|
+
this.heuristicRunner?.run();
|
|
258
|
+
}).finally(() => {
|
|
259
|
+
this.heuristicRunnerMicroTaskExists = false;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Can the given op trigger a summary?
|
|
266
|
+
* # Currently only prevents summaries for Summarize and SummaryAck ops
|
|
267
|
+
* @param op - op to check
|
|
268
|
+
* @returns true if this type of op can trigger a summary
|
|
269
|
+
*/
|
|
270
|
+
private opCanTriggerSummary(op: ISequencedDocumentMessage): boolean {
|
|
271
|
+
switch (op.type) {
|
|
272
|
+
case MessageType.Summarize:
|
|
273
|
+
case MessageType.SummaryAck:
|
|
274
|
+
case MessageType.SummaryNack:
|
|
275
|
+
return false;
|
|
276
|
+
default:
|
|
277
|
+
return true;
|
|
226
278
|
}
|
|
227
279
|
}
|
|
228
280
|
|
|
@@ -274,6 +326,7 @@ export class RunningSummarizer implements IDisposable {
|
|
|
274
326
|
summarySequenceNumber: waitStartResult.value.summaryOp.sequenceNumber,
|
|
275
327
|
});
|
|
276
328
|
}
|
|
329
|
+
this.initialized = true;
|
|
277
330
|
}
|
|
278
331
|
|
|
279
332
|
/**
|
|
@@ -290,7 +343,7 @@ export class RunningSummarizer implements IDisposable {
|
|
|
290
343
|
*/
|
|
291
344
|
public async lockedRefreshSummaryAckAction<T>(action: () => Promise<T>) {
|
|
292
345
|
assert(this.refreshSummaryAckLock === undefined,
|
|
293
|
-
|
|
346
|
+
0x396 /* Refresh Summary Ack - Caller is responsible for checking lock */);
|
|
294
347
|
|
|
295
348
|
const refreshSummaryAckLock = new Deferred<void>();
|
|
296
349
|
this.refreshSummaryAckLock = refreshSummaryAckLock.promise;
|