@fluidframework/container-runtime 1.1.0 → 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/containerRuntime.d.ts +1 -1
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +8 -8
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.d.ts +2 -2
- package/dist/dataStore.d.ts.map +1 -1
- package/dist/dataStore.js +2 -2
- package/dist/dataStore.js.map +1 -1
- package/dist/dataStoreContext.d.ts +4 -4
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +2 -2
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +6 -5
- package/dist/dataStores.js.map +1 -1
- package/dist/garbageCollection.d.ts +33 -14
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +243 -122
- package/dist/garbageCollection.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/summarizerTypes.d.ts +5 -2
- package/dist/summarizerTypes.d.ts.map +1 -1
- package/dist/summarizerTypes.js.map +1 -1
- package/dist/summaryFormat.d.ts +6 -3
- package/dist/summaryFormat.d.ts.map +1 -1
- package/dist/summaryFormat.js +6 -3
- package/dist/summaryFormat.js.map +1 -1
- package/dist/summaryGenerator.d.ts.map +1 -1
- package/dist/summaryGenerator.js +0 -1
- package/dist/summaryGenerator.js.map +1 -1
- package/garbageCollection.md +7 -7
- package/lib/containerRuntime.d.ts +1 -1
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +9 -9
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.d.ts +2 -2
- package/lib/dataStore.d.ts.map +1 -1
- package/lib/dataStore.js +2 -2
- package/lib/dataStore.js.map +1 -1
- package/lib/dataStoreContext.d.ts +4 -4
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStores.d.ts +2 -2
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +6 -5
- package/lib/dataStores.js.map +1 -1
- package/lib/garbageCollection.d.ts +33 -14
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +242 -121
- package/lib/garbageCollection.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/summarizerTypes.d.ts +5 -2
- package/lib/summarizerTypes.d.ts.map +1 -1
- package/lib/summarizerTypes.js.map +1 -1
- package/lib/summaryFormat.d.ts +6 -3
- package/lib/summaryFormat.d.ts.map +1 -1
- package/lib/summaryFormat.js +6 -3
- package/lib/summaryFormat.js.map +1 -1
- package/lib/summaryGenerator.d.ts.map +1 -1
- package/lib/summaryGenerator.js +0 -1
- package/lib/summaryGenerator.js.map +1 -1
- package/package.json +16 -16
- package/src/containerRuntime.ts +60 -58
- package/src/dataStore.ts +4 -4
- package/src/dataStoreContext.ts +4 -4
- package/src/dataStores.ts +5 -5
- package/src/garbageCollection.ts +308 -167
- package/src/packageVersion.ts +1 -1
- package/src/summarizerTypes.ts +6 -3
- package/src/summaryFormat.ts +6 -3
- package/src/summaryGenerator.ts +0 -2
package/lib/garbageCollection.js
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
6
|
+
var t = {};
|
|
7
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
8
|
+
t[p] = s[p];
|
|
9
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
10
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
11
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
12
|
+
t[p[i]] = s[p[i]];
|
|
13
|
+
}
|
|
14
|
+
return t;
|
|
15
|
+
};
|
|
5
16
|
import { assert, LazyPromise, Timer } from "@fluidframework/common-utils";
|
|
6
17
|
import { ClientSessionExpiredError, DataProcessingError, UsageError } from "@fluidframework/container-utils";
|
|
7
18
|
import { cloneGCData, concatGarbageCollectionStates, concatGarbageCollectionData, runGarbageCollection, unpackChildNodesGCDetails, } from "@fluidframework/garbage-collector";
|
|
@@ -20,10 +31,10 @@ export const gcTreeKey = "gc";
|
|
|
20
31
|
export const gcBlobPrefix = "__gc";
|
|
21
32
|
// Feature gate key to turn GC on / off.
|
|
22
33
|
const runGCKey = "Fluid.GarbageCollection.RunGC";
|
|
23
|
-
// Feature gate key to turn GC test mode on / off.
|
|
24
|
-
const gcTestModeKey = "Fluid.GarbageCollection.GCTestMode";
|
|
25
34
|
// Feature gate key to turn GC sweep on / off.
|
|
26
35
|
const runSweepKey = "Fluid.GarbageCollection.RunSweep";
|
|
36
|
+
// Feature gate key to turn GC test mode on / off.
|
|
37
|
+
const gcTestModeKey = "Fluid.GarbageCollection.GCTestMode";
|
|
27
38
|
// Feature gate key to write GC data at the root of the summary tree.
|
|
28
39
|
const writeAtRootKey = "Fluid.GarbageCollection.WriteDataAtRoot";
|
|
29
40
|
// Feature gate key to expire a session after a set period of time.
|
|
@@ -32,8 +43,12 @@ export const runSessionExpiryKey = "Fluid.GarbageCollection.RunSessionExpiry";
|
|
|
32
43
|
export const disableSessionExpiryKey = "Fluid.GarbageCollection.DisableSessionExpiry";
|
|
33
44
|
// Feature gate key to write the gc blob as a handle if the data is the same.
|
|
34
45
|
export const trackGCStateKey = "Fluid.GarbageCollection.TrackGCState";
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
// Feature gate key to turn GC sweep log off.
|
|
47
|
+
const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
|
|
48
|
+
// One day in milliseconds.
|
|
49
|
+
export const oneDayMs = 1 * 24 * 60 * 60 * 1000;
|
|
50
|
+
const defaultInactiveTimeoutMs = 7 * oneDayMs; // 7 days
|
|
51
|
+
export const defaultSessionExpiryDurationMs = 30 * oneDayMs; // 30 days
|
|
37
52
|
/** The types of GC nodes in the GC reference graph. */
|
|
38
53
|
export const GCNodeType = {
|
|
39
54
|
// Nodes that are for data stores.
|
|
@@ -45,50 +60,84 @@ export const GCNodeType = {
|
|
|
45
60
|
// Nodes that are neither of the above. For example, root node.
|
|
46
61
|
Other: "Other",
|
|
47
62
|
};
|
|
63
|
+
/** The state of node that is unreferenced. */
|
|
64
|
+
const UnreferencedState = {
|
|
65
|
+
/** The node is active, i.e., it can become referenced again. */
|
|
66
|
+
Active: "Active",
|
|
67
|
+
/** The node is inactive, i.e., it should not become referenced. */
|
|
68
|
+
Inactive: "Inactive",
|
|
69
|
+
/** The node is ready to be deleted by the sweep phase. */
|
|
70
|
+
SweepReady: "SweepReady",
|
|
71
|
+
};
|
|
48
72
|
/**
|
|
49
|
-
* Helper class that tracks the state of an unreferenced node such as the time it was unreferenced
|
|
50
|
-
*
|
|
73
|
+
* Helper class that tracks the state of an unreferenced node such as the time it was unreferenced and if it can
|
|
74
|
+
* be deleted by the sweep phase.
|
|
51
75
|
*/
|
|
52
76
|
class UnreferencedStateTracker {
|
|
53
|
-
constructor(unreferencedTimestampMs,
|
|
77
|
+
constructor(unreferencedTimestampMs,
|
|
78
|
+
/** The time after which node transitions to Inactive state. */
|
|
79
|
+
inactiveTimeoutMs,
|
|
80
|
+
/** The time after which node transitions to SweepReady state; undefined if session expiry is disabled. */
|
|
81
|
+
sweepTimeoutMs,
|
|
82
|
+
/** The current reference timestamp; undefined if no ops have ever been processed which can happen in tests. */
|
|
83
|
+
currentReferenceTimestampMs) {
|
|
54
84
|
this.unreferencedTimestampMs = unreferencedTimestampMs;
|
|
55
85
|
this.inactiveTimeoutMs = inactiveTimeoutMs;
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
//
|
|
86
|
+
this.sweepTimeoutMs = sweepTimeoutMs;
|
|
87
|
+
this._state = UnreferencedState.Active;
|
|
88
|
+
// If there is no current reference timestamp, don't track the node's unreferenced state. This will happen
|
|
89
|
+
// later when updateTracking is called with a reference timestamp.
|
|
59
90
|
if (currentReferenceTimestampMs !== undefined) {
|
|
60
91
|
this.updateTracking(currentReferenceTimestampMs);
|
|
61
92
|
}
|
|
62
93
|
}
|
|
63
|
-
get
|
|
64
|
-
return this.
|
|
94
|
+
get state() {
|
|
95
|
+
return this._state;
|
|
65
96
|
}
|
|
66
|
-
|
|
67
|
-
* Updates the tracking state based on the provided timestamp.
|
|
68
|
-
*/
|
|
97
|
+
/* Updates the unreferenced state based on the provided timestamp. */
|
|
69
98
|
updateTracking(currentReferenceTimestampMs) {
|
|
70
|
-
var _a;
|
|
71
99
|
const unreferencedDurationMs = currentReferenceTimestampMs - this.unreferencedTimestampMs;
|
|
72
|
-
// If the
|
|
73
|
-
if (unreferencedDurationMs
|
|
74
|
-
this.
|
|
75
|
-
|
|
100
|
+
// If the node has been unreferenced for sweep timeout amount of time, update the state to SweepReady.
|
|
101
|
+
if (this.sweepTimeoutMs !== undefined && unreferencedDurationMs >= this.sweepTimeoutMs) {
|
|
102
|
+
this._state = UnreferencedState.SweepReady;
|
|
103
|
+
this.clearTimers();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// If the node has been unreferenced for inactive timeoutMs amount of time, update the state to inactive.
|
|
107
|
+
// Also, start a timer for the sweep timeout.
|
|
108
|
+
if (unreferencedDurationMs >= this.inactiveTimeoutMs) {
|
|
109
|
+
this._state = UnreferencedState.Inactive;
|
|
110
|
+
this.clearTimers();
|
|
111
|
+
if (this.sweepTimeoutMs !== undefined) {
|
|
112
|
+
setLongTimeout(this.sweepTimeoutMs - unreferencedDurationMs, () => { this._state = UnreferencedState.SweepReady; }, (timer) => { this.sweepTimer = timer; });
|
|
113
|
+
}
|
|
76
114
|
return;
|
|
77
115
|
}
|
|
78
|
-
// The node
|
|
116
|
+
// The node is still active. Start the inactive timer for the remaining duration.
|
|
79
117
|
const remainingDurationMs = this.inactiveTimeoutMs - unreferencedDurationMs;
|
|
80
|
-
if (this.
|
|
81
|
-
|
|
118
|
+
if (this.inactiveTimer === undefined) {
|
|
119
|
+
const inactiveTimeoutHandler = () => {
|
|
120
|
+
this._state = UnreferencedState.Inactive;
|
|
121
|
+
// After the node becomes inactive, start the sweep timer after which the node will be ready for sweep.
|
|
122
|
+
if (this.sweepTimeoutMs !== undefined) {
|
|
123
|
+
setLongTimeout(this.sweepTimeoutMs - this.inactiveTimeoutMs, () => { this._state = UnreferencedState.SweepReady; }, (timer) => { this.sweepTimer = timer; });
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
this.inactiveTimer = new Timer(remainingDurationMs, () => inactiveTimeoutHandler());
|
|
82
127
|
}
|
|
83
|
-
this.
|
|
128
|
+
this.inactiveTimer.restart(remainingDurationMs);
|
|
84
129
|
}
|
|
85
|
-
|
|
86
|
-
* Stop tracking this node. Reset the unreferenced timer, if any, and reset inactive state.
|
|
87
|
-
*/
|
|
88
|
-
stopTracking() {
|
|
130
|
+
clearTimers() {
|
|
89
131
|
var _a;
|
|
90
|
-
(_a = this.
|
|
91
|
-
this.
|
|
132
|
+
(_a = this.inactiveTimer) === null || _a === void 0 ? void 0 : _a.clear();
|
|
133
|
+
if (this.sweepTimer !== undefined) {
|
|
134
|
+
clearTimeout(this.sweepTimer);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Stop tracking this node. Reset the unreferenced timers and state, if any. */
|
|
138
|
+
stopTracking() {
|
|
139
|
+
this.clearTimers();
|
|
140
|
+
this._state = UnreferencedState.Active;
|
|
92
141
|
}
|
|
93
142
|
}
|
|
94
143
|
/**
|
|
@@ -181,20 +230,21 @@ export class GarbageCollector {
|
|
|
181
230
|
this.sessionExpiryTimeoutMs = defaultSessionExpiryDurationMs;
|
|
182
231
|
}
|
|
183
232
|
}
|
|
184
|
-
// If session expiry is enabled, we need to close the container when the timeout expires
|
|
185
|
-
if (this.sessionExpiryTimeoutMs !== undefined
|
|
186
|
-
|
|
187
|
-
// If Test Override config is set, override Session Expiry timeout
|
|
233
|
+
// If session expiry is enabled, we need to close the container when the session expiry timeout expires.
|
|
234
|
+
if (this.sessionExpiryTimeoutMs !== undefined && this.mc.config.getBoolean(disableSessionExpiryKey) !== true) {
|
|
235
|
+
// If Test Override config is set, override Session Expiry timeout.
|
|
188
236
|
const overrideSessionExpiryTimeoutMs = this.mc.config.getNumber("Fluid.GarbageCollection.TestOverride.SessionExpiryMs");
|
|
189
|
-
|
|
190
|
-
|
|
237
|
+
const timeoutMs = overrideSessionExpiryTimeoutMs !== null && overrideSessionExpiryTimeoutMs !== void 0 ? overrideSessionExpiryTimeoutMs : this.sessionExpiryTimeoutMs;
|
|
238
|
+
setLongTimeout(timeoutMs, () => { this.runtime.closeFn(new ClientSessionExpiredError(`Client session expired.`, timeoutMs)); }, (timer) => { this.sessionExpiryTimer = timer; });
|
|
239
|
+
/**
|
|
240
|
+
* Sweep timeout is the time after which unreferenced content can be swept.
|
|
241
|
+
* Sweep timeout = session expiry timeout + snapshot cache expiry timeout + one day buffer. The buffer is
|
|
242
|
+
* added to account for any clock skew. We use server timestamps throughout so the skew should be minimal
|
|
243
|
+
* but make it one day to be safe.
|
|
244
|
+
*/
|
|
245
|
+
if (createParams.snapshotCacheExpiryMs !== undefined) {
|
|
246
|
+
this.sweepTimeoutMs = this.sessionExpiryTimeoutMs + createParams.snapshotCacheExpiryMs + oneDayMs;
|
|
191
247
|
}
|
|
192
|
-
const timeoutMs = this.sessionExpiryTimeoutMs;
|
|
193
|
-
setLongTimeout(timeoutMs, () => {
|
|
194
|
-
this.runtime.closeFn(new ClientSessionExpiredError(`Client session expired.`, timeoutMs));
|
|
195
|
-
}, (timer) => {
|
|
196
|
-
this.sessionExpiryTimer = timer;
|
|
197
|
-
});
|
|
198
248
|
}
|
|
199
249
|
// For existing document, the latest summary is the one that we loaded from. So, use its GC version as the
|
|
200
250
|
// latest tracked GC version. For new documents, we will be writing the first summary with the current version.
|
|
@@ -210,17 +260,23 @@ export class GarbageCollector {
|
|
|
210
260
|
this.gcEnabled
|
|
211
261
|
// GC must not be disabled via GC options.
|
|
212
262
|
&& !this.gcOptions.disableGC);
|
|
213
|
-
this.trackGCState = this.mc.config.getBoolean(trackGCStateKey) === true;
|
|
214
263
|
/**
|
|
215
264
|
* Whether sweep should run or not. The following conditions have to be met to run sweep:
|
|
216
265
|
* 1. Overall GC or mark phase must be enabled (this.shouldRunGC).
|
|
217
|
-
* 2.
|
|
218
|
-
*
|
|
266
|
+
* 2. Sweep timeout should be available. Without this, we wouldn't know when an object should be deleted.
|
|
267
|
+
* 3. Sweep should be enabled for this container (this.sweepEnabled). This can be overridden via runSweep
|
|
268
|
+
* feature flag.
|
|
219
269
|
*/
|
|
220
|
-
this.shouldRunSweep = this.shouldRunGC
|
|
270
|
+
this.shouldRunSweep = this.shouldRunGC
|
|
271
|
+
&& this.sweepTimeoutMs !== undefined
|
|
272
|
+
&& ((_c = this.mc.config.getBoolean(runSweepKey)) !== null && _c !== void 0 ? _c : this.sweepEnabled);
|
|
273
|
+
this.trackGCState = this.mc.config.getBoolean(trackGCStateKey) === true;
|
|
221
274
|
// Override inactive timeout if test config or gc options to override it is set.
|
|
222
|
-
this.inactiveTimeoutMs =
|
|
223
|
-
|
|
275
|
+
this.inactiveTimeoutMs = (_e = (_d = this.mc.config.getNumber("Fluid.GarbageCollection.TestOverride.InactiveTimeoutMs")) !== null && _d !== void 0 ? _d : this.gcOptions.inactiveTimeoutMs) !== null && _e !== void 0 ? _e : defaultInactiveTimeoutMs;
|
|
276
|
+
// Inactive timeout must be greater than sweep timeout since a node goes from active -> inactive -> sweep ready.
|
|
277
|
+
if (this.sweepTimeoutMs !== undefined && this.inactiveTimeoutMs > this.sweepTimeoutMs) {
|
|
278
|
+
throw new UsageError("inactive timeout should not be greated than the sweep timeout");
|
|
279
|
+
}
|
|
224
280
|
// Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
|
|
225
281
|
this.testMode = (_f = this.mc.config.getBoolean(gcTestModeKey)) !== null && _f !== void 0 ? _f : this.gcOptions.runGCInTestMode === true;
|
|
226
282
|
// GC state is written into root of the summary tree by default. Can be overridden via feature flag for now.
|
|
@@ -299,7 +355,7 @@ export class GarbageCollector {
|
|
|
299
355
|
const gcNodes = {};
|
|
300
356
|
for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
|
|
301
357
|
if (nodeData.unreferencedTimestampMs !== undefined) {
|
|
302
|
-
this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(nodeData.unreferencedTimestampMs, this.inactiveTimeoutMs, currentReferenceTimestampMs));
|
|
358
|
+
this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(nodeData.unreferencedTimestampMs, this.inactiveTimeoutMs, this.sweepTimeoutMs, currentReferenceTimestampMs));
|
|
303
359
|
}
|
|
304
360
|
gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
|
|
305
361
|
}
|
|
@@ -335,7 +391,7 @@ export class GarbageCollector {
|
|
|
335
391
|
});
|
|
336
392
|
// Log all the GC options and the state determined by the garbage collector. This is interesting only for the
|
|
337
393
|
// summarizer client since it is the only one that runs GC. It also helps keep the telemetry less noisy.
|
|
338
|
-
const gcConfigProps = JSON.stringify(Object.assign({ gcEnabled: this.gcEnabled, sweepEnabled: this.sweepEnabled, runGC: this.shouldRunGC, runSweep: this.shouldRunSweep, writeAtRoot: this._writeDataAtRoot, testMode: this.testMode, sessionExpiry: this.sessionExpiryTimeoutMs, inactiveTimeout: this.inactiveTimeoutMs, existing: createParams.existing }, this.gcOptions));
|
|
394
|
+
const gcConfigProps = JSON.stringify(Object.assign({ gcEnabled: this.gcEnabled, sweepEnabled: this.sweepEnabled, runGC: this.shouldRunGC, runSweep: this.shouldRunSweep, writeAtRoot: this._writeDataAtRoot, testMode: this.testMode, sessionExpiry: this.sessionExpiryTimeoutMs, inactiveTimeout: this.inactiveTimeoutMs, existing: createParams.existing, trackGCState: this.trackGCState }, this.gcOptions));
|
|
339
395
|
if (this.isSummarizerClient) {
|
|
340
396
|
this.mc.logger.sendTelemetryEvent({
|
|
341
397
|
eventName: "GarbageCollectorLoaded",
|
|
@@ -374,38 +430,52 @@ export class GarbageCollector {
|
|
|
374
430
|
* @returns the number of data stores that have been marked as unreferenced.
|
|
375
431
|
*/
|
|
376
432
|
async collectGarbage(options) {
|
|
377
|
-
const {
|
|
433
|
+
const { fullGC = this.gcOptions.runFullGC === true || this.summaryStateNeedsReset, } = options;
|
|
378
434
|
const logger = options.logger
|
|
379
435
|
? ChildLogger.create(options.logger, undefined, { all: { completedGCRuns: () => this.completedRuns } })
|
|
380
436
|
: this.mc.logger;
|
|
381
437
|
return PerformanceEvent.timedExecAsync(logger, { eventName: "GarbageCollection" }, async (event) => {
|
|
382
|
-
await this.
|
|
383
|
-
// Let the runtime update its pending state before GC runs.
|
|
384
|
-
await this.runtime.updateStateBeforeGC();
|
|
438
|
+
await this.runPreGCSteps();
|
|
385
439
|
// Get the runtime's GC data and run GC on the reference graph in it.
|
|
386
440
|
const gcData = await this.runtime.getGCData(fullGC);
|
|
387
441
|
const gcResult = runGarbageCollection(gcData.gcNodes, ["/"]);
|
|
388
|
-
const gcStats = this.
|
|
389
|
-
// Update the state since the last GC run. There can be nodes that were referenced between the last and
|
|
390
|
-
// the current run. We need to identify than and update their unreferenced state if needed.
|
|
391
|
-
this.updateStateSinceLastRun(gcData, logger);
|
|
392
|
-
// Update the current state of the system based on the GC run.
|
|
393
|
-
const currentReferenceTimestampMs = this.runtime.getCurrentReferenceTimestampMs();
|
|
394
|
-
this.updateCurrentState(gcData, gcResult, currentReferenceTimestampMs);
|
|
395
|
-
this.runtime.updateUsedRoutes(gcResult.referencedNodeIds, currentReferenceTimestampMs);
|
|
396
|
-
if (runSweep) {
|
|
397
|
-
// Placeholder for running sweep logic.
|
|
398
|
-
}
|
|
399
|
-
// If we are running in GC test mode, delete objects for unused routes. This enables testing scenarios
|
|
400
|
-
// involving access to deleted data.
|
|
401
|
-
if (this.testMode) {
|
|
402
|
-
this.runtime.deleteUnusedRoutes(gcResult.deletedNodeIds);
|
|
403
|
-
}
|
|
442
|
+
const gcStats = await this.runPostGCSteps(gcData, gcResult, logger);
|
|
404
443
|
event.end(Object.assign({}, gcStats));
|
|
405
444
|
this.completedRuns++;
|
|
406
445
|
return gcStats;
|
|
407
446
|
}, { end: true, cancel: "error" });
|
|
408
447
|
}
|
|
448
|
+
async runPreGCSteps() {
|
|
449
|
+
// Ensure that base state has been initialized.
|
|
450
|
+
await this.initializeBaseStateP;
|
|
451
|
+
// Let the runtime update its pending state before GC runs.
|
|
452
|
+
await this.runtime.updateStateBeforeGC();
|
|
453
|
+
}
|
|
454
|
+
async runPostGCSteps(gcData, gcResult, logger) {
|
|
455
|
+
// Generate statistics from the current run. This is done before updating the current state because it
|
|
456
|
+
// generates some of its data based on previous state of the system.
|
|
457
|
+
const gcStats = this.generateStats(gcResult);
|
|
458
|
+
// Update the state since the last GC run. There can be nodes that were referenced between the last and
|
|
459
|
+
// the current run. We need to identify than and update their unreferenced state if needed.
|
|
460
|
+
this.updateStateSinceLastRun(gcData, logger);
|
|
461
|
+
// Update the current state and update the runtime of all routes or ids that used as per the GC run.
|
|
462
|
+
const currentReferenceTimestampMs = this.runtime.getCurrentReferenceTimestampMs();
|
|
463
|
+
this.updateCurrentState(gcData, gcResult, currentReferenceTimestampMs);
|
|
464
|
+
this.runtime.updateUsedRoutes(gcResult.referencedNodeIds, currentReferenceTimestampMs);
|
|
465
|
+
// Log events for objects that are ready to be deleted by sweep. When we have sweep enabled, we will
|
|
466
|
+
// delete these objects here instead.
|
|
467
|
+
this.logSweepEvents(logger, currentReferenceTimestampMs);
|
|
468
|
+
// If we are running in GC test mode, delete objects for unused routes. This enables testing scenarios
|
|
469
|
+
// involving access to deleted data.
|
|
470
|
+
if (this.testMode) {
|
|
471
|
+
this.runtime.deleteUnusedRoutes(gcResult.deletedNodeIds);
|
|
472
|
+
}
|
|
473
|
+
// Log pending unreferenced events such as a node being used after inactive. This is done after GC runs and
|
|
474
|
+
// updates its state so that we don't send false positives based on intermediate state. For example, we may get
|
|
475
|
+
// reference to an unreferenced node from another unreferenced node which means the node wasn't revived.
|
|
476
|
+
await this.logUnreferencedEvents(logger);
|
|
477
|
+
return gcStats;
|
|
478
|
+
}
|
|
409
479
|
/**
|
|
410
480
|
* Summarizes the GC data and returns it as a summary tree.
|
|
411
481
|
* We current write the entire GC state in a single blob. This can be modified later to write multiple
|
|
@@ -517,7 +587,10 @@ export class GarbageCollector {
|
|
|
517
587
|
if (!this.shouldRunGC) {
|
|
518
588
|
return;
|
|
519
589
|
}
|
|
520
|
-
this.
|
|
590
|
+
const nodeStateTracker = this.unreferencedNodesState.get(nodePath);
|
|
591
|
+
if (nodeStateTracker && nodeStateTracker.state !== UnreferencedState.Active) {
|
|
592
|
+
this.inactiveNodeUsed(reason, nodePath, nodeStateTracker, undefined /* fromNodeId */, packagePath, timestampMs, requestHeaders);
|
|
593
|
+
}
|
|
521
594
|
}
|
|
522
595
|
/**
|
|
523
596
|
* Called when an outbound reference is added to a node. This is used to identify all nodes that have been
|
|
@@ -534,8 +607,10 @@ export class GarbageCollector {
|
|
|
534
607
|
const outboundRoutes = (_a = this.newReferencesSinceLastRun.get(fromNodePath)) !== null && _a !== void 0 ? _a : [];
|
|
535
608
|
outboundRoutes.push(toNodePath);
|
|
536
609
|
this.newReferencesSinceLastRun.set(fromNodePath, outboundRoutes);
|
|
537
|
-
|
|
538
|
-
|
|
610
|
+
const nodeStateTracker = this.unreferencedNodesState.get(toNodePath);
|
|
611
|
+
if (nodeStateTracker && nodeStateTracker.state !== UnreferencedState.Active) {
|
|
612
|
+
this.inactiveNodeUsed("Revived", toNodePath, nodeStateTracker, fromNodePath);
|
|
613
|
+
}
|
|
539
614
|
}
|
|
540
615
|
dispose() {
|
|
541
616
|
if (this.sessionExpiryTimer !== undefined) {
|
|
@@ -581,7 +656,7 @@ export class GarbageCollector {
|
|
|
581
656
|
for (const nodeId of gcResult.deletedNodeIds) {
|
|
582
657
|
const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
|
|
583
658
|
if (nodeStateTracker === undefined) {
|
|
584
|
-
this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(currentReferenceTimestampMs, this.inactiveTimeoutMs, currentReferenceTimestampMs));
|
|
659
|
+
this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(currentReferenceTimestampMs, this.inactiveTimeoutMs, this.sweepTimeoutMs, currentReferenceTimestampMs));
|
|
585
660
|
}
|
|
586
661
|
else {
|
|
587
662
|
nodeStateTracker.updateTracking(currentReferenceTimestampMs);
|
|
@@ -628,10 +703,10 @@ export class GarbageCollector {
|
|
|
628
703
|
* references added new outbound references before getting deleted, we need to detect them.
|
|
629
704
|
* 2. We need new outbound references since last run because some of them may have been deleted later. If those
|
|
630
705
|
* references added new outbound references before getting deleted, we need to detect them.
|
|
631
|
-
* 3. We need data from the current run because currently we may not detect when
|
|
632
|
-
* - We don't require
|
|
706
|
+
* 3. We need data from the current run because currently we may not detect when DDSes are referenced:
|
|
707
|
+
* - We don't require DDSes handles to be stored in a referenced DDS. For this, we need GC at DDS level
|
|
633
708
|
* which is tracked by https://github.com/microsoft/FluidFramework/issues/8470.
|
|
634
|
-
* - A new data store may have "root"
|
|
709
|
+
* - A new data store may have "root" DDSes already created and we don't detect them today.
|
|
635
710
|
*/
|
|
636
711
|
const gcDataSuperSet = concatGarbageCollectionData(this.previousGCDataFromLastRun, currentGCData);
|
|
637
712
|
this.newReferencesSinceLastRun.forEach((outboundRoutes, sourceNodeId) => {
|
|
@@ -699,21 +774,11 @@ export class GarbageCollector {
|
|
|
699
774
|
return missingExplicitReferences;
|
|
700
775
|
}
|
|
701
776
|
/**
|
|
702
|
-
* Generates the stats of a garbage collection run from the given results of the run.
|
|
703
|
-
* in the pendingEventsQueue. This should be called before updating the current state because it generates stats
|
|
704
|
-
* based on previous state of the system.
|
|
777
|
+
* Generates the stats of a garbage collection run from the given results of the run.
|
|
705
778
|
* @param gcResult - The result of a GC run.
|
|
706
779
|
* @returns the GC stats of the GC run.
|
|
707
780
|
*/
|
|
708
|
-
|
|
709
|
-
// Log pending events for unreferenced nodes after GC has run. We should have the package data available for
|
|
710
|
-
// them now since the GC run should have loaded these nodes.
|
|
711
|
-
let event = this.pendingEventsQueue.shift();
|
|
712
|
-
while (event !== undefined) {
|
|
713
|
-
const pkg = this.getNodePackagePath(event.id);
|
|
714
|
-
logger.sendErrorEvent(Object.assign(Object.assign({}, event), { pkg: pkg ? { value: `/${pkg.join("/")}`, tag: TelemetryDataTag.PackageData } : undefined }));
|
|
715
|
-
event = this.pendingEventsQueue.shift();
|
|
716
|
-
}
|
|
781
|
+
generateStats(gcResult) {
|
|
717
782
|
const gcStats = {
|
|
718
783
|
nodeCount: 0,
|
|
719
784
|
dataStoreCount: 0,
|
|
@@ -765,12 +830,53 @@ export class GarbageCollector {
|
|
|
765
830
|
return gcStats;
|
|
766
831
|
}
|
|
767
832
|
/**
|
|
768
|
-
*
|
|
833
|
+
* For nodes that are ready to sweep, log an event for now. Until we start running sweep which deletes objects,
|
|
834
|
+
* this will give us a view into how much deleted content a container has.
|
|
769
835
|
*/
|
|
770
|
-
|
|
836
|
+
logSweepEvents(logger, currentReferenceTimestampMs) {
|
|
837
|
+
if (this.mc.config.getBoolean(disableSweepLogKey) === true
|
|
838
|
+
|| currentReferenceTimestampMs === undefined
|
|
839
|
+
|| this.sweepTimeoutMs === undefined) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
this.unreferencedNodesState.forEach((nodeStateTracker, nodeId) => {
|
|
843
|
+
if (nodeStateTracker.state !== UnreferencedState.SweepReady) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const nodeType = this.runtime.getNodeType(nodeId);
|
|
847
|
+
if (nodeType !== GCNodeType.DataStore && nodeType !== GCNodeType.Blob) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
// Log deleted event for each node only once to reduce noise in telemetry.
|
|
851
|
+
const uniqueEventId = `Deleted-${nodeId}`;
|
|
852
|
+
if (this.loggedUnreferencedEvents.has(uniqueEventId)) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
this.loggedUnreferencedEvents.add(uniqueEventId);
|
|
856
|
+
logger.sendTelemetryEvent({
|
|
857
|
+
eventName: "GCObjectDeleted",
|
|
858
|
+
id: nodeId,
|
|
859
|
+
type: nodeType,
|
|
860
|
+
age: currentReferenceTimestampMs - nodeStateTracker.unreferencedTimestampMs,
|
|
861
|
+
timeout: this.sweepTimeoutMs,
|
|
862
|
+
completedGCRuns: this.completedRuns,
|
|
863
|
+
lastSummaryTime: this.getLastSummaryTimestampMs(),
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Called when an inactive node is used after. Queue up an event that will be logged next time GC runs.
|
|
869
|
+
*/
|
|
870
|
+
inactiveNodeUsed(usageType, nodeId, nodeStateTracker, fromNodeId, packagePath, currentReferenceTimestampMs = this.runtime.getCurrentReferenceTimestampMs(), requestHeaders) {
|
|
771
871
|
// If there is no reference timestamp to work with, no ops have been processed after creation. If so, skip
|
|
772
872
|
// logging as nothing interesting would have happened worth logging.
|
|
773
|
-
|
|
873
|
+
// If the node is active, skip logging.
|
|
874
|
+
if (currentReferenceTimestampMs === undefined || nodeStateTracker.state === UnreferencedState.Active) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
// For non-summarizer clients, only log "Loaded" type events since these objects may not be loaded in the
|
|
878
|
+
// summarizer clients if they are based off of user actions (such as scrolling to content for these objects).
|
|
879
|
+
if (!this.isSummarizerClient && usageType !== "Loaded") {
|
|
774
880
|
return;
|
|
775
881
|
}
|
|
776
882
|
// We only care about data stores and attachment blobs for this telemetry since GC only marks these objects
|
|
@@ -779,39 +885,54 @@ export class GarbageCollector {
|
|
|
779
885
|
if (nodeType !== GCNodeType.DataStore && nodeType !== GCNodeType.Blob) {
|
|
780
886
|
return;
|
|
781
887
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (
|
|
888
|
+
const state = nodeStateTracker.state;
|
|
889
|
+
const uniqueEventId = `${state}-${nodeId}-${usageType}`;
|
|
890
|
+
if (this.loggedUnreferencedEvents.has(uniqueEventId)) {
|
|
785
891
|
return;
|
|
786
892
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
893
|
+
this.loggedUnreferencedEvents.add(uniqueEventId);
|
|
894
|
+
const propsToLog = {
|
|
895
|
+
id: nodeId,
|
|
896
|
+
type: nodeType,
|
|
897
|
+
unrefTime: nodeStateTracker.unreferencedTimestampMs,
|
|
898
|
+
age: currentReferenceTimestampMs - nodeStateTracker.unreferencedTimestampMs,
|
|
899
|
+
timeout: nodeStateTracker.state === UnreferencedState.Inactive
|
|
900
|
+
? this.inactiveTimeoutMs
|
|
901
|
+
: this.sweepTimeoutMs,
|
|
902
|
+
completedGCRuns: this.completedRuns,
|
|
903
|
+
lastSummaryTime: this.getLastSummaryTimestampMs(),
|
|
904
|
+
externalRequest: requestHeaders === null || requestHeaders === void 0 ? void 0 : requestHeaders[RuntimeHeaders.externalRequest],
|
|
905
|
+
viaHandle: requestHeaders === null || requestHeaders === void 0 ? void 0 : requestHeaders[RuntimeHeaders.viaHandle],
|
|
906
|
+
fromId: fromNodeId,
|
|
907
|
+
};
|
|
908
|
+
// For summarizer client, queue the event so it is logged the next time GC runs if the event is still valid.
|
|
909
|
+
// For non-summarizer client, log the event now since GC won't run on it. This may result in false positives
|
|
910
|
+
// but it's a good signal nonetheless and we can consume it with a grain of salt.
|
|
911
|
+
if (this.isSummarizerClient) {
|
|
912
|
+
this.pendingEventsQueue.push(Object.assign(Object.assign({}, propsToLog), { usageType, state }));
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
this.mc.logger.sendErrorEvent(Object.assign(Object.assign({}, propsToLog), { eventName: `${state}Object_${usageType}`, pkg: packagePath ? { value: packagePath.join("/"), tag: TelemetryDataTag.CodeArtifact } : undefined }));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async logUnreferencedEvents(logger) {
|
|
919
|
+
for (const eventProps of this.pendingEventsQueue) {
|
|
920
|
+
const { usageType, state } = eventProps, propsToLog = __rest(eventProps, ["usageType", "state"]);
|
|
921
|
+
/**
|
|
922
|
+
* Revived event is logged only if the node is active. If the node is not active, the reference to it was
|
|
923
|
+
* from another unreferenced node and this scenario is not interesting to log.
|
|
924
|
+
* Loaded and Changed events are logged only if the node is not active. If the node is active, it was
|
|
925
|
+
* revived and a Revived event will be logged for it.
|
|
926
|
+
*/
|
|
927
|
+
const nodeStateTracker = this.unreferencedNodesState.get(eventProps.id);
|
|
928
|
+
const active = nodeStateTracker === undefined || nodeStateTracker.state === UnreferencedState.Active;
|
|
929
|
+
if ((usageType === "Revived") === active) {
|
|
930
|
+
const pkg = await this.getNodePackagePath(eventProps.id);
|
|
931
|
+
const fromPkg = eventProps.fromId ? await this.getNodePackagePath(eventProps.fromId) : undefined;
|
|
932
|
+
logger.sendErrorEvent(Object.assign(Object.assign({}, propsToLog), { eventName: `${state}Object_${usageType}`, pkg: pkg ? { value: pkg.join("/"), tag: TelemetryDataTag.CodeArtifact } : undefined, fromPkg: fromPkg ? { value: fromPkg.join("/"), tag: TelemetryDataTag.CodeArtifact } : undefined }));
|
|
813
933
|
}
|
|
814
934
|
}
|
|
935
|
+
this.pendingEventsQueue = [];
|
|
815
936
|
}
|
|
816
937
|
}
|
|
817
938
|
/**
|