@fluidframework/container-runtime 0.56.7 → 0.57.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blobManager.d.ts.map +1 -1
- package/dist/blobManager.js +9 -1
- package/dist/blobManager.js.map +1 -1
- package/dist/connectionTelemetry.d.ts.map +1 -1
- package/dist/connectionTelemetry.js +6 -6
- package/dist/connectionTelemetry.js.map +1 -1
- package/dist/containerRuntime.d.ts +68 -28
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +148 -89
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.d.ts +27 -0
- package/dist/dataStore.d.ts.map +1 -0
- package/dist/dataStore.js +113 -0
- package/dist/dataStore.js.map +1 -0
- package/dist/dataStoreContext.d.ts +1 -7
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +10 -6
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +9 -5
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +14 -19
- package/dist/dataStores.js.map +1 -1
- package/dist/garbageCollection.d.ts +66 -27
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +272 -97
- package/dist/garbageCollection.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/runningSummarizer.d.ts +1 -0
- package/dist/runningSummarizer.d.ts.map +1 -1
- package/dist/runningSummarizer.js +23 -15
- package/dist/runningSummarizer.js.map +1 -1
- package/dist/summarizerTypes.d.ts +4 -6
- package/dist/summarizerTypes.d.ts.map +1 -1
- package/dist/summarizerTypes.js.map +1 -1
- package/dist/summaryGenerator.d.ts +2 -1
- package/dist/summaryGenerator.d.ts.map +1 -1
- package/dist/summaryGenerator.js +46 -29
- package/dist/summaryGenerator.js.map +1 -1
- package/lib/blobManager.d.ts.map +1 -1
- package/lib/blobManager.js +9 -1
- package/lib/blobManager.js.map +1 -1
- package/lib/connectionTelemetry.d.ts.map +1 -1
- package/lib/connectionTelemetry.js +6 -6
- package/lib/connectionTelemetry.js.map +1 -1
- package/lib/containerRuntime.d.ts +68 -28
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +149 -90
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.d.ts +27 -0
- package/lib/dataStore.d.ts.map +1 -0
- package/lib/dataStore.js +108 -0
- package/lib/dataStore.js.map +1 -0
- package/lib/dataStoreContext.d.ts +1 -7
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +10 -6
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStores.d.ts +9 -5
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +13 -18
- package/lib/dataStores.js.map +1 -1
- package/lib/garbageCollection.d.ts +66 -27
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +274 -99
- package/lib/garbageCollection.js.map +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/runningSummarizer.d.ts +1 -0
- package/lib/runningSummarizer.d.ts.map +1 -1
- package/lib/runningSummarizer.js +23 -15
- package/lib/runningSummarizer.js.map +1 -1
- package/lib/summarizerTypes.d.ts +4 -6
- package/lib/summarizerTypes.d.ts.map +1 -1
- package/lib/summarizerTypes.js.map +1 -1
- package/lib/summaryGenerator.d.ts +2 -1
- package/lib/summaryGenerator.d.ts.map +1 -1
- package/lib/summaryGenerator.js +46 -29
- package/lib/summaryGenerator.js.map +1 -1
- package/package.json +13 -13
- package/src/blobManager.ts +12 -1
- package/src/connectionTelemetry.ts +7 -6
- package/src/containerRuntime.ts +244 -115
- package/src/dataStore.ts +151 -0
- package/src/dataStoreContext.ts +11 -14
- package/src/dataStores.ts +23 -38
- package/src/garbageCollection.ts +385 -150
- package/src/index.ts +2 -1
- package/src/packageVersion.ts +1 -1
- package/src/runningSummarizer.ts +25 -16
- package/src/summarizerTypes.ts +4 -8
- package/src/summaryGenerator.ts +71 -23
package/lib/garbageCollection.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
import { assert, LazyPromise, Timer } from "@fluidframework/common-utils";
|
|
6
|
-
import { ClientSessionExpiredError } from "@fluidframework/container-utils";
|
|
6
|
+
import { ClientSessionExpiredError, DataProcessingError } from "@fluidframework/container-utils";
|
|
7
7
|
import { cloneGCData, concatGarbageCollectionStates, concatGarbageCollectionData, runGarbageCollection, unpackChildNodesGCDetails, } from "@fluidframework/garbage-collector";
|
|
8
8
|
import { gcBlobKey, } from "@fluidframework/runtime-definitions";
|
|
9
9
|
import { SummaryTreeBuilder, } from "@fluidframework/runtime-utils";
|
|
10
|
-
import { ChildLogger, loggerToMonitoringContext, PerformanceEvent, } from "@fluidframework/telemetry-utils";
|
|
10
|
+
import { ChildLogger, loggerToMonitoringContext, PerformanceEvent, TelemetryDataTag, } from "@fluidframework/telemetry-utils";
|
|
11
|
+
import { RuntimeHeaders } from ".";
|
|
11
12
|
import { getSummaryForDatastores } from "./dataStores";
|
|
12
13
|
import { getGCVersion, metadataBlobName, dataStoreAttributesBlobName, } from "./summaryFormat";
|
|
13
14
|
/** This is the current version of garbage collection. */
|
|
@@ -24,45 +25,55 @@ const gcTestModeKey = "Fluid.GarbageCollection.GCTestMode";
|
|
|
24
25
|
const runSweepKey = "Fluid.GarbageCollection.RunSweep";
|
|
25
26
|
// Feature gate key to write GC data at the root of the summary tree.
|
|
26
27
|
const writeAtRootKey = "Fluid.GarbageCollection.WriteDataAtRoot";
|
|
28
|
+
// Feature gate key to expire a session after a set period of time.
|
|
29
|
+
const runSessionExpiry = "Fluid.GarbageCollection.RunSessionExpiry";
|
|
27
30
|
const defaultDeleteTimeoutMs = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
31
|
+
const defaultSessionExpiryDurationMs = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
32
|
+
;
|
|
28
33
|
/**
|
|
29
34
|
* Helper class that tracks the state of an unreferenced node such as the time it was unreferenced. It also sets
|
|
30
35
|
* the node's state to inactive if it remains unreferenced for a given amount of time (inactiveTimeoutMs).
|
|
31
36
|
*/
|
|
32
37
|
class UnreferencedStateTracker {
|
|
33
|
-
constructor(unreferencedTimestampMs, inactiveTimeoutMs) {
|
|
38
|
+
constructor(unreferencedTimestampMs, inactiveTimeoutMs, currentReferenceTimestampMs) {
|
|
34
39
|
this.unreferencedTimestampMs = unreferencedTimestampMs;
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (inactiveTimeoutMs <= 0) {
|
|
42
|
-
this.inactive = true;
|
|
40
|
+
this.inactiveTimeoutMs = inactiveTimeoutMs;
|
|
41
|
+
this._inactive = false;
|
|
42
|
+
// If there is no current reference timestamp, don't track the node's inactive state. This will happen later
|
|
43
|
+
// when updateTracking is called with a reference timestamp.
|
|
44
|
+
if (currentReferenceTimestampMs !== undefined) {
|
|
45
|
+
this.updateTracking(currentReferenceTimestampMs);
|
|
43
46
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
}
|
|
48
|
+
get inactive() {
|
|
49
|
+
return this._inactive;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Updates the tracking state based on the provided timestamp.
|
|
53
|
+
*/
|
|
54
|
+
updateTracking(currentReferenceTimestampMs) {
|
|
55
|
+
var _a;
|
|
56
|
+
const unreferencedDurationMs = currentReferenceTimestampMs - this.unreferencedTimestampMs;
|
|
57
|
+
// If the timeout has already expired, the node has become inactive.
|
|
58
|
+
if (unreferencedDurationMs > this.inactiveTimeoutMs) {
|
|
59
|
+
this._inactive = true;
|
|
60
|
+
(_a = this.timer) === null || _a === void 0 ? void 0 : _a.clear();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// The node isn't inactive yet. Restart a timer for the duration remaining for it to become inactive.
|
|
64
|
+
const remainingDurationMs = this.inactiveTimeoutMs - unreferencedDurationMs;
|
|
65
|
+
if (this.timer === undefined) {
|
|
66
|
+
this.timer = new Timer(remainingDurationMs, () => { this._inactive = true; });
|
|
47
67
|
}
|
|
68
|
+
this.timer.restart(remainingDurationMs);
|
|
48
69
|
}
|
|
49
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Stop tracking this node. Reset the unreferenced timer, if any, and reset inactive state.
|
|
72
|
+
*/
|
|
50
73
|
stopTracking() {
|
|
51
74
|
var _a;
|
|
52
75
|
(_a = this.timer) === null || _a === void 0 ? void 0 : _a.clear();
|
|
53
|
-
this.
|
|
54
|
-
}
|
|
55
|
-
/** Logs an error with the given properties if the node is inactive. */
|
|
56
|
-
logIfInactive(logger, eventName, currentTimestampMs, deleteTimeoutMs, inactiveNodeId) {
|
|
57
|
-
if (this.inactive && !this.inactiveEventsLogged.has(eventName)) {
|
|
58
|
-
logger.sendErrorEvent({
|
|
59
|
-
eventName,
|
|
60
|
-
age: currentTimestampMs - this.unreferencedTimestampMs,
|
|
61
|
-
timeout: deleteTimeoutMs,
|
|
62
|
-
id: inactiveNodeId,
|
|
63
|
-
});
|
|
64
|
-
this.inactiveEventsLogged.add(eventName);
|
|
65
|
-
}
|
|
76
|
+
this._inactive = false;
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
/**
|
|
@@ -73,14 +84,23 @@ export class GarbageCollector {
|
|
|
73
84
|
constructor(provider, gcOptions,
|
|
74
85
|
/** After GC has run, called to delete objects in the runtime whose routes are unused. */
|
|
75
86
|
deleteUnusedRoutes,
|
|
76
|
-
/**
|
|
77
|
-
|
|
87
|
+
/** For a given node path, returns the node's package path. */
|
|
88
|
+
getNodePackagePath,
|
|
89
|
+
/**
|
|
90
|
+
* Returns a referenced timestamp to be used to track unreferenced nodes. This is a server generated timestamp
|
|
91
|
+
* and may not be available if there aren't any ops processed yet. If so, we skip tracking unreferenced state
|
|
92
|
+
* such as time when node becomes unreferenced or inactive.
|
|
93
|
+
*/
|
|
94
|
+
getCurrentReferenceTimestampMs,
|
|
95
|
+
/** Returns the timestamp of the last summary generated for this container. */
|
|
96
|
+
getLastSummaryTimestampMs, baseSnapshot, readAndParseBlob, baseLogger, existing, metadata) {
|
|
78
97
|
var _a, _b, _c, _d, _e;
|
|
79
98
|
this.provider = provider;
|
|
80
99
|
this.gcOptions = gcOptions;
|
|
81
100
|
this.deleteUnusedRoutes = deleteUnusedRoutes;
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
101
|
+
this.getNodePackagePath = getNodePackagePath;
|
|
102
|
+
this.getCurrentReferenceTimestampMs = getCurrentReferenceTimestampMs;
|
|
103
|
+
this.getLastSummaryTimestampMs = getLastSummaryTimestampMs;
|
|
84
104
|
/**
|
|
85
105
|
* Tells whether the GC data should be written to the root of the summary tree.
|
|
86
106
|
*/
|
|
@@ -103,6 +123,11 @@ export class GarbageCollector {
|
|
|
103
123
|
this.referencesSinceLastRun = new Map();
|
|
104
124
|
// Map of node ids to their unreferenced state tracker.
|
|
105
125
|
this.unreferencedNodesState = new Map();
|
|
126
|
+
// Keeps track of unreferenced events that are logged for a node. This is used to limit the log generation to one
|
|
127
|
+
// per event per node.
|
|
128
|
+
this.loggedUnreferencedEvents = new Set();
|
|
129
|
+
// Queue for unreferenced events that should be logged the next time GC runs.
|
|
130
|
+
this.pendingEventsQueue = [];
|
|
106
131
|
this.mc = loggerToMonitoringContext(ChildLogger.create(baseLogger, "GarbageCollector"));
|
|
107
132
|
this.deleteTimeoutMs = (_a = this.gcOptions.deleteTimeoutMs) !== null && _a !== void 0 ? _a : defaultDeleteTimeoutMs;
|
|
108
133
|
let prevSummaryGCVersion;
|
|
@@ -119,12 +144,19 @@ export class GarbageCollector {
|
|
|
119
144
|
else {
|
|
120
145
|
// For new documents, GC has to be exlicitly enabled via the gcAllowed flag in GC options.
|
|
121
146
|
this.gcEnabled = gcOptions.gcAllowed === true;
|
|
122
|
-
|
|
147
|
+
// Set the Session Expiry only if the flag is enabled or the test option is set.
|
|
148
|
+
if (this.mc.config.getBoolean(runSessionExpiry) && this.gcEnabled) {
|
|
149
|
+
this.sessionExpiryTimeoutMs = defaultSessionExpiryDurationMs;
|
|
150
|
+
}
|
|
123
151
|
}
|
|
124
152
|
// If session expiry is enabled, we need to close the container when the timeout expires
|
|
125
153
|
if (this.sessionExpiryTimeoutMs !== undefined) {
|
|
126
|
-
const
|
|
127
|
-
|
|
154
|
+
const timeoutMs = this.sessionExpiryTimeoutMs;
|
|
155
|
+
setLongTimeout(timeoutMs, () => {
|
|
156
|
+
this.provider.closeFn(new ClientSessionExpiredError(`Client session expired.`, timeoutMs));
|
|
157
|
+
}, (timer) => {
|
|
158
|
+
this.sessionExpiryTimer = timer;
|
|
159
|
+
});
|
|
128
160
|
}
|
|
129
161
|
// For existing document, the latest summary is the one that we loaded from. So, use its GC version as the
|
|
130
162
|
// latest tracked GC version. For new documents, we will be writing the first summary with the current version.
|
|
@@ -204,22 +236,21 @@ export class GarbageCollector {
|
|
|
204
236
|
// very first summary generated by detached container. In both cases, GC was not run - return undefined.
|
|
205
237
|
return Object.keys(gcState.gcNodes).length === 1 ? undefined : gcState;
|
|
206
238
|
});
|
|
207
|
-
|
|
208
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Set up the initializer which initializes the base GC state from the base snapshot. Note that the reference
|
|
241
|
+
* timestamp maybe from old ops which were not summarized and stored in the file. So, the unreferenced state
|
|
242
|
+
* may be out of date. This is fine because the state is updated every time GC runs based on the time then.
|
|
243
|
+
*/
|
|
209
244
|
this.initializeBaseStateP = new LazyPromise(async () => {
|
|
210
|
-
const
|
|
245
|
+
const currentReferenceTimestampMs = this.getCurrentReferenceTimestampMs();
|
|
211
246
|
const baseState = await baseSummaryStateP;
|
|
212
247
|
if (baseState === undefined) {
|
|
213
248
|
return;
|
|
214
249
|
}
|
|
215
250
|
const gcNodes = {};
|
|
216
251
|
for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// Get how long it has been since the node was unreferenced. Start a timeout for the remaining time
|
|
220
|
-
// left for it to be eligible for deletion.
|
|
221
|
-
const unreferencedDurationMs = currentTimestampMs - unreferencedTimestampMs;
|
|
222
|
-
this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(unreferencedTimestampMs, this.deleteTimeoutMs - unreferencedDurationMs));
|
|
252
|
+
if (nodeData.unreferencedTimestampMs !== undefined) {
|
|
253
|
+
this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(nodeData.unreferencedTimestampMs, this.deleteTimeoutMs, currentReferenceTimestampMs));
|
|
223
254
|
}
|
|
224
255
|
gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
|
|
225
256
|
}
|
|
@@ -253,9 +284,22 @@ export class GarbageCollector {
|
|
|
253
284
|
}
|
|
254
285
|
return dataStoreGCDetailsMap;
|
|
255
286
|
});
|
|
287
|
+
// Initialize the base state. The base GC data is used to detect and log when inactive / deleted objects are
|
|
288
|
+
// used in the container.
|
|
289
|
+
if (this.shouldRunGC) {
|
|
290
|
+
this.initializeBaseStateP.catch((error) => {
|
|
291
|
+
throw new DataProcessingError(error === null || error === void 0 ? void 0 : error.message, "FailedToInitializeGC", {
|
|
292
|
+
gcEnabled: this.gcEnabled,
|
|
293
|
+
runSweep: this.shouldRunSweep,
|
|
294
|
+
writeAtRoot: this._writeDataAtRoot,
|
|
295
|
+
testMode: this.testMode,
|
|
296
|
+
sessionExpiry: this.sessionExpiryTimeoutMs,
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
256
300
|
}
|
|
257
|
-
static create(provider, gcOptions, deleteUnusedRoutes,
|
|
258
|
-
return new GarbageCollector(provider, gcOptions, deleteUnusedRoutes,
|
|
301
|
+
static create(provider, gcOptions, deleteUnusedRoutes, getNodePackagePath, getCurrentReferenceTimestampMs, getLastSummaryTimestampMs, baseSnapshot, readAndParseBlob, baseLogger, existing, metadata) {
|
|
302
|
+
return new GarbageCollector(provider, gcOptions, deleteUnusedRoutes, getNodePackagePath, getCurrentReferenceTimestampMs, getLastSummaryTimestampMs, baseSnapshot, readAndParseBlob, baseLogger, existing, metadata);
|
|
259
303
|
}
|
|
260
304
|
/**
|
|
261
305
|
* This tracks two things:
|
|
@@ -290,29 +334,26 @@ export class GarbageCollector {
|
|
|
290
334
|
await this.initializeBaseStateP;
|
|
291
335
|
// Let the runtime update its pending state before GC runs.
|
|
292
336
|
await this.provider.updateStateBeforeGC();
|
|
293
|
-
const gcStats = {};
|
|
294
337
|
// Get the runtime's GC data and run GC on the reference graph in it.
|
|
295
338
|
const gcData = await this.provider.getGCData(fullGC);
|
|
296
|
-
this.updateStateSinceLatestRun(gcData);
|
|
297
339
|
const gcResult = runGarbageCollection(gcData.gcNodes, ["/"], logger);
|
|
298
|
-
const
|
|
340
|
+
const gcStats = this.generateStatsAndLogEvents(gcResult);
|
|
341
|
+
// Update the state since the last GC run. There can be nodes that were referenced between the last and
|
|
342
|
+
// the current run. We need to identify than and update their unreferenced state if needed.
|
|
343
|
+
this.updateStateSinceLastRun(gcData);
|
|
299
344
|
// Update the current state of the system based on the GC run.
|
|
300
|
-
this.
|
|
301
|
-
|
|
345
|
+
const currentReferenceTimestampMs = this.getCurrentReferenceTimestampMs();
|
|
346
|
+
this.updateCurrentState(gcData, gcResult, currentReferenceTimestampMs);
|
|
347
|
+
this.provider.updateUsedRoutes(gcResult.referencedNodeIds, currentReferenceTimestampMs);
|
|
302
348
|
if (runSweep) {
|
|
303
349
|
// Placeholder for running sweep logic.
|
|
304
350
|
}
|
|
305
|
-
// Update stats to be reported in the peformance event.
|
|
306
|
-
gcStats.deletedNodes = gcResult.deletedNodeIds.length;
|
|
307
|
-
gcStats.totalNodes = gcResult.referencedNodeIds.length + gcResult.deletedNodeIds.length;
|
|
308
|
-
gcStats.deletedDataStores = dataStoreUsedStateStats.unusedNodeCount;
|
|
309
|
-
gcStats.totalDataStores = dataStoreUsedStateStats.totalNodeCount;
|
|
310
351
|
// If we are running in GC test mode, delete objects for unused routes. This enables testing scenarios
|
|
311
352
|
// involving access to deleted data.
|
|
312
353
|
if (this.testMode) {
|
|
313
354
|
this.deleteUnusedRoutes(gcResult.deletedNodeIds);
|
|
314
355
|
}
|
|
315
|
-
event.end(gcStats);
|
|
356
|
+
event.end(Object.assign({}, gcStats));
|
|
316
357
|
return gcStats;
|
|
317
358
|
}, { end: true, cancel: "error" });
|
|
318
359
|
}
|
|
@@ -366,32 +407,42 @@ export class GarbageCollector {
|
|
|
366
407
|
await this.updateSummaryGCVersionFromSnapshot(result.snapshot, readAndParseBlob);
|
|
367
408
|
}
|
|
368
409
|
/**
|
|
369
|
-
* Called when a node with the given id is
|
|
410
|
+
* Called when a node with the given id is updated. If the node is inactive, log an error.
|
|
411
|
+
* @param nodePath - The id of the node that changed.
|
|
412
|
+
* @param reason - Whether the node was loaded or changed.
|
|
413
|
+
* @param timestampMs - The timestamp when the node changed.
|
|
414
|
+
* @param packagePath - The package path of the node. This may not be available if the node hasn't been loaded yet.
|
|
415
|
+
* @param requestHeaders - If the node was loaded via request path, the headers in the request.
|
|
370
416
|
*/
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const nodeId = id.startsWith("/") ? id : `/${id}`;
|
|
375
|
-
(_a = this.unreferencedNodesState.get(nodeId)) === null || _a === void 0 ? void 0 : _a.logIfInactive(this.mc.logger, "inactiveObjectChanged", this.getCurrentTimestampMs(), this.deleteTimeoutMs, nodeId);
|
|
376
|
-
}
|
|
377
|
-
dispose() {
|
|
378
|
-
if (this.sessionExpiryTimer !== undefined) {
|
|
379
|
-
clearTimeout(this.sessionExpiryTimer);
|
|
380
|
-
this.sessionExpiryTimer = undefined;
|
|
417
|
+
nodeUpdated(nodePath, reason, timestampMs, packagePath, requestHeaders) {
|
|
418
|
+
if (!this.shouldRunGC) {
|
|
419
|
+
return;
|
|
381
420
|
}
|
|
421
|
+
this.logIfInactive(reason, nodePath, timestampMs, packagePath, requestHeaders);
|
|
382
422
|
}
|
|
383
423
|
/**
|
|
384
424
|
* Called when an outbound reference is added to a node. This is used to identify all nodes that have been
|
|
385
425
|
* referenced between summaries so that their unreferenced timestamp can be reset.
|
|
386
426
|
*
|
|
387
|
-
* @param
|
|
388
|
-
* @param
|
|
427
|
+
* @param fromNodePath - The node from which the reference is added.
|
|
428
|
+
* @param toNodePath - The node to which the reference is added.
|
|
389
429
|
*/
|
|
390
|
-
addedOutboundReference(
|
|
430
|
+
addedOutboundReference(fromNodePath, toNodePath) {
|
|
391
431
|
var _a;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
432
|
+
if (!this.shouldRunGC) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const outboundRoutes = (_a = this.referencesSinceLastRun.get(fromNodePath)) !== null && _a !== void 0 ? _a : [];
|
|
436
|
+
outboundRoutes.push(toNodePath);
|
|
437
|
+
this.referencesSinceLastRun.set(fromNodePath, outboundRoutes);
|
|
438
|
+
// If the node that got referenced is inactive, log an event as that may indicate use-after-delete.
|
|
439
|
+
this.logIfInactive("Revived", toNodePath);
|
|
440
|
+
}
|
|
441
|
+
dispose() {
|
|
442
|
+
if (this.sessionExpiryTimer !== undefined) {
|
|
443
|
+
clearTimeout(this.sessionExpiryTimer);
|
|
444
|
+
this.sessionExpiryTimer = undefined;
|
|
445
|
+
}
|
|
395
446
|
}
|
|
396
447
|
/**
|
|
397
448
|
* Update the latest summary GC version from the metadata blob in the given snapshot.
|
|
@@ -410,37 +461,43 @@ export class GarbageCollector {
|
|
|
410
461
|
* 3. Clears tracking for nodes that were unreferenced but became referenced in this run.
|
|
411
462
|
* @param gcData - The data representing the reference graph on which GC is run.
|
|
412
463
|
* @param gcResult - The result of the GC run on the gcData.
|
|
413
|
-
* @param
|
|
464
|
+
* @param currentReferenceTimestampMs - The timestamp to be used for unreferenced nodes' timestamp.
|
|
414
465
|
*/
|
|
415
|
-
updateCurrentState(gcData, gcResult,
|
|
466
|
+
updateCurrentState(gcData, gcResult, currentReferenceTimestampMs) {
|
|
416
467
|
this.gcDataFromLastRun = cloneGCData(gcData);
|
|
417
468
|
this.referencesSinceLastRun.clear();
|
|
418
|
-
// Iterate through the deleted nodes and start tracking if they became unreferenced in this run.
|
|
419
|
-
for (const nodeId of gcResult.deletedNodeIds) {
|
|
420
|
-
// The time when the node became unreferenced. This is added to the current GC state.
|
|
421
|
-
let unreferencedTimestampMs = currentTimestampMs;
|
|
422
|
-
const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
|
|
423
|
-
if (nodeStateTracker !== undefined) {
|
|
424
|
-
unreferencedTimestampMs = nodeStateTracker.unreferencedTimestampMs;
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
// Start tracking this node as it became unreferenced in this run.
|
|
428
|
-
this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(unreferencedTimestampMs, this.deleteTimeoutMs));
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
469
|
// Iterate through the referenced nodes and stop tracking if they were unreferenced before.
|
|
432
470
|
for (const nodeId of gcResult.referencedNodeIds) {
|
|
433
471
|
const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
|
|
434
472
|
if (nodeStateTracker !== undefined) {
|
|
435
|
-
// If this node has been unreferenced for longer than deleteTimeoutMs and is being referenced,
|
|
436
|
-
// log an error as this may mean the deleteTimeoutMs is not long enough.
|
|
437
|
-
nodeStateTracker.logIfInactive(this.mc.logger, "inactiveObjectRevived", currentTimestampMs, this.deleteTimeoutMs, nodeId);
|
|
438
473
|
// Stop tracking so as to clear out any running timers.
|
|
439
474
|
nodeStateTracker.stopTracking();
|
|
440
475
|
// Delete the node as we don't need to track it any more.
|
|
441
476
|
this.unreferencedNodesState.delete(nodeId);
|
|
442
477
|
}
|
|
443
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* If there is no current reference time, skip tracking when a node becomes unreferenced. This would happen
|
|
481
|
+
* if no ops have been processed ever and we still try to run GC. If so, there is nothing interesting to track
|
|
482
|
+
* anyway.
|
|
483
|
+
*/
|
|
484
|
+
if (currentReferenceTimestampMs === undefined) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* If a node became unreferenced in this run, start tracking it.
|
|
489
|
+
* If a node was already unreferenced, update its tracking information. Since the current reference time is
|
|
490
|
+
* from the ops seen, this will ensure that we keep updating the unreferenced state as time moves forward.
|
|
491
|
+
*/
|
|
492
|
+
for (const nodeId of gcResult.deletedNodeIds) {
|
|
493
|
+
const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
|
|
494
|
+
if (nodeStateTracker === undefined) {
|
|
495
|
+
this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(currentReferenceTimestampMs, this.deleteTimeoutMs, currentReferenceTimestampMs));
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
nodeStateTracker.updateTracking(currentReferenceTimestampMs);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
444
501
|
}
|
|
445
502
|
/**
|
|
446
503
|
* Since GC runs periodically, the GC data that is generated only tells us the state of the world at that point in
|
|
@@ -450,7 +507,7 @@ export class GarbageCollector {
|
|
|
450
507
|
* This function identifies nodes that were referenced since last run and removes their unreferenced state, if any.
|
|
451
508
|
* If these nodes are currently unreferenced, they will be assigned new unreferenced state by the current run.
|
|
452
509
|
*/
|
|
453
|
-
|
|
510
|
+
updateStateSinceLastRun(currentGCData) {
|
|
454
511
|
// If we haven't run GC before or no references were added since the last run, there is nothing to do.
|
|
455
512
|
if (this.gcDataFromLastRun === undefined || this.referencesSinceLastRun.size === 0) {
|
|
456
513
|
return;
|
|
@@ -506,8 +563,7 @@ export class GarbageCollector {
|
|
|
506
563
|
* @param currentGCData - The GC data (reference graph) from the current GC run.
|
|
507
564
|
*/
|
|
508
565
|
validateReferenceCorrectness(currentGCData) {
|
|
509
|
-
assert(this.gcDataFromLastRun !== undefined, 0x2b7
|
|
510
|
-
/* "Can't validate correctness without GC data from last run" */ );
|
|
566
|
+
assert(this.gcDataFromLastRun !== undefined, 0x2b7);
|
|
511
567
|
// Get a list of all the outbound routes (or references) in the current GC data.
|
|
512
568
|
const currentReferences = [];
|
|
513
569
|
for (const [nodeId, outboundRoutes] of Object.entries(currentGCData.gcNodes)) {
|
|
@@ -531,9 +587,9 @@ export class GarbageCollector {
|
|
|
531
587
|
// Validate that the current reference graph doesn't have references that we are not already aware of. If this
|
|
532
588
|
// happens, it might indicate data corruption since we may delete objects prematurely.
|
|
533
589
|
currentReferences.forEach((route) => {
|
|
534
|
-
// Validate references for data stores only
|
|
535
|
-
//
|
|
536
|
-
if (route
|
|
590
|
+
// Validate references for data stores only. Currently, layers below data stores don't have GC implemented
|
|
591
|
+
// so there is no guarantee their references will be notified.
|
|
592
|
+
if (isDataStoreNode(route) && !explicitReferences.includes(route)) {
|
|
537
593
|
/**
|
|
538
594
|
* The following log will be enabled once this issue is resolved:
|
|
539
595
|
* https://github.com/microsoft/FluidFramework/issues/8878.
|
|
@@ -547,11 +603,103 @@ export class GarbageCollector {
|
|
|
547
603
|
}
|
|
548
604
|
});
|
|
549
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Generates the stats of a garbage collection run from the given results of the run. Also, logs any pending events
|
|
608
|
+
* in the pendingEventsQueue.
|
|
609
|
+
* @param gcResult - The result of a GC run.
|
|
610
|
+
* @returns the GC stats of the GC run.
|
|
611
|
+
*/
|
|
612
|
+
generateStatsAndLogEvents(gcResult) {
|
|
613
|
+
// Log pending events for unreferenced nodes after GC has run. We should have the package data available for
|
|
614
|
+
// them now since the GC run should have loaded these nodes.
|
|
615
|
+
let event = this.pendingEventsQueue.shift();
|
|
616
|
+
while (event !== undefined) {
|
|
617
|
+
const pkg = this.getNodePackagePath(event.id);
|
|
618
|
+
this.mc.logger.sendErrorEvent(Object.assign(Object.assign({}, event), { pkg: pkg ? { value: `/${pkg.join("/")}`, tag: TelemetryDataTag.PackageData } : undefined }));
|
|
619
|
+
event = this.pendingEventsQueue.shift();
|
|
620
|
+
}
|
|
621
|
+
const gcStats = {
|
|
622
|
+
nodeCount: 0,
|
|
623
|
+
dataStoreCount: 0,
|
|
624
|
+
unrefNodeCount: 0,
|
|
625
|
+
unrefDataStoreCount: 0,
|
|
626
|
+
updatedNodeCount: 0,
|
|
627
|
+
updatedDataStoreCount: 0,
|
|
628
|
+
};
|
|
629
|
+
for (const nodeId of gcResult.referencedNodeIds) {
|
|
630
|
+
gcStats.nodeCount++;
|
|
631
|
+
const isDataStore = isDataStoreNode(nodeId);
|
|
632
|
+
if (isDataStore) {
|
|
633
|
+
gcStats.dataStoreCount++;
|
|
634
|
+
}
|
|
635
|
+
// If a referenced node has an entry in `unreferencedNodesState`, it was previously unreferenced. So, its
|
|
636
|
+
// reference state updated from the last GC run.
|
|
637
|
+
if (this.unreferencedNodesState.has(nodeId)) {
|
|
638
|
+
gcStats.updatedNodeCount++;
|
|
639
|
+
if (isDataStore) {
|
|
640
|
+
gcStats.updatedDataStoreCount++;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
for (const nodeId of gcResult.deletedNodeIds) {
|
|
645
|
+
gcStats.nodeCount++;
|
|
646
|
+
gcStats.unrefNodeCount++;
|
|
647
|
+
const isDataStore = isDataStoreNode(nodeId);
|
|
648
|
+
if (isDataStore) {
|
|
649
|
+
gcStats.dataStoreCount++;
|
|
650
|
+
gcStats.unrefDataStoreCount++;
|
|
651
|
+
}
|
|
652
|
+
// If an unreferenced node doesn't an entry in `unreferencedNodesState`, it was previously referenced. So,
|
|
653
|
+
// its reference state updated from the last GC run.
|
|
654
|
+
if (!this.unreferencedNodesState.has(nodeId)) {
|
|
655
|
+
gcStats.updatedNodeCount++;
|
|
656
|
+
if (isDataStore) {
|
|
657
|
+
gcStats.updatedDataStoreCount++;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return gcStats;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Logs an event if a node is inactive and is used.
|
|
665
|
+
*/
|
|
666
|
+
logIfInactive(eventSuffix, nodeId, currentReferenceTimestampMs = this.getCurrentReferenceTimestampMs(), packagePath, requestHeaders) {
|
|
667
|
+
// If there is no reference timestamp to work with, no ops have been processed after creation. If so, skip
|
|
668
|
+
// logging as nothing interesting would have happened worth logging.
|
|
669
|
+
if (currentReferenceTimestampMs === undefined) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const eventName = `inactiveObject_${eventSuffix}`;
|
|
673
|
+
// We log a particular event for a given node only once so that it is not too noisy.
|
|
674
|
+
const uniqueEventId = `${nodeId}-${eventName}`;
|
|
675
|
+
const nodeState = this.unreferencedNodesState.get(nodeId);
|
|
676
|
+
if ((nodeState === null || nodeState === void 0 ? void 0 : nodeState.inactive) && !this.loggedUnreferencedEvents.has(uniqueEventId)) {
|
|
677
|
+
this.loggedUnreferencedEvents.add(uniqueEventId);
|
|
678
|
+
const event = {
|
|
679
|
+
eventName,
|
|
680
|
+
id: nodeId,
|
|
681
|
+
age: currentReferenceTimestampMs - nodeState.unreferencedTimestampMs,
|
|
682
|
+
timeout: this.deleteTimeoutMs,
|
|
683
|
+
lastSummaryTime: this.getLastSummaryTimestampMs(),
|
|
684
|
+
externalRequest: requestHeaders === null || requestHeaders === void 0 ? void 0 : requestHeaders[RuntimeHeaders.externalRequest],
|
|
685
|
+
viaHandle: requestHeaders === null || requestHeaders === void 0 ? void 0 : requestHeaders[RuntimeHeaders.viaHandle],
|
|
686
|
+
};
|
|
687
|
+
// If the package data for the node exists, log immediately. Otherwise, queue it and it will be logged the
|
|
688
|
+
// next time GC runs as the package data should be available then.
|
|
689
|
+
const pkg = packagePath !== null && packagePath !== void 0 ? packagePath : this.getNodePackagePath(nodeId);
|
|
690
|
+
if (pkg !== undefined) {
|
|
691
|
+
this.mc.logger.sendErrorEvent(Object.assign(Object.assign({}, event), { pkg: { value: `/${pkg.join("/")}`, tag: TelemetryDataTag.PackageData } }));
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
this.pendingEventsQueue.push(event);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
550
698
|
}
|
|
551
699
|
/**
|
|
552
700
|
* Gets the garbage collection state from the given snapshot tree. The GC state may be written into multiple blobs.
|
|
553
701
|
* Merge the GC state from all such blobs and return the merged GC state.
|
|
554
|
-
*/
|
|
702
|
+
*/
|
|
555
703
|
async function getGCStateFromSnapshot(gcSnapshotTree, readAndParseBlob) {
|
|
556
704
|
let rootGCState = { gcNodes: {} };
|
|
557
705
|
for (const key of Object.keys(gcSnapshotTree.blobs)) {
|
|
@@ -570,4 +718,31 @@ async function getGCStateFromSnapshot(gcSnapshotTree, readAndParseBlob) {
|
|
|
570
718
|
}
|
|
571
719
|
return rootGCState;
|
|
572
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* setLongTimeout is used for timeouts longer than setTimeout's ~24.8 day max
|
|
723
|
+
* @param timeoutMs - the total time the timeout needs to last in ms
|
|
724
|
+
* @param timeoutFn - the function to execute when the timer ends
|
|
725
|
+
* @param setTimerFn - the function used to update your timer variable
|
|
726
|
+
*/
|
|
727
|
+
function setLongTimeout(timeoutMs, timeoutFn, setTimerFn) {
|
|
728
|
+
// The setTimeout max is 24.8 days before looping occurs.
|
|
729
|
+
const maxTimeout = 2147483647;
|
|
730
|
+
let timer;
|
|
731
|
+
if (timeoutMs > maxTimeout) {
|
|
732
|
+
const newTimeoutMs = timeoutMs - maxTimeout;
|
|
733
|
+
timer = setTimeout(() => setLongTimeout(newTimeoutMs, timeoutFn, setTimerFn), maxTimeout);
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
timer = setTimeout(() => timeoutFn(), timeoutMs);
|
|
737
|
+
}
|
|
738
|
+
setTimerFn(timer);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Given a GC nodeId, tells whether it belongs to a data store or not.
|
|
742
|
+
*/
|
|
743
|
+
function isDataStoreNode(nodeId) {
|
|
744
|
+
const pathParts = nodeId.split("/");
|
|
745
|
+
// Data store ids are in the format "/dataStoreId".
|
|
746
|
+
return pathParts.length === 2 && pathParts[1] !== "" ? true : false;
|
|
747
|
+
}
|
|
573
748
|
//# sourceMappingURL=garbageCollection.js.map
|