@fluidframework/container-runtime 2.0.0-internal.1.1.2 → 2.0.0-internal.1.2.0
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/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 +1 -2
- package/dist/batchTracker.js.map +1 -1
- package/dist/containerRuntime.d.ts +52 -20
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +252 -126
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts +18 -9
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +24 -16
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +6 -2
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +7 -9
- 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 +41 -12
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +176 -98
- 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/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.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 +9 -44
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runningSummarizer.js +1 -1
- package/dist/runningSummarizer.js.map +1 -1
- package/dist/scheduleManager.d.ts +6 -3
- package/dist/scheduleManager.d.ts.map +1 -1
- package/dist/scheduleManager.js +22 -14
- package/dist/scheduleManager.js.map +1 -1
- package/dist/summarizerTypes.d.ts +16 -9
- 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 +29 -13
- package/dist/summaryCollection.js.map +1 -1
- package/dist/summaryManager.d.ts +2 -2
- package/dist/summaryManager.js +2 -2
- 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 +1 -2
- package/lib/batchTracker.js.map +1 -1
- package/lib/containerRuntime.d.ts +52 -20
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +255 -129
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStoreContext.d.ts +18 -9
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +25 -17
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStores.d.ts +6 -2
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +7 -9
- 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 +41 -12
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +175 -97
- 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/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.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 +9 -44
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runningSummarizer.js +1 -1
- package/lib/runningSummarizer.js.map +1 -1
- package/lib/scheduleManager.d.ts +6 -3
- package/lib/scheduleManager.d.ts.map +1 -1
- package/lib/scheduleManager.js +24 -16
- package/lib/scheduleManager.js.map +1 -1
- package/lib/summarizerTypes.d.ts +16 -9
- 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 +29 -13
- package/lib/summaryCollection.js.map +1 -1
- package/lib/summaryManager.d.ts +2 -2
- package/lib/summaryManager.js +2 -2
- package/lib/summaryManager.js.map +1 -1
- package/package.json +21 -18
- package/src/batchManager.ts +91 -0
- package/src/batchTracker.ts +1 -2
- package/src/containerRuntime.ts +331 -176
- package/src/dataStoreContext.ts +27 -17
- package/src/dataStores.ts +7 -8
- package/src/deltaScheduler.ts +6 -4
- package/src/garbageCollection.ts +224 -134
- package/src/gcSweepReadyUsageDetection.ts +147 -0
- package/src/orderedClientElection.ts +31 -10
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +9 -57
- package/src/runningSummarizer.ts +1 -1
- package/src/scheduleManager.ts +32 -12
- package/src/summarizerTypes.ts +17 -9
- package/src/summaryCollection.ts +31 -16
- package/src/summaryManager.ts +2 -2
|
@@ -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
|
+
}
|
|
@@ -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,7 +11,6 @@ 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";
|
|
17
16
|
import { pkgVersion } from "./packageVersion";
|
|
@@ -69,10 +68,6 @@ export interface IRuntimeStateHandler{
|
|
|
69
68
|
content: any,
|
|
70
69
|
localOpMetadata: unknown,
|
|
71
70
|
opMetadata: Record<string, unknown> | undefined): void;
|
|
72
|
-
rollback(
|
|
73
|
-
type: ContainerMessageType,
|
|
74
|
-
content: any,
|
|
75
|
-
localOpMetadata: unknown): void;
|
|
76
71
|
}
|
|
77
72
|
|
|
78
73
|
/**
|
|
@@ -233,7 +228,11 @@ export class PendingStateManager implements IDisposable {
|
|
|
233
228
|
|
|
234
229
|
// then we push onto pendingStates which will cause PendingStateManager to resubmit when we connect
|
|
235
230
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
236
|
-
this.
|
|
231
|
+
const firstPendingState = this.initialStates.shift()!;
|
|
232
|
+
this.pendingStates.push(firstPendingState);
|
|
233
|
+
if (firstPendingState.type === "message") {
|
|
234
|
+
this._pendingMessagesCount++;
|
|
235
|
+
}
|
|
237
236
|
}
|
|
238
237
|
}
|
|
239
238
|
|
|
@@ -267,6 +266,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
267
266
|
}
|
|
268
267
|
|
|
269
268
|
this._pendingMessagesCount--;
|
|
269
|
+
assert(this._pendingMessagesCount >= 0, 0x3d6 /* positive */);
|
|
270
270
|
|
|
271
271
|
// Post-processing part - If we are processing a batch then this could be the last message in the batch.
|
|
272
272
|
this.maybeProcessBatchEnd(message);
|
|
@@ -286,9 +286,11 @@ export class PendingStateManager implements IDisposable {
|
|
|
286
286
|
|
|
287
287
|
/**
|
|
288
288
|
* We are checking if the next message is the start of a batch. It can happen in the following scenarios:
|
|
289
|
+
*
|
|
289
290
|
* 1. The FlushMode was set to TurnBased before this message was sent.
|
|
291
|
+
*
|
|
290
292
|
* 2. The FlushMode was already TurnBased and a flush was called before this message was sent. This essentially
|
|
291
|
-
*
|
|
293
|
+
* means that the flush marked the end of a previous batch and beginning of a new batch.
|
|
292
294
|
*
|
|
293
295
|
* Keep reading pending states from the queue until we encounter a message. It's possible that the FlushMode was
|
|
294
296
|
* updated a bunch of times without sending any messages.
|
|
@@ -393,31 +395,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
393
395
|
this.isProcessingBatch = false;
|
|
394
396
|
}
|
|
395
397
|
|
|
396
|
-
/**
|
|
397
|
-
* Capture the pending state at this point
|
|
398
|
-
*/
|
|
399
|
-
public checkpoint() {
|
|
400
|
-
const checkpointHead = this.pendingStates.peekBack();
|
|
401
|
-
return {
|
|
402
|
-
rollback: () => {
|
|
403
|
-
try {
|
|
404
|
-
while (this.pendingStates.peekBack() !== checkpointHead) {
|
|
405
|
-
this.rollbackNextPendingState();
|
|
406
|
-
}
|
|
407
|
-
} catch (err) {
|
|
408
|
-
const error = wrapError(err, (message) => {
|
|
409
|
-
return DataProcessingError.create(
|
|
410
|
-
`RollbackError: ${message}`,
|
|
411
|
-
"checkpointRollback",
|
|
412
|
-
undefined) as DataProcessingError;
|
|
413
|
-
});
|
|
414
|
-
this.stateHandler.close(error);
|
|
415
|
-
throw error;
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
398
|
/**
|
|
422
399
|
* Returns the next pending state from the pending state queue.
|
|
423
400
|
*/
|
|
@@ -427,31 +404,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
427
404
|
return nextPendingState;
|
|
428
405
|
}
|
|
429
406
|
|
|
430
|
-
/**
|
|
431
|
-
* Undo the last pending state
|
|
432
|
-
*/
|
|
433
|
-
private rollbackNextPendingState() {
|
|
434
|
-
const pendingStatesCount = this.pendingStates.length;
|
|
435
|
-
if (pendingStatesCount === 0) {
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
this._pendingMessagesCount--;
|
|
440
|
-
|
|
441
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
442
|
-
const pendingState = this.pendingStates.pop()!;
|
|
443
|
-
switch (pendingState.type) {
|
|
444
|
-
case "message":
|
|
445
|
-
this.stateHandler.rollback(
|
|
446
|
-
pendingState.messageType,
|
|
447
|
-
pendingState.content,
|
|
448
|
-
pendingState.localOpMetadata);
|
|
449
|
-
break;
|
|
450
|
-
default:
|
|
451
|
-
throw new Error(`Can't rollback state ${pendingState.type}`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
407
|
/**
|
|
456
408
|
* Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
|
|
457
409
|
* states in its queue. This includes setting the FlushMode and triggering resubmission of unacked ops.
|
package/src/runningSummarizer.ts
CHANGED
|
@@ -239,7 +239,7 @@ export class RunningSummarizer implements IDisposable {
|
|
|
239
239
|
public handleOp(op: ISequencedDocumentMessage) {
|
|
240
240
|
this.heuristicData.lastOpSequenceNumber = op.sequenceNumber;
|
|
241
241
|
|
|
242
|
-
if (
|
|
242
|
+
if (isRuntimeMessage(op)) {
|
|
243
243
|
this.heuristicData.numRuntimeOps++;
|
|
244
244
|
} else {
|
|
245
245
|
this.heuristicData.numNonRuntimeOps++;
|
package/src/scheduleManager.ts
CHANGED
|
@@ -8,8 +8,12 @@ import { IDocumentMessage, ISequencedDocumentMessage } from "@fluidframework/pro
|
|
|
8
8
|
import { ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
9
9
|
import { ChildLogger } from "@fluidframework/telemetry-utils";
|
|
10
10
|
import { assert, performance } from "@fluidframework/common-utils";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { isRuntimeMessage } from "@fluidframework/driver-utils";
|
|
12
|
+
import {
|
|
13
|
+
DataCorruptionError,
|
|
14
|
+
DataProcessingError,
|
|
15
|
+
extractSafePropertiesFromMessage,
|
|
16
|
+
} from "@fluidframework/container-utils";
|
|
13
17
|
import { DeltaScheduler } from "./deltaScheduler";
|
|
14
18
|
import { pkgVersion } from "./packageVersion";
|
|
15
19
|
import { latencyThreshold } from "./connectionTelemetry";
|
|
@@ -20,10 +24,12 @@ type IRuntimeMessageMetadata = undefined | {
|
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
26
|
* This class has the following responsibilities:
|
|
27
|
+
*
|
|
23
28
|
* 1. It tracks batches as we process ops and raises "batchBegin" and "batchEnd" events.
|
|
24
|
-
*
|
|
29
|
+
* As part of it, it validates batch correctness (i.e. no system ops in the middle of batch)
|
|
30
|
+
*
|
|
25
31
|
* 2. It creates instance of ScheduleManagerCore that ensures we never start processing ops from batch
|
|
26
|
-
*
|
|
32
|
+
* unless all ops of the batch are in.
|
|
27
33
|
*/
|
|
28
34
|
export class ScheduleManager {
|
|
29
35
|
private readonly deltaScheduler: DeltaScheduler;
|
|
@@ -33,13 +39,14 @@ export class ScheduleManager {
|
|
|
33
39
|
constructor(
|
|
34
40
|
private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
|
|
35
41
|
private readonly emitter: EventEmitter,
|
|
42
|
+
readonly getClientId: () => string | undefined,
|
|
36
43
|
private readonly logger: ITelemetryLogger,
|
|
37
44
|
) {
|
|
38
45
|
this.deltaScheduler = new DeltaScheduler(
|
|
39
46
|
this.deltaManager,
|
|
40
47
|
ChildLogger.create(this.logger, "DeltaScheduler"),
|
|
41
48
|
);
|
|
42
|
-
void new ScheduleManagerCore(deltaManager, logger);
|
|
49
|
+
void new ScheduleManagerCore(deltaManager, getClientId, logger);
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
public beforeOpProcessing(message: ISequencedDocumentMessage) {
|
|
@@ -52,11 +59,7 @@ export class ScheduleManager {
|
|
|
52
59
|
this.deltaScheduler.batchBegin(message);
|
|
53
60
|
|
|
54
61
|
const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
|
|
55
|
-
|
|
56
|
-
this.batchClientId = message.clientId;
|
|
57
|
-
} else {
|
|
58
|
-
this.batchClientId = undefined;
|
|
59
|
-
}
|
|
62
|
+
this.batchClientId = batch ? message.clientId : undefined;
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -99,6 +102,7 @@ class ScheduleManagerCore {
|
|
|
99
102
|
|
|
100
103
|
constructor(
|
|
101
104
|
private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
|
|
105
|
+
private readonly getClientId: () => string | undefined,
|
|
102
106
|
private readonly logger: ITelemetryLogger,
|
|
103
107
|
) {
|
|
104
108
|
// Listen for delta manager sends and add batch metadata to messages
|
|
@@ -234,9 +238,22 @@ class ScheduleManagerCore {
|
|
|
234
238
|
const batchMetadata = metadata?.batch;
|
|
235
239
|
|
|
236
240
|
// Protocol messages are never part of a runtime batch of messages
|
|
237
|
-
if (!
|
|
241
|
+
if (!isRuntimeMessage(message)) {
|
|
238
242
|
// Protocol messages should never show up in the middle of the batch!
|
|
239
|
-
|
|
243
|
+
if (this.currentBatchClientId !== undefined) {
|
|
244
|
+
throw DataProcessingError.create(
|
|
245
|
+
"Received a system message during batch processing", // Formerly known as assert 0x29a
|
|
246
|
+
"trackPending",
|
|
247
|
+
message,
|
|
248
|
+
{
|
|
249
|
+
runtimeVersion: pkgVersion,
|
|
250
|
+
batchClientId: this.currentBatchClientId,
|
|
251
|
+
pauseSequenceNumber: this.pauseSequenceNumber,
|
|
252
|
+
localBatch: this.currentBatchClientId === this.getClientId(),
|
|
253
|
+
messageType: message.type,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
240
257
|
assert(batchMetadata === undefined, 0x29b /* "system op in a batch?" */);
|
|
241
258
|
assert(!this.localPaused, 0x29c /* "we should be processing ops when there is no active batch" */);
|
|
242
259
|
return;
|
|
@@ -258,6 +275,9 @@ class ScheduleManagerCore {
|
|
|
258
275
|
{
|
|
259
276
|
runtimeVersion: pkgVersion,
|
|
260
277
|
batchClientId: this.currentBatchClientId,
|
|
278
|
+
pauseSequenceNumber: this.pauseSequenceNumber,
|
|
279
|
+
localBatch: this.currentBatchClientId === this.getClientId(),
|
|
280
|
+
localMessage: message.clientId === this.getClientId(),
|
|
261
281
|
...extractSafePropertiesFromMessage(message),
|
|
262
282
|
});
|
|
263
283
|
}
|
package/src/summarizerTypes.ts
CHANGED
|
@@ -25,16 +25,16 @@ import { SummarizeReason } from "./summaryGenerator";
|
|
|
25
25
|
import { ISummaryConfigurationHeuristics } from ".";
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* @deprecated
|
|
28
|
+
* @deprecated This will be removed in a later release.
|
|
29
29
|
*/
|
|
30
30
|
export const ISummarizer: keyof IProvideSummarizer = "ISummarizer";
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* @deprecated
|
|
33
|
+
* @deprecated This will be removed in a later release.
|
|
34
34
|
*/
|
|
35
35
|
export interface IProvideSummarizer {
|
|
36
36
|
/**
|
|
37
|
-
* @deprecated
|
|
37
|
+
* @deprecated This will be removed in a later release.
|
|
38
38
|
*/
|
|
39
39
|
readonly ISummarizer: ISummarizer;
|
|
40
40
|
}
|
|
@@ -130,6 +130,7 @@ export interface IOnDemandSummarizeOptions extends ISummarizeOptions {
|
|
|
130
130
|
export interface IEnqueueSummarizeOptions extends IOnDemandSummarizeOptions {
|
|
131
131
|
/** If specified, The summarize attempt will not occur until after this sequence number. */
|
|
132
132
|
readonly afterSequenceNumber?: number;
|
|
133
|
+
|
|
133
134
|
/**
|
|
134
135
|
* True to override the existing enqueued summarize attempt if there is one.
|
|
135
136
|
* This will guarantee that this attempt gets enqueued. If override is false,
|
|
@@ -204,11 +205,16 @@ export interface ISubmitSummaryOpResult extends Omit<IUploadSummaryResult, "stag
|
|
|
204
205
|
* The result consists of 4 possible stages, each with its own data.
|
|
205
206
|
* The data is cumulative, so each stage will contain the data from the previous stages.
|
|
206
207
|
* If the final "submitted" stage is not reached, the result may contain the error object.
|
|
208
|
+
*
|
|
207
209
|
* Stages:
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
210
|
+
*
|
|
211
|
+
* 1. "base" - stopped before the summary tree was even generated, and the result only contains the base data
|
|
212
|
+
*
|
|
213
|
+
* 2. "generate" - the summary tree was generated, and the result will contain that tree + stats
|
|
214
|
+
*
|
|
215
|
+
* 3. "upload" - the summary was uploaded to storage, and the result contains the server-provided handle
|
|
216
|
+
*
|
|
217
|
+
* 4. "submit" - the summarize op was submitted, and the result contains the op client sequence number.
|
|
212
218
|
*/
|
|
213
219
|
export type SubmitSummaryResult =
|
|
214
220
|
| IBaseSummarizeResult
|
|
@@ -453,8 +459,10 @@ type SummaryGeneratorOptionalTelemetryProperties =
|
|
|
453
459
|
"opsSinceLastAttempt" |
|
|
454
460
|
/** Delta between the current reference sequence number and the reference sequence number of the last summary */
|
|
455
461
|
"opsSinceLastSummary" |
|
|
456
|
-
/**
|
|
457
|
-
*
|
|
462
|
+
/**
|
|
463
|
+
* Delta in sum of op sizes between the current reference sequence number and the reference
|
|
464
|
+
* sequence number of the last summary
|
|
465
|
+
*/
|
|
458
466
|
"opsSizesSinceLastSummary" |
|
|
459
467
|
/** Delta between the number of non-runtime ops since the last summary */
|
|
460
468
|
"nonRuntimeOpsSinceLastSummary" |
|
package/src/summaryCollection.ts
CHANGED
|
@@ -239,9 +239,7 @@ export class SummaryCollection extends TypedEventEmitter<ISummaryCollectionOpEve
|
|
|
239
239
|
private readonly logger: ITelemetryLogger,
|
|
240
240
|
) {
|
|
241
241
|
super();
|
|
242
|
-
this.deltaManager.on(
|
|
243
|
-
"op",
|
|
244
|
-
(op) => this.handleOp(op));
|
|
242
|
+
this.deltaManager.on("op", (op) => this.handleOp(op));
|
|
245
243
|
}
|
|
246
244
|
|
|
247
245
|
/**
|
|
@@ -295,24 +293,41 @@ export class SummaryCollection extends TypedEventEmitter<ISummaryCollectionOpEve
|
|
|
295
293
|
return this.lastAck;
|
|
296
294
|
}
|
|
297
295
|
|
|
296
|
+
private parseContent(op: ISequencedDocumentMessage) {
|
|
297
|
+
// back-compat: ADO #1385: Make this unconditional in the future,
|
|
298
|
+
// when Container.processRemoteMessage stops parsing contents. That said, we should move to
|
|
299
|
+
// listen for "op" events from ContainerRuntime, and parsing may not be required at all if
|
|
300
|
+
// ContainerRuntime.process() would parse it for all types of ops.
|
|
301
|
+
// Can make either of those changes only when LTS moves to a version that has no content
|
|
302
|
+
// parsing in loader layer!
|
|
303
|
+
if (typeof op.contents === "string") {
|
|
304
|
+
op.contents = JSON.parse(op.contents);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
298
308
|
/**
|
|
299
309
|
* Handler for ops; only handles ops relating to summaries.
|
|
300
310
|
* @param op - op message to handle
|
|
301
311
|
*/
|
|
302
|
-
private handleOp(
|
|
312
|
+
private handleOp(opArg: ISequencedDocumentMessage) {
|
|
313
|
+
const op = { ...opArg };
|
|
314
|
+
|
|
303
315
|
switch (op.type) {
|
|
304
|
-
case MessageType.Summarize:
|
|
305
|
-
this.
|
|
306
|
-
return;
|
|
307
|
-
|
|
308
|
-
case MessageType.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
case MessageType.Summarize:
|
|
317
|
+
this.parseContent(op);
|
|
318
|
+
return this.handleSummaryOp(op as ISummaryOpMessage);
|
|
319
|
+
case MessageType.SummaryAck:
|
|
320
|
+
case MessageType.SummaryNack:
|
|
321
|
+
// Old files (prior to PR #10077) may not contain this info
|
|
322
|
+
// back-compat: ADO #1385: remove cast when ISequencedDocumentMessage changes are propagated
|
|
323
|
+
if ((op as any).data !== undefined) {
|
|
324
|
+
op.contents = JSON.parse((op as any).data);
|
|
325
|
+
} else {
|
|
326
|
+
this.parseContent(op);
|
|
327
|
+
}
|
|
328
|
+
return op.type === MessageType.SummaryAck
|
|
329
|
+
? this.handleSummaryAck(op as ISummaryAckMessage)
|
|
330
|
+
: this.handleSummaryNack(op as ISummaryNackMessage);
|
|
316
331
|
default: {
|
|
317
332
|
// If the difference between timestamp of current op and last summary op is greater than
|
|
318
333
|
// the maxAckWaitTime, then we need to inform summarizer to not wait and summarize
|
package/src/summaryManager.ts
CHANGED
|
@@ -302,8 +302,8 @@ export class SummaryManager implements IDisposable {
|
|
|
302
302
|
|
|
303
303
|
/**
|
|
304
304
|
* Implements initial delay before creating summarizer
|
|
305
|
-
* @returns true
|
|
306
|
-
*
|
|
305
|
+
* @returns `true`, if creation is delayed due to heuristics (not many ops to summarize).
|
|
306
|
+
* `false` if summarizer should start immediately due to too many unsummarized ops.
|
|
307
307
|
*/
|
|
308
308
|
private async delayBeforeCreatingSummarizer(): Promise<boolean> {
|
|
309
309
|
// throttle creation of new summarizer containers to prevent spamming the server with websocket connections
|