@fluidframework/container-runtime 0.52.0-44610 → 0.53.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/containerHandleContext.d.ts +0 -1
- package/dist/containerHandleContext.d.ts.map +1 -1
- package/dist/containerHandleContext.js +0 -1
- package/dist/containerHandleContext.js.map +1 -1
- package/dist/containerRuntime.d.ts +18 -3
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +84 -42
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts +4 -1
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +16 -13
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +8 -8
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +20 -37
- package/dist/dataStores.js.map +1 -1
- package/dist/garbageCollection.d.ts +61 -14
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +275 -20
- 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 +11 -2
- package/dist/index.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +0 -1
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +0 -36
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/summarizer.d.ts +1 -3
- package/dist/summarizer.d.ts.map +1 -1
- package/dist/summarizer.js +0 -12
- package/dist/summarizer.js.map +1 -1
- package/dist/summarizerTypes.d.ts +2 -2
- package/dist/summarizerTypes.d.ts.map +1 -1
- package/dist/summarizerTypes.js.map +1 -1
- package/dist/summaryFormat.d.ts +9 -1
- package/dist/summaryFormat.d.ts.map +1 -1
- package/dist/summaryFormat.js.map +1 -1
- package/dist/summaryGenerator.d.ts.map +1 -1
- package/dist/summaryGenerator.js +1 -3
- package/dist/summaryGenerator.js.map +1 -1
- package/lib/containerHandleContext.d.ts +0 -1
- package/lib/containerHandleContext.d.ts.map +1 -1
- package/lib/containerHandleContext.js +0 -1
- package/lib/containerHandleContext.js.map +1 -1
- package/lib/containerRuntime.d.ts +18 -3
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +84 -43
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStoreContext.d.ts +4 -1
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +16 -13
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStores.d.ts +8 -8
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +23 -40
- package/lib/dataStores.js.map +1 -1
- package/lib/garbageCollection.d.ts +61 -14
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +276 -21
- 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 +2 -1
- package/lib/index.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +0 -1
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +0 -36
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/summarizer.d.ts +1 -3
- package/lib/summarizer.d.ts.map +1 -1
- package/lib/summarizer.js +0 -12
- package/lib/summarizer.js.map +1 -1
- package/lib/summarizerTypes.d.ts +2 -2
- package/lib/summarizerTypes.d.ts.map +1 -1
- package/lib/summarizerTypes.js.map +1 -1
- package/lib/summaryFormat.d.ts +9 -1
- package/lib/summaryFormat.d.ts.map +1 -1
- package/lib/summaryFormat.js.map +1 -1
- package/lib/summaryGenerator.d.ts.map +1 -1
- package/lib/summaryGenerator.js +1 -3
- package/lib/summaryGenerator.js.map +1 -1
- package/package.json +16 -16
- package/src/containerHandleContext.ts +0 -1
- package/src/containerRuntime.ts +110 -53
- package/src/dataStoreContext.ts +15 -14
- package/src/dataStores.ts +32 -50
- package/src/garbageCollection.ts +390 -18
- package/src/index.ts +20 -2
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +0 -43
- package/src/summarizer.ts +0 -15
- package/src/summarizerTypes.ts +1 -2
- package/src/summaryFormat.ts +10 -1
- package/src/summaryGenerator.ts +2 -3
package/src/garbageCollection.ts
CHANGED
|
@@ -4,24 +4,49 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
7
|
-
import {
|
|
7
|
+
import { assert, LazyPromise, Timer } from "@fluidframework/common-utils";
|
|
8
|
+
import {
|
|
9
|
+
concatGarbageCollectionStates,
|
|
10
|
+
unpackChildNodesGCDetails,
|
|
11
|
+
IGCResult,
|
|
12
|
+
runGarbageCollection,
|
|
13
|
+
} from "@fluidframework/garbage-collector";
|
|
8
14
|
import { ISnapshotTree } from "@fluidframework/protocol-definitions";
|
|
9
|
-
import {
|
|
10
|
-
|
|
15
|
+
import {
|
|
16
|
+
gcBlobKey,
|
|
17
|
+
IGarbageCollectionData,
|
|
18
|
+
IGarbageCollectionNodeData,
|
|
19
|
+
IGarbageCollectionState,
|
|
20
|
+
IGarbageCollectionSummaryDetails,
|
|
21
|
+
ISummaryTreeWithStats,
|
|
22
|
+
} from "@fluidframework/runtime-definitions";
|
|
23
|
+
import {
|
|
24
|
+
ReadAndParseBlob,
|
|
25
|
+
RefreshSummaryResult,
|
|
26
|
+
SummaryTreeBuilder,
|
|
27
|
+
} from "@fluidframework/runtime-utils";
|
|
11
28
|
import { ChildLogger, PerformanceEvent } from "@fluidframework/telemetry-utils";
|
|
12
29
|
|
|
13
30
|
import { IGCRuntimeOptions } from "./containerRuntime";
|
|
31
|
+
import { getSummaryForDatastores } from "./dataStores";
|
|
14
32
|
import { getLocalStorageFeatureGate } from "./localStorageFeatureGates";
|
|
15
33
|
import {
|
|
16
34
|
getGCVersion,
|
|
17
35
|
GCVersion,
|
|
18
36
|
IContainerRuntimeMetadata,
|
|
19
37
|
metadataBlobName,
|
|
38
|
+
ReadFluidDataStoreAttributes,
|
|
39
|
+
dataStoreAttributesBlobName,
|
|
20
40
|
} from "./summaryFormat";
|
|
21
41
|
|
|
22
42
|
/** This is the current version of garbage collection. */
|
|
23
43
|
const GCVersion = 1;
|
|
24
44
|
|
|
45
|
+
// The key for the GC tree in summary.
|
|
46
|
+
export const gcTreeKey = "gc";
|
|
47
|
+
// They prefix for GC blobs in the GC tree in summary.
|
|
48
|
+
export const gcBlobPrefix = "__gc";
|
|
49
|
+
|
|
25
50
|
// Local storage key to turn GC on / off.
|
|
26
51
|
const runGCKey = "FluidRunGC";
|
|
27
52
|
// Local storage key to turn GC test mode on / off.
|
|
@@ -29,6 +54,8 @@ const gcTestModeKey = "FluidGCTestMode";
|
|
|
29
54
|
// Local storage key to turn GC sweep on / off.
|
|
30
55
|
const runSweepKey = "FluidRunSweep";
|
|
31
56
|
|
|
57
|
+
const defaultDeleteTimeoutMs = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
58
|
+
|
|
32
59
|
/** The used state statistics of a node. */
|
|
33
60
|
export interface IUsedStateStats {
|
|
34
61
|
totalNodeCount: number;
|
|
@@ -48,7 +75,7 @@ export interface IGarbageCollectionRuntime {
|
|
|
48
75
|
/** Returns the garbage collection data of the runtime. */
|
|
49
76
|
getGCData(fullGC?: boolean): Promise<IGarbageCollectionData>;
|
|
50
77
|
/** After GC has run, called to notify the runtime of routes that are used in it. */
|
|
51
|
-
updateUsedRoutes(usedRoutes: string[]): IUsedStateStats;
|
|
78
|
+
updateUsedRoutes(usedRoutes: string[], gcTimestamp?: number): IUsedStateStats;
|
|
52
79
|
}
|
|
53
80
|
|
|
54
81
|
/** Defines the contract for the garbage collector. */
|
|
@@ -57,18 +84,77 @@ export interface IGarbageCollector {
|
|
|
57
84
|
readonly shouldRunGC: boolean;
|
|
58
85
|
/**
|
|
59
86
|
* This tracks two things:
|
|
60
|
-
* 1. Whether GC is enabled - If this is 0, GC is disabled. If this is
|
|
87
|
+
* 1. Whether GC is enabled - If this is 0, GC is disabled. If this is greater than 0, GC is enabled.
|
|
61
88
|
* 2. If GC is enabled, the version of GC used to generate the GC data written in a summary.
|
|
62
89
|
*/
|
|
63
90
|
readonly gcSummaryFeatureVersion: number;
|
|
64
91
|
/** Tells whether the GC version has changed compared to the version in the latest summary. */
|
|
65
92
|
readonly hasGCVersionChanged: boolean;
|
|
93
|
+
/** Tells whether GC data should be written to the root of the summary tree. */
|
|
94
|
+
readonly writeDataAtRoot: boolean;
|
|
66
95
|
/** Run garbage collection and update the reference / used state of the system. */
|
|
67
96
|
collectGarbage(
|
|
68
97
|
options: { logger?: ITelemetryLogger, runGC?: boolean, runSweep?: boolean, fullGC?: boolean },
|
|
69
98
|
): Promise<IGCStats>;
|
|
99
|
+
/** Summarizes the GC data and returns it as a summary tree. */
|
|
100
|
+
summarize(): ISummaryTreeWithStats | undefined;
|
|
101
|
+
/** Returns a map of each data store id to its GC details in the base summary. */
|
|
102
|
+
getDataStoreBaseGCDetails(): Promise<Map<string, IGarbageCollectionSummaryDetails>>;
|
|
70
103
|
/** Called when the latest summary of the system has been refreshed. */
|
|
71
104
|
latestSummaryStateRefreshed(result: RefreshSummaryResult, readAndParseBlob: ReadAndParseBlob): Promise<void>;
|
|
105
|
+
/** Called when a node is changed. Used to detect and log when an inactive node is changed. */
|
|
106
|
+
nodeChanged(id: string): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Helper class that tracks the state of an unreferenced node such as the time it was unreferenced. It also sets
|
|
111
|
+
* the node's state to inactive if it remains unreferenced for a given amount of time (inactiveTimeoutMs).
|
|
112
|
+
*/
|
|
113
|
+
class UnreferencedStateTracker {
|
|
114
|
+
private inactive: boolean = false;
|
|
115
|
+
// Keeps track of all inactive events that are logged. This is used to limit the log generation for each event to 1
|
|
116
|
+
// so that it is not noisy.
|
|
117
|
+
private readonly inactiveEventsLogged: Set<string> = new Set();
|
|
118
|
+
private readonly timer: Timer | undefined;
|
|
119
|
+
|
|
120
|
+
constructor(
|
|
121
|
+
public readonly unreferencedTimestampMs: number,
|
|
122
|
+
inactiveTimeoutMs: number,
|
|
123
|
+
) {
|
|
124
|
+
// If the timeout has already expired, the node should become inactive immediately. Otherwise, start a timer of
|
|
125
|
+
// inactiveTimeoutMs after which the node will become inactive.
|
|
126
|
+
if (inactiveTimeoutMs <= 0) {
|
|
127
|
+
this.inactive = true;
|
|
128
|
+
} else {
|
|
129
|
+
this.timer = new Timer(inactiveTimeoutMs, () => { this.inactive = true; });
|
|
130
|
+
this.timer.start();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Stop tracking this node. Reset the unreferenced timer, if any, and reset inactive state. */
|
|
135
|
+
public stopTracking() {
|
|
136
|
+
this.timer?.clear();
|
|
137
|
+
this.inactive = false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Logs an error with the given properties if the node is inactive. */
|
|
141
|
+
public logIfInactive(
|
|
142
|
+
logger: ITelemetryLogger,
|
|
143
|
+
eventName: string,
|
|
144
|
+
currentTimestampMs: number,
|
|
145
|
+
deleteTimeoutMs: number,
|
|
146
|
+
inactiveNodeId: string,
|
|
147
|
+
) {
|
|
148
|
+
if (this.inactive && !this.inactiveEventsLogged.has(eventName)) {
|
|
149
|
+
logger.sendErrorEvent({
|
|
150
|
+
eventName,
|
|
151
|
+
unreferencedDuratonMs: currentTimestampMs - this.unreferencedTimestampMs,
|
|
152
|
+
deleteTimeoutMs,
|
|
153
|
+
inactiveNodeId,
|
|
154
|
+
});
|
|
155
|
+
this.inactiveEventsLogged.add(eventName);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
72
158
|
}
|
|
73
159
|
|
|
74
160
|
/**
|
|
@@ -80,11 +166,24 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
80
166
|
provider: IGarbageCollectionRuntime,
|
|
81
167
|
gcOptions: IGCRuntimeOptions,
|
|
82
168
|
deleteUnusedRoutes: (unusedRoutes: string[]) => void,
|
|
169
|
+
getCurrentTimestampMs: () => number,
|
|
170
|
+
baseSnapshot: ISnapshotTree | undefined,
|
|
171
|
+
readAndParseBlob: ReadAndParseBlob,
|
|
83
172
|
baseLogger: ITelemetryLogger,
|
|
84
173
|
existing: boolean,
|
|
85
174
|
metadata?: IContainerRuntimeMetadata,
|
|
86
175
|
): IGarbageCollector {
|
|
87
|
-
return new GarbageCollector(
|
|
176
|
+
return new GarbageCollector(
|
|
177
|
+
provider,
|
|
178
|
+
gcOptions,
|
|
179
|
+
deleteUnusedRoutes,
|
|
180
|
+
getCurrentTimestampMs,
|
|
181
|
+
baseSnapshot,
|
|
182
|
+
readAndParseBlob,
|
|
183
|
+
baseLogger,
|
|
184
|
+
existing,
|
|
185
|
+
metadata,
|
|
186
|
+
);
|
|
88
187
|
}
|
|
89
188
|
|
|
90
189
|
/**
|
|
@@ -94,7 +193,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
94
193
|
|
|
95
194
|
/**
|
|
96
195
|
* This tracks two things:
|
|
97
|
-
* 1. Whether GC is enabled - If this is 0, GC is disabled. If this is
|
|
196
|
+
* 1. Whether GC is enabled - If this is 0, GC is disabled. If this is greater than 0, GC is enabled.
|
|
98
197
|
* 2. If GC is enabled, the version of GC used to generate the GC data written in a summary.
|
|
99
198
|
*/
|
|
100
199
|
public get gcSummaryFeatureVersion(): number {
|
|
@@ -120,25 +219,51 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
120
219
|
private readonly testMode: boolean;
|
|
121
220
|
private readonly logger: ITelemetryLogger;
|
|
122
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Tells whether the GC data should be written to the root of the summary tree. We do this under 2 conditions:
|
|
224
|
+
* 1. If `writeDataAtRoot` GC option is enabled.
|
|
225
|
+
* 2. If the base summary has the GC data written at the root. This is to support forward compatibility where when
|
|
226
|
+
* we start writing the GC data at root, older versions can detect that and write at root too.
|
|
227
|
+
*/
|
|
228
|
+
private _writeDataAtRoot: boolean = false;
|
|
229
|
+
public get writeDataAtRoot(): boolean {
|
|
230
|
+
return this._writeDataAtRoot;
|
|
231
|
+
}
|
|
232
|
+
|
|
123
233
|
// The current GC version that this container is running.
|
|
124
234
|
private readonly currentGCVersion = GCVersion;
|
|
125
235
|
// This is the version of GC data in the latest summary being tracked.
|
|
126
236
|
private latestSummaryGCVersion: GCVersion;
|
|
127
237
|
|
|
238
|
+
// The current state - each node's GC data and unreferenced timestamp.
|
|
239
|
+
private currentGCState: IGarbageCollectionState | undefined;
|
|
240
|
+
|
|
241
|
+
// Promise when resolved initializes the base state of the nodes from the base summary state.
|
|
242
|
+
private readonly initializeBaseStateP: Promise<void>;
|
|
243
|
+
// The map of data store ids to their GC details in the base summary returned in getDataStoreGCDetails().
|
|
244
|
+
private readonly dataStoreGCDetailsP: Promise<Map<string, IGarbageCollectionSummaryDetails>>;
|
|
245
|
+
// The time after which an unreferenced node can be deleted. Currently, we only set the node's state to expired.
|
|
246
|
+
private readonly deleteTimeoutMs: number;
|
|
247
|
+
// Map of node ids to their unreferenced state tracker.
|
|
248
|
+
private readonly unreferencedNodesState: Map<string, UnreferencedStateTracker> = new Map();
|
|
249
|
+
|
|
128
250
|
protected constructor(
|
|
129
251
|
private readonly provider: IGarbageCollectionRuntime,
|
|
130
252
|
private readonly gcOptions: IGCRuntimeOptions,
|
|
131
|
-
/**
|
|
132
|
-
* After GC has run, called to delete objects in the runtime whose routes are unused. This is not part of the
|
|
133
|
-
* provider because its specific to this garbage collector implementation and is not part of the contract.
|
|
134
|
-
*/
|
|
253
|
+
/** After GC has run, called to delete objects in the runtime whose routes are unused. */
|
|
135
254
|
private readonly deleteUnusedRoutes: (unusedRoutes: string[]) => void,
|
|
255
|
+
/** Returns the current timestamp to be assigned to nodes that become unreferenced. */
|
|
256
|
+
private readonly getCurrentTimestampMs: () => number,
|
|
257
|
+
baseSnapshot: ISnapshotTree | undefined,
|
|
258
|
+
readAndParseBlob: ReadAndParseBlob,
|
|
136
259
|
baseLogger: ITelemetryLogger,
|
|
137
260
|
existing: boolean,
|
|
138
261
|
metadata?: IContainerRuntimeMetadata,
|
|
139
262
|
) {
|
|
140
263
|
this.logger = ChildLogger.create(baseLogger, "GarbageCollector");
|
|
141
264
|
|
|
265
|
+
this.deleteTimeoutMs = this.gcOptions.deleteTimeoutMs ?? defaultDeleteTimeoutMs;
|
|
266
|
+
|
|
142
267
|
let prevSummaryGCVersion: number | undefined;
|
|
143
268
|
// GC can only be enabled during creation. After that, it can never be enabled again. So, for existing
|
|
144
269
|
// documents, we get this information from the metadata blob.
|
|
@@ -170,6 +295,127 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
170
295
|
|
|
171
296
|
// Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
|
|
172
297
|
this.testMode = getLocalStorageFeatureGate(gcTestModeKey) ?? gcOptions.runGCInTestMode === true;
|
|
298
|
+
|
|
299
|
+
// If `writeDataAtRoot` GC option is true, we should write the GC data into the root of the summary tree. This
|
|
300
|
+
// GC option is used for testing only. It will be removed once we start writing GC data into root by default.
|
|
301
|
+
this._writeDataAtRoot = this.gcOptions.writeDataAtRoot === true;
|
|
302
|
+
|
|
303
|
+
// Get the GC state from the GC blob in the base snapshot. Use LazyPromise because we only want to do
|
|
304
|
+
// this once since it involves fetching blobs from storage which is expensive.
|
|
305
|
+
const baseSummaryStateP = new LazyPromise<IGarbageCollectionState>(async () => {
|
|
306
|
+
if (baseSnapshot === undefined) {
|
|
307
|
+
return { gcNodes: {} };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// For newer documents, GC data should be present in the GC tree in the root of the snapshot.
|
|
311
|
+
const gcSnapshotTree = baseSnapshot.trees[gcTreeKey];
|
|
312
|
+
if (gcSnapshotTree !== undefined) {
|
|
313
|
+
// forward-compat - If a newer version has written the GC tree at root, we should also do the same.
|
|
314
|
+
this._writeDataAtRoot = true;
|
|
315
|
+
return getGCStateFromSnapshot(gcSnapshotTree, readAndParseBlob);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// back-compat - Older documents will have the GC blobs in each data store's summary tree. Get them and
|
|
319
|
+
// consolidate into IGarbageCollectionState format.
|
|
320
|
+
const gcState: IGarbageCollectionState = { gcNodes: { "/": { outboundRoutes: [] } } };
|
|
321
|
+
const dataStoreSnaphotTree = getSummaryForDatastores(baseSnapshot, metadata);
|
|
322
|
+
assert(dataStoreSnaphotTree !== undefined,
|
|
323
|
+
0x2a8 /* "Expected data store snapshot tree in base snapshot" */);
|
|
324
|
+
for (const [dsId, dsSnapshotTree] of Object.entries(dataStoreSnaphotTree.trees)) {
|
|
325
|
+
const blobId = dsSnapshotTree.blobs[gcBlobKey];
|
|
326
|
+
if (blobId === undefined) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const gcSummaryDetails = await readAndParseBlob<IGarbageCollectionSummaryDetails>(blobId);
|
|
331
|
+
// If there are no nodes for this data store, skip it.
|
|
332
|
+
if (gcSummaryDetails.gcData?.gcNodes === undefined) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const dsRootId = `/${dsId}`;
|
|
337
|
+
// Since we used to write GC data at data store level, we won't have an entry for the root ("/").
|
|
338
|
+
// Construct that entry by adding root data store ids to its outbound routes.
|
|
339
|
+
const initialSnapshotDetails = await readAndParseBlob<ReadFluidDataStoreAttributes>(
|
|
340
|
+
dsSnapshotTree.blobs[dataStoreAttributesBlobName],
|
|
341
|
+
);
|
|
342
|
+
if (initialSnapshotDetails.isRootDataStore) {
|
|
343
|
+
gcState.gcNodes["/"].outboundRoutes.push(dsRootId);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const [id, outboundRoutes] of Object.entries(gcSummaryDetails.gcData.gcNodes)) {
|
|
347
|
+
// Prefix the data store id to the GC node ids to make them relative to the root from being
|
|
348
|
+
// relative to the data store. Similar to how its done in DataStore::getGCData.
|
|
349
|
+
const rootId = id === "/" ? dsRootId : `${dsRootId}${id}`;
|
|
350
|
+
gcState.gcNodes[rootId] = { outboundRoutes: Array.from(outboundRoutes) };
|
|
351
|
+
}
|
|
352
|
+
assert(gcState.gcNodes[dsRootId] !== undefined,
|
|
353
|
+
0x2a9 /* `GC nodes for data store ${dsId} not in GC blob` */);
|
|
354
|
+
gcState.gcNodes[dsRootId].unreferencedTimestampMs = gcSummaryDetails.unrefTimestamp;
|
|
355
|
+
}
|
|
356
|
+
return gcState;
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Set up the initializer which initializes the base GC state from the base snapshot. Use lazy promise because
|
|
360
|
+
// we only do this once - the very first time we run GC.
|
|
361
|
+
this.initializeBaseStateP = new LazyPromise<void>(async () => {
|
|
362
|
+
const baseState = await baseSummaryStateP;
|
|
363
|
+
|
|
364
|
+
const gcNodes: { [ id: string ]: IGarbageCollectionNodeData } = {};
|
|
365
|
+
// Set up tracking for the nodes in the base summary state and add them to GC nodes.
|
|
366
|
+
for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
|
|
367
|
+
const unreferencedTimestampMs = nodeData.unreferencedTimestampMs;
|
|
368
|
+
if (unreferencedTimestampMs !== undefined) {
|
|
369
|
+
// Get how long it has been since the node was unreferenced. Start a timeout for the remaining time
|
|
370
|
+
// left for it to be eligible for deletion.
|
|
371
|
+
const unreferencedDurationMs = this.getCurrentTimestampMs() - unreferencedTimestampMs;
|
|
372
|
+
this.unreferencedNodesState.set(
|
|
373
|
+
nodeId,
|
|
374
|
+
new UnreferencedStateTracker(
|
|
375
|
+
unreferencedTimestampMs,
|
|
376
|
+
this.deleteTimeoutMs - unreferencedDurationMs,
|
|
377
|
+
),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
gcNodes[nodeId] = {
|
|
382
|
+
outboundRoutes: Array.from(nodeData.outboundRoutes),
|
|
383
|
+
unreferencedTimestampMs,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
this.currentGCState = { gcNodes };
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Get the GC details for each data store from the GC state in the base summary. This is returned in
|
|
390
|
+
// getDataStoreBaseGCDetails and is used to initialize each data store's base GC details.
|
|
391
|
+
this.dataStoreGCDetailsP = new LazyPromise<Map<string, IGarbageCollectionSummaryDetails>>(async () => {
|
|
392
|
+
const gcNodes: { [ id: string ]: string[] } = {};
|
|
393
|
+
const baseState = await baseSummaryStateP;
|
|
394
|
+
for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
|
|
395
|
+
gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
|
|
396
|
+
}
|
|
397
|
+
// Run GC on the nodes in the base summary to get the routes used in each node in the container.
|
|
398
|
+
// This is an optimization for space (vs performance) wherein we don't need to store the used routes of
|
|
399
|
+
// each node in the summary.
|
|
400
|
+
const usedRoutes = runGarbageCollection(
|
|
401
|
+
gcNodes,
|
|
402
|
+
[ "/" ],
|
|
403
|
+
this.logger,
|
|
404
|
+
).referencedNodeIds;
|
|
405
|
+
|
|
406
|
+
const dataStoreGCDetailsMap = unpackChildNodesGCDetails({ gcData: { gcNodes }, usedRoutes });
|
|
407
|
+
// Currently, the data stores write the GC data. So, we need to update it's base GC details with the
|
|
408
|
+
// unreferenced timestamp. Once we start writing the GC data here, we won't need to do this anymore.
|
|
409
|
+
for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
|
|
410
|
+
if (nodeData.unreferencedTimestampMs !== undefined) {
|
|
411
|
+
const dataStoreGCDetails = dataStoreGCDetailsMap.get(nodeId.slice(1));
|
|
412
|
+
if (dataStoreGCDetails !== undefined) {
|
|
413
|
+
dataStoreGCDetails.unrefTimestamp = nodeData.unreferencedTimestampMs;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return dataStoreGCDetailsMap;
|
|
418
|
+
});
|
|
173
419
|
}
|
|
174
420
|
|
|
175
421
|
/**
|
|
@@ -193,6 +439,8 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
193
439
|
} = options;
|
|
194
440
|
|
|
195
441
|
return PerformanceEvent.timedExecAsync(logger, { eventName: "GarbageCollection" }, async (event) => {
|
|
442
|
+
await this.initializeBaseStateP;
|
|
443
|
+
|
|
196
444
|
const gcStats: {
|
|
197
445
|
deletedNodes?: number,
|
|
198
446
|
totalNodes?: number,
|
|
@@ -202,30 +450,33 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
202
450
|
|
|
203
451
|
// Get the runtime's GC data and run GC on the reference graph in it.
|
|
204
452
|
const gcData = await this.provider.getGCData(fullGC);
|
|
205
|
-
const
|
|
453
|
+
const gcResult = runGarbageCollection(
|
|
206
454
|
gcData.gcNodes,
|
|
207
455
|
[ "/" ],
|
|
208
456
|
logger,
|
|
209
457
|
);
|
|
210
458
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
459
|
+
const currentTimestampMs = this.getCurrentTimestampMs();
|
|
460
|
+
// Update the current state of the system based on the GC run.
|
|
461
|
+
this.updateCurrentState(gcData, gcResult, currentTimestampMs);
|
|
462
|
+
|
|
463
|
+
const dataStoreUsedStateStats =
|
|
464
|
+
this.provider.updateUsedRoutes(gcResult.referencedNodeIds, currentTimestampMs);
|
|
214
465
|
|
|
215
466
|
if (runSweep) {
|
|
216
467
|
// Placeholder for running sweep logic.
|
|
217
468
|
}
|
|
218
469
|
|
|
219
470
|
// Update stats to be reported in the peformance event.
|
|
220
|
-
gcStats.deletedNodes = deletedNodeIds.length;
|
|
221
|
-
gcStats.totalNodes = referencedNodeIds.length + deletedNodeIds.length;
|
|
471
|
+
gcStats.deletedNodes = gcResult.deletedNodeIds.length;
|
|
472
|
+
gcStats.totalNodes = gcResult.referencedNodeIds.length + gcResult.deletedNodeIds.length;
|
|
222
473
|
gcStats.deletedDataStores = dataStoreUsedStateStats.unusedNodeCount;
|
|
223
474
|
gcStats.totalDataStores = dataStoreUsedStateStats.totalNodeCount;
|
|
224
475
|
|
|
225
476
|
// If we are running in GC test mode, delete objects for unused routes. This enables testing scenarios
|
|
226
477
|
// involving access to deleted data.
|
|
227
478
|
if (this.testMode) {
|
|
228
|
-
this.deleteUnusedRoutes(deletedNodeIds);
|
|
479
|
+
this.deleteUnusedRoutes(gcResult.deletedNodeIds);
|
|
229
480
|
}
|
|
230
481
|
event.end(gcStats);
|
|
231
482
|
return gcStats as IGCStats;
|
|
@@ -233,6 +484,29 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
233
484
|
{ end: true, cancel: "error" });
|
|
234
485
|
}
|
|
235
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Summarizes the GC data and returns it as a summary tree.
|
|
489
|
+
* We current write the entire GC state in a single blob. This can be modified later to write multiple
|
|
490
|
+
* blobs. All the blob keys should start with `gcBlobPrefix`.
|
|
491
|
+
*/
|
|
492
|
+
public summarize(): ISummaryTreeWithStats | undefined {
|
|
493
|
+
if (!this.shouldRunGC || this.currentGCState === undefined) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const builder = new SummaryTreeBuilder();
|
|
498
|
+
builder.addBlob(`${gcBlobPrefix}_root`, JSON.stringify(this.currentGCState));
|
|
499
|
+
return builder.getSummaryTree();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Returns a map of data store ids to their base GC details generated from the base summary.This is used to
|
|
504
|
+
* initialize the data stores with their base GC state.
|
|
505
|
+
*/
|
|
506
|
+
public async getDataStoreBaseGCDetails(): Promise<Map<string, IGarbageCollectionSummaryDetails>> {
|
|
507
|
+
return this.dataStoreGCDetailsP;
|
|
508
|
+
}
|
|
509
|
+
|
|
236
510
|
/**
|
|
237
511
|
* Called when the latest summary of the system has been refreshed. This will be used to update the state of the
|
|
238
512
|
* latest summary tracked.
|
|
@@ -256,6 +530,21 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
256
530
|
await this.updateSummaryGCVersionFromSnapshot(result.snapshot, readAndParseBlob);
|
|
257
531
|
}
|
|
258
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Called when a node with the given id is changed. If the node is inactive, log an error.
|
|
535
|
+
*/
|
|
536
|
+
public nodeChanged(id: string) {
|
|
537
|
+
// Prefix "/" if needed to make it relative to the root.
|
|
538
|
+
const nodeId = id.startsWith("/") ? id : `/${id}`;
|
|
539
|
+
this.unreferencedNodesState.get(nodeId)?.logIfInactive(
|
|
540
|
+
this.logger,
|
|
541
|
+
"inactiveObjectChanged",
|
|
542
|
+
this.getCurrentTimestampMs(),
|
|
543
|
+
this.deleteTimeoutMs,
|
|
544
|
+
nodeId,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
259
548
|
/**
|
|
260
549
|
* Update the latest summary GC version from the metadata blob in the given snapshot.
|
|
261
550
|
*/
|
|
@@ -266,4 +555,87 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
266
555
|
this.latestSummaryGCVersion = getGCVersion(metadata);
|
|
267
556
|
}
|
|
268
557
|
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Updates the state of the system as per the current GC run. It does the following:
|
|
561
|
+
* 1. Sets up the current GC state as per the gcData.
|
|
562
|
+
* 2. Starts tracking for nodes that have become unreferenced in this run.
|
|
563
|
+
* 3. Clears tracking for nodes that were unreferenced but became referenced in this run.
|
|
564
|
+
* @param gcData - The data representing the reference graph on which GC is run.
|
|
565
|
+
* @param gcResult - The result of the GC run on the gcData.
|
|
566
|
+
* @param currentTimestampMs - The current timestamp to be used for unreferenced nodes' timestamp.
|
|
567
|
+
*/
|
|
568
|
+
private updateCurrentState(gcData: IGarbageCollectionData, gcResult: IGCResult, currentTimestampMs: number) {
|
|
569
|
+
this.currentGCState = { gcNodes: {} };
|
|
570
|
+
for (const [id, outboundRoutes] of Object.entries(gcData.gcNodes)) {
|
|
571
|
+
this.currentGCState.gcNodes[id] = { outboundRoutes: Array.from(outboundRoutes) };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Iterate through the deleted nodes and start tracking if they became unreferenced in this run.
|
|
575
|
+
for (const nodeId of gcResult.deletedNodeIds) {
|
|
576
|
+
assert(this.currentGCState.gcNodes[nodeId] !== undefined, 0x2aa /* "Unexpected node when running GC" */);
|
|
577
|
+
|
|
578
|
+
// The time when the node became unreferenced. This is added to the current GC state.
|
|
579
|
+
let unreferencedTimestampMs: number = currentTimestampMs;
|
|
580
|
+
const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
|
|
581
|
+
if (nodeStateTracker !== undefined) {
|
|
582
|
+
unreferencedTimestampMs = nodeStateTracker.unreferencedTimestampMs;
|
|
583
|
+
} else {
|
|
584
|
+
// Start tracking this node as it became unreferenced in this run.
|
|
585
|
+
this.unreferencedNodesState.set(
|
|
586
|
+
nodeId,
|
|
587
|
+
new UnreferencedStateTracker(unreferencedTimestampMs, this.deleteTimeoutMs),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
this.currentGCState.gcNodes[nodeId].unreferencedTimestampMs = unreferencedTimestampMs;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Iterate through the referenced nodes and stop tracking if they were unreferenced before.
|
|
594
|
+
for (const nodeId of gcResult.referencedNodeIds) {
|
|
595
|
+
assert(this.currentGCState.gcNodes[nodeId] !== undefined, 0x2ab /* "Unexpected node when running GC" */);
|
|
596
|
+
const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
|
|
597
|
+
if (nodeStateTracker !== undefined) {
|
|
598
|
+
// If this node has been unreferenced for longer than deleteTimeoutMs and is being referenced,
|
|
599
|
+
// log an error as this may mean the deleteTimeoutMs is not long enough.
|
|
600
|
+
nodeStateTracker.logIfInactive(
|
|
601
|
+
this.logger,
|
|
602
|
+
"inactiveObjectRevived",
|
|
603
|
+
currentTimestampMs,
|
|
604
|
+
this.deleteTimeoutMs,
|
|
605
|
+
nodeId,
|
|
606
|
+
);
|
|
607
|
+
// Stop tracking so as to clear out any running timers.
|
|
608
|
+
nodeStateTracker.stopTracking();
|
|
609
|
+
// Delete the node as we don't need to track it any more.
|
|
610
|
+
this.unreferencedNodesState.delete(nodeId);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Gets the garbage collection state from the given snapshot tree. The GC state may be written into multiple blobs.
|
|
618
|
+
* Merge the GC state from all such blobs and return the merged GC state.
|
|
619
|
+
*/
|
|
620
|
+
async function getGCStateFromSnapshot(
|
|
621
|
+
gcSnapshotTree: ISnapshotTree,
|
|
622
|
+
readAndParseBlob: ReadAndParseBlob,
|
|
623
|
+
): Promise<IGarbageCollectionState> {
|
|
624
|
+
let rootGCState: IGarbageCollectionState = { gcNodes: {} };
|
|
625
|
+
for (const key of Object.keys(gcSnapshotTree.blobs)) {
|
|
626
|
+
// Skip blobs that do not stsart with the GC prefix.
|
|
627
|
+
if (!key.startsWith(gcBlobPrefix)) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const blobId = gcSnapshotTree.blobs[key];
|
|
632
|
+
if (blobId === undefined) {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const gcState = await readAndParseBlob<IGarbageCollectionState>(blobId);
|
|
636
|
+
assert(gcState !== undefined, 0x2ad /* "GC blob missing from snapshot" */);
|
|
637
|
+
// Merge the GC state of this blob into the root GC state.
|
|
638
|
+
rootGCState = concatGarbageCollectionStates(rootGCState, gcState);
|
|
639
|
+
}
|
|
640
|
+
return rootGCState;
|
|
269
641
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,10 +3,28 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export
|
|
6
|
+
export {
|
|
7
|
+
ContainerMessageType,
|
|
8
|
+
IChunkedOp,
|
|
9
|
+
ContainerRuntimeMessage,
|
|
10
|
+
IGCRuntimeOptions,
|
|
11
|
+
ISummaryRuntimeOptions,
|
|
12
|
+
IContainerRuntimeOptions,
|
|
13
|
+
isRuntimeMessage,
|
|
14
|
+
unpackRuntimeMessage,
|
|
15
|
+
ScheduleManager,
|
|
16
|
+
agentSchedulerId,
|
|
17
|
+
ContainerRuntime,
|
|
18
|
+
} from "./containerRuntime";
|
|
7
19
|
export * from "./deltaScheduler";
|
|
8
20
|
export * from "./dataStoreRegistry";
|
|
9
|
-
export {
|
|
21
|
+
export {
|
|
22
|
+
gcBlobPrefix,
|
|
23
|
+
gcTreeKey,
|
|
24
|
+
IGarbageCollectionRuntime,
|
|
25
|
+
IGCStats,
|
|
26
|
+
IUsedStateStats,
|
|
27
|
+
} from "./garbageCollection";
|
|
10
28
|
export * from "./pendingStateManager";
|
|
11
29
|
export * from "./summarizer";
|
|
12
30
|
export * from "./summarizerTypes";
|
package/src/packageVersion.ts
CHANGED
|
@@ -71,7 +71,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
71
71
|
private readonly initialStates: Deque<IPendingState>;
|
|
72
72
|
private readonly previousClientIds = new Set<string>();
|
|
73
73
|
private readonly firstStashedCSN: number = -1;
|
|
74
|
-
private stashedCount = 0;
|
|
75
74
|
private readonly disposeOnce = new Lazy<void>(() => {
|
|
76
75
|
this.initialStates.clear();
|
|
77
76
|
this.pendingStates.clear();
|
|
@@ -127,7 +126,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
127
126
|
// get stashed op count and client sequence number of first op
|
|
128
127
|
const messages = initialState.pendingStates
|
|
129
128
|
.filter((state) => state.type === "message") as IPendingMessage[];
|
|
130
|
-
this.stashedCount = messages.length;
|
|
131
129
|
this.firstStashedCSN = messages[0].clientSequenceNumber;
|
|
132
130
|
}
|
|
133
131
|
}
|
|
@@ -290,7 +288,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
290
288
|
// if it's not a message just drop it and keep looking
|
|
291
289
|
if (nextState.type === "message") {
|
|
292
290
|
this.assertOpMatch(nextState, message, isOriginalClientId);
|
|
293
|
-
--this.stashedCount;
|
|
294
291
|
return { localAck: true, localOpMetadata: nextState.localOpMetadata };
|
|
295
292
|
}
|
|
296
293
|
}
|
|
@@ -454,7 +451,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
454
451
|
// This assert suggests we are about to send same ops twice, which will result in data loss.
|
|
455
452
|
assert(this.clientId !== this.containerRuntime.clientId,
|
|
456
453
|
0x173 /* "replayPendingStates called twice for same clientId!" */);
|
|
457
|
-
const prevClientId = this.clientId;
|
|
458
454
|
this.clientId = this.containerRuntime.clientId;
|
|
459
455
|
|
|
460
456
|
assert(this.initialStates.isEmpty(), 0x174 /* "initial states should be empty before replaying pending" */);
|
|
@@ -464,45 +460,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
464
460
|
return;
|
|
465
461
|
}
|
|
466
462
|
|
|
467
|
-
if (!prevClientId && this.stashedCount > 0) {
|
|
468
|
-
// this is first connect, verify we are about to "resubmit" only stashed ops
|
|
469
|
-
assert(this.pendingStates.toArray().filter((s) => s.type === "message").length === this.stashedCount,
|
|
470
|
-
0x290 /* "unexpected message queued before first connect" */);
|
|
471
|
-
|
|
472
|
-
Array.from(this.previousClientIds).map((id) =>
|
|
473
|
-
assert(this.containerRuntime.getQuorum().getMember(id) === undefined,
|
|
474
|
-
0x291 /* "client with stashed ops already connected" */));
|
|
475
|
-
|
|
476
|
-
// send rejoin op with stashed client ID if we have it
|
|
477
|
-
if (this.previousClientIds.size > 0) {
|
|
478
|
-
const clientId = Array.from(this.previousClientIds)[0];
|
|
479
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
480
|
-
this.pendingStates.unshift({
|
|
481
|
-
type: "message",
|
|
482
|
-
messageType: ContainerMessageType.Rejoin,
|
|
483
|
-
content: { clientId },
|
|
484
|
-
} as IPendingMessage);
|
|
485
|
-
++pendingStatesCount;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (prevClientId) {
|
|
490
|
-
// add a rejoin op so future clients provided with our stashed pending ops can recognize them
|
|
491
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
492
|
-
const firstState = this.pendingStates.peekFront()!;
|
|
493
|
-
if (firstState.type !== "message" || firstState.messageType !== ContainerMessageType.Rejoin) {
|
|
494
|
-
// if there is already a rejoin op in the queue, just resubmit same op under new client ID
|
|
495
|
-
// otherwise, add one to the queue
|
|
496
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
497
|
-
this.pendingStates.unshift({
|
|
498
|
-
type: "message",
|
|
499
|
-
messageType: ContainerMessageType.Rejoin,
|
|
500
|
-
content: { clientId: prevClientId },
|
|
501
|
-
} as IPendingMessage);
|
|
502
|
-
++pendingStatesCount;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
463
|
// Reset the pending message count because all these messages will be removed from the queue.
|
|
507
464
|
this.pendingMessagesCount = 0;
|
|
508
465
|
|