@fluidframework/container-runtime 1.2.0-77818 → 1.2.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/garbageCollection.d.ts +25 -10
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +182 -88
- package/dist/garbageCollection.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/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/lib/garbageCollection.d.ts +25 -10
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +181 -87
- package/lib/garbageCollection.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/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/package.json +16 -16
- package/src/garbageCollection.ts +228 -120
- package/src/packageVersion.ts +1 -1
- package/src/summaryFormat.ts +6 -3
package/src/garbageCollection.ts
CHANGED
|
@@ -62,10 +62,10 @@ export const gcBlobPrefix = "__gc";
|
|
|
62
62
|
|
|
63
63
|
// Feature gate key to turn GC on / off.
|
|
64
64
|
const runGCKey = "Fluid.GarbageCollection.RunGC";
|
|
65
|
-
// Feature gate key to turn GC test mode on / off.
|
|
66
|
-
const gcTestModeKey = "Fluid.GarbageCollection.GCTestMode";
|
|
67
65
|
// Feature gate key to turn GC sweep on / off.
|
|
68
66
|
const runSweepKey = "Fluid.GarbageCollection.RunSweep";
|
|
67
|
+
// Feature gate key to turn GC test mode on / off.
|
|
68
|
+
const gcTestModeKey = "Fluid.GarbageCollection.GCTestMode";
|
|
69
69
|
// Feature gate key to write GC data at the root of the summary tree.
|
|
70
70
|
const writeAtRootKey = "Fluid.GarbageCollection.WriteDataAtRoot";
|
|
71
71
|
// Feature gate key to expire a session after a set period of time.
|
|
@@ -74,9 +74,14 @@ export const runSessionExpiryKey = "Fluid.GarbageCollection.RunSessionExpiry";
|
|
|
74
74
|
export const disableSessionExpiryKey = "Fluid.GarbageCollection.DisableSessionExpiry";
|
|
75
75
|
// Feature gate key to write the gc blob as a handle if the data is the same.
|
|
76
76
|
export const trackGCStateKey = "Fluid.GarbageCollection.TrackGCState";
|
|
77
|
+
// Feature gate key to turn GC sweep log off.
|
|
78
|
+
const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
export const
|
|
80
|
+
// One day in milliseconds.
|
|
81
|
+
export const oneDayMs = 1 * 24 * 60 * 60 * 1000;
|
|
82
|
+
|
|
83
|
+
const defaultInactiveTimeoutMs = 7 * oneDayMs; // 7 days
|
|
84
|
+
export const defaultSessionExpiryDurationMs = 30 * oneDayMs; // 30 days
|
|
80
85
|
|
|
81
86
|
/** The statistics of the system state after a garbage collection run. */
|
|
82
87
|
export interface IGCStats {
|
|
@@ -113,20 +118,6 @@ export const GCNodeType = {
|
|
|
113
118
|
};
|
|
114
119
|
export type GCNodeType = typeof GCNodeType[keyof typeof GCNodeType];
|
|
115
120
|
|
|
116
|
-
/** The event that is logged when unreferenced node is used after a certain time. */
|
|
117
|
-
interface IUnreferencedEvent {
|
|
118
|
-
usageType: "Changed" | "Loaded" | "Revived";
|
|
119
|
-
id: string;
|
|
120
|
-
type: GCNodeType;
|
|
121
|
-
age: number;
|
|
122
|
-
timeout: number;
|
|
123
|
-
completedGCRuns: number;
|
|
124
|
-
fromId?: string;
|
|
125
|
-
lastSummaryTime?: number;
|
|
126
|
-
externalRequest?: boolean;
|
|
127
|
-
viaHandle?: boolean;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
121
|
/** Defines the APIs for the runtime object to be passed to the garbage collector. */
|
|
131
122
|
export interface IGarbageCollectionRuntime {
|
|
132
123
|
/** Before GC runs, called to notify the runtime to update any pending GC state. */
|
|
@@ -156,7 +147,7 @@ export interface IGarbageCollector {
|
|
|
156
147
|
readonly trackGCState: boolean;
|
|
157
148
|
/** Run garbage collection and update the reference / used state of the system. */
|
|
158
149
|
collectGarbage(
|
|
159
|
-
options: { logger?: ITelemetryLogger;
|
|
150
|
+
options: { logger?: ITelemetryLogger; runSweep?: boolean; fullGC?: boolean; },
|
|
160
151
|
): Promise<IGCStats>;
|
|
161
152
|
/** Summarizes the GC data and returns it as a summary tree. */
|
|
162
153
|
summarize(
|
|
@@ -195,58 +186,122 @@ export interface IGarbageCollectorCreateParams {
|
|
|
195
186
|
readonly getNodePackagePath: (nodePath: string) => Promise<readonly string[] | undefined>;
|
|
196
187
|
readonly getLastSummaryTimestampMs: () => number | undefined;
|
|
197
188
|
readonly readAndParseBlob: ReadAndParseBlob;
|
|
189
|
+
readonly snapshotCacheExpiryMs?: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The state of node that is unreferenced. */
|
|
193
|
+
const UnreferencedState = {
|
|
194
|
+
/** The node is active, i.e., it can become referenced again. */
|
|
195
|
+
Active: "Active",
|
|
196
|
+
/** The node is inactive, i.e., it should not become referenced. */
|
|
197
|
+
Inactive: "Inactive",
|
|
198
|
+
/** The node is ready to be deleted by the sweep phase. */
|
|
199
|
+
SweepReady: "SweepReady",
|
|
200
|
+
};
|
|
201
|
+
export type UnreferencedState = typeof UnreferencedState[keyof typeof UnreferencedState];
|
|
202
|
+
|
|
203
|
+
/** The event that is logged when unreferenced node is used after a certain time. */
|
|
204
|
+
interface IUnreferencedEventProps {
|
|
205
|
+
usageType: "Changed" | "Loaded" | "Revived";
|
|
206
|
+
state: UnreferencedState;
|
|
207
|
+
id: string;
|
|
208
|
+
type: GCNodeType;
|
|
209
|
+
unrefTime: number;
|
|
210
|
+
age: number;
|
|
211
|
+
completedGCRuns: number;
|
|
212
|
+
fromId?: string;
|
|
213
|
+
timeout?: number;
|
|
214
|
+
lastSummaryTime?: number;
|
|
215
|
+
externalRequest?: boolean;
|
|
216
|
+
viaHandle?: boolean;
|
|
198
217
|
}
|
|
199
218
|
|
|
200
219
|
/**
|
|
201
|
-
* Helper class that tracks the state of an unreferenced node such as the time it was unreferenced
|
|
202
|
-
*
|
|
220
|
+
* Helper class that tracks the state of an unreferenced node such as the time it was unreferenced and if it can
|
|
221
|
+
* be deleted by the sweep phase.
|
|
203
222
|
*/
|
|
204
223
|
class UnreferencedStateTracker {
|
|
205
|
-
private
|
|
206
|
-
public get
|
|
207
|
-
return this.
|
|
224
|
+
private _state: UnreferencedState = UnreferencedState.Active;
|
|
225
|
+
public get state(): UnreferencedState {
|
|
226
|
+
return this._state;
|
|
208
227
|
}
|
|
209
228
|
|
|
210
|
-
private
|
|
229
|
+
private inactiveTimer: Timer | undefined;
|
|
230
|
+
private sweepTimer: ReturnType<typeof setTimeout> | undefined;
|
|
211
231
|
|
|
212
232
|
constructor(
|
|
213
233
|
public readonly unreferencedTimestampMs: number,
|
|
234
|
+
/** The time after which node transitions to Inactive state. */
|
|
214
235
|
private readonly inactiveTimeoutMs: number,
|
|
236
|
+
/** The time after which node transitions to SweepReady state; undefined if session expiry is disabled. */
|
|
237
|
+
private readonly sweepTimeoutMs?: number,
|
|
238
|
+
/** The current reference timestamp; undefined if no ops have ever been processed which can happen in tests. */
|
|
215
239
|
currentReferenceTimestampMs?: number,
|
|
216
240
|
) {
|
|
217
|
-
// If there is no current reference timestamp, don't track the node's
|
|
218
|
-
// when updateTracking is called with a reference timestamp.
|
|
241
|
+
// If there is no current reference timestamp, don't track the node's unreferenced state. This will happen
|
|
242
|
+
// later when updateTracking is called with a reference timestamp.
|
|
219
243
|
if (currentReferenceTimestampMs !== undefined) {
|
|
220
244
|
this.updateTracking(currentReferenceTimestampMs);
|
|
221
245
|
}
|
|
222
246
|
}
|
|
223
247
|
|
|
224
|
-
|
|
225
|
-
* Updates the tracking state based on the provided timestamp.
|
|
226
|
-
*/
|
|
248
|
+
/* Updates the unreferenced state based on the provided timestamp. */
|
|
227
249
|
public updateTracking(currentReferenceTimestampMs: number) {
|
|
228
250
|
const unreferencedDurationMs = currentReferenceTimestampMs - this.unreferencedTimestampMs;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
this.
|
|
251
|
+
|
|
252
|
+
// If the node has been unreferenced for sweep timeout amount of time, update the state to SweepReady.
|
|
253
|
+
if (this.sweepTimeoutMs !== undefined && unreferencedDurationMs >= this.sweepTimeoutMs) {
|
|
254
|
+
this._state = UnreferencedState.SweepReady;
|
|
255
|
+
this.clearTimers();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// If the node has been unreferenced for inactive timeoutMs amount of time, update the state to inactive.
|
|
260
|
+
// Also, start a timer for the sweep timeout.
|
|
261
|
+
if (unreferencedDurationMs >= this.inactiveTimeoutMs) {
|
|
262
|
+
this._state = UnreferencedState.Inactive;
|
|
263
|
+
this.clearTimers();
|
|
264
|
+
|
|
265
|
+
if (this.sweepTimeoutMs !== undefined) {
|
|
266
|
+
setLongTimeout(
|
|
267
|
+
this.sweepTimeoutMs - unreferencedDurationMs,
|
|
268
|
+
() => { this._state = UnreferencedState.SweepReady; },
|
|
269
|
+
(timer) => { this.sweepTimer = timer; },
|
|
270
|
+
);
|
|
271
|
+
}
|
|
233
272
|
return;
|
|
234
273
|
}
|
|
235
274
|
|
|
236
|
-
// The node
|
|
275
|
+
// The node is still active. Start the inactive timer for the remaining duration.
|
|
237
276
|
const remainingDurationMs = this.inactiveTimeoutMs - unreferencedDurationMs;
|
|
238
|
-
if (this.
|
|
239
|
-
|
|
277
|
+
if (this.inactiveTimer === undefined) {
|
|
278
|
+
const inactiveTimeoutHandler = () => {
|
|
279
|
+
this._state = UnreferencedState.Inactive;
|
|
280
|
+
// After the node becomes inactive, start the sweep timer after which the node will be ready for sweep.
|
|
281
|
+
if (this.sweepTimeoutMs !== undefined) {
|
|
282
|
+
setLongTimeout(
|
|
283
|
+
this.sweepTimeoutMs - this.inactiveTimeoutMs,
|
|
284
|
+
() => { this._state = UnreferencedState.SweepReady; },
|
|
285
|
+
(timer) => { this.sweepTimer = timer; },
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
this.inactiveTimer = new Timer(remainingDurationMs, () => inactiveTimeoutHandler());
|
|
240
290
|
}
|
|
241
|
-
this.
|
|
291
|
+
this.inactiveTimer.restart(remainingDurationMs);
|
|
242
292
|
}
|
|
243
293
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
294
|
+
private clearTimers() {
|
|
295
|
+
this.inactiveTimer?.clear();
|
|
296
|
+
if (this.sweepTimer !== undefined) {
|
|
297
|
+
clearTimeout(this.sweepTimer);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Stop tracking this node. Reset the unreferenced timers and state, if any. */
|
|
247
302
|
public stopTracking() {
|
|
248
|
-
this.
|
|
249
|
-
this.
|
|
303
|
+
this.clearTimers();
|
|
304
|
+
this._state = UnreferencedState.Active;
|
|
250
305
|
}
|
|
251
306
|
}
|
|
252
307
|
|
|
@@ -272,11 +327,6 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
272
327
|
return new GarbageCollector(createParams);
|
|
273
328
|
}
|
|
274
329
|
|
|
275
|
-
/**
|
|
276
|
-
* The time in ms to expire a session for a client for gc.
|
|
277
|
-
*/
|
|
278
|
-
private readonly sessionExpiryTimeoutMs: number | undefined;
|
|
279
|
-
|
|
280
330
|
/**
|
|
281
331
|
* Tells whether the GC state needs to be reset in the next summary. We need to do this if:
|
|
282
332
|
* 1. GC was enabled and is now disabled. The GC state needs to be removed and everything becomes referenced.
|
|
@@ -360,8 +410,6 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
360
410
|
private readonly initializeBaseStateP: Promise<void>;
|
|
361
411
|
// The map of data store ids to their GC details in the base summary returned in getDataStoreGCDetails().
|
|
362
412
|
private readonly baseGCDetailsP: Promise<Map<string, IGarbageCollectionDetailsBase>>;
|
|
363
|
-
// The time after which an unreferenced node is inactive.
|
|
364
|
-
private readonly inactiveTimeoutMs: number;
|
|
365
413
|
// Map of node ids to their unreferenced state tracker.
|
|
366
414
|
private readonly unreferencedNodesState: Map<string, UnreferencedStateTracker> = new Map();
|
|
367
415
|
// The timeout responsible for closing the container when the session has expired
|
|
@@ -371,7 +419,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
371
419
|
// per event per node.
|
|
372
420
|
private readonly loggedUnreferencedEvents: Set<string> = new Set();
|
|
373
421
|
// Queue for unreferenced events that should be logged the next time GC runs.
|
|
374
|
-
private pendingEventsQueue:
|
|
422
|
+
private pendingEventsQueue: IUnreferencedEventProps[] = [];
|
|
375
423
|
|
|
376
424
|
// The number of times GC has successfully completed on this instance of GarbageCollector.
|
|
377
425
|
private completedRuns = 0;
|
|
@@ -380,6 +428,13 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
380
428
|
private readonly gcOptions: IGCRuntimeOptions;
|
|
381
429
|
private readonly isSummarizerClient: boolean;
|
|
382
430
|
|
|
431
|
+
/** The time in ms to expire a session for a client for gc. */
|
|
432
|
+
private readonly sessionExpiryTimeoutMs: number | undefined;
|
|
433
|
+
/** The time after which an unreferenced node is inactive. */
|
|
434
|
+
private readonly inactiveTimeoutMs: number;
|
|
435
|
+
/** The time after which an unreferenced node is ready to be swept. */
|
|
436
|
+
private readonly sweepTimeoutMs: number | undefined;
|
|
437
|
+
|
|
383
438
|
/** For a given node path, returns the node's package path. */
|
|
384
439
|
private readonly getNodePackagePath: (nodePath: string) => Promise<readonly string[] | undefined>;
|
|
385
440
|
/** Returns the timestamp of the last summary generated for this container. */
|
|
@@ -435,24 +490,28 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
435
490
|
}
|
|
436
491
|
}
|
|
437
492
|
|
|
438
|
-
// If session expiry is enabled, we need to close the container when the timeout expires
|
|
439
|
-
if (this.sessionExpiryTimeoutMs !== undefined
|
|
440
|
-
|
|
441
|
-
// If Test Override config is set, override Session Expiry timeout
|
|
493
|
+
// If session expiry is enabled, we need to close the container when the session expiry timeout expires.
|
|
494
|
+
if (this.sessionExpiryTimeoutMs !== undefined && this.mc.config.getBoolean(disableSessionExpiryKey) !== true) {
|
|
495
|
+
// If Test Override config is set, override Session Expiry timeout.
|
|
442
496
|
const overrideSessionExpiryTimeoutMs =
|
|
443
497
|
this.mc.config.getNumber("Fluid.GarbageCollection.TestOverride.SessionExpiryMs");
|
|
444
|
-
|
|
445
|
-
this.sessionExpiryTimeoutMs = overrideSessionExpiryTimeoutMs;
|
|
446
|
-
}
|
|
498
|
+
const timeoutMs = overrideSessionExpiryTimeoutMs ?? this.sessionExpiryTimeoutMs;
|
|
447
499
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
() => {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
500
|
+
setLongTimeout(
|
|
501
|
+
timeoutMs,
|
|
502
|
+
() => { this.runtime.closeFn(new ClientSessionExpiredError(`Client session expired.`, timeoutMs)); },
|
|
503
|
+
(timer) => { this.sessionExpiryTimer = timer; },
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Sweep timeout is the time after which unreferenced content can be swept.
|
|
508
|
+
* Sweep timeout = session expiry timeout + snapshot cache expiry timeout + one day buffer. The buffer is
|
|
509
|
+
* added to account for any clock skew. We use server timestamps throughout so the skew should be minimal
|
|
510
|
+
* but make it one day to be safe.
|
|
511
|
+
*/
|
|
512
|
+
if (createParams.snapshotCacheExpiryMs !== undefined) {
|
|
513
|
+
this.sweepTimeoutMs = this.sessionExpiryTimeoutMs + createParams.snapshotCacheExpiryMs + oneDayMs;
|
|
514
|
+
}
|
|
456
515
|
}
|
|
457
516
|
|
|
458
517
|
// For existing document, the latest summary is the one that we loaded from. So, use its GC version as the
|
|
@@ -472,24 +531,29 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
472
531
|
&& !this.gcOptions.disableGC
|
|
473
532
|
);
|
|
474
533
|
|
|
475
|
-
this.trackGCState = this.mc.config.getBoolean(trackGCStateKey) === true;
|
|
476
|
-
|
|
477
534
|
/**
|
|
478
535
|
* Whether sweep should run or not. The following conditions have to be met to run sweep:
|
|
479
536
|
* 1. Overall GC or mark phase must be enabled (this.shouldRunGC).
|
|
480
|
-
* 2.
|
|
481
|
-
*
|
|
537
|
+
* 2. Sweep timeout should be available. Without this, we wouldn't know when an object should be deleted.
|
|
538
|
+
* 3. Sweep should be enabled for this container (this.sweepEnabled). This can be overridden via runSweep
|
|
539
|
+
* feature flag.
|
|
482
540
|
*/
|
|
483
|
-
this.shouldRunSweep = this.shouldRunGC
|
|
484
|
-
|
|
485
|
-
|
|
541
|
+
this.shouldRunSweep = this.shouldRunGC
|
|
542
|
+
&& this.sweepTimeoutMs !== undefined
|
|
543
|
+
&& (this.mc.config.getBoolean(runSweepKey) ?? this.sweepEnabled);
|
|
544
|
+
|
|
545
|
+
this.trackGCState = this.mc.config.getBoolean(trackGCStateKey) === true;
|
|
486
546
|
|
|
487
547
|
// Override inactive timeout if test config or gc options to override it is set.
|
|
488
|
-
this.inactiveTimeoutMs =
|
|
489
|
-
this.mc.config.getNumber("Fluid.GarbageCollection.TestOverride.InactiveTimeoutMs") ??
|
|
548
|
+
this.inactiveTimeoutMs = this.mc.config.getNumber("Fluid.GarbageCollection.TestOverride.InactiveTimeoutMs") ??
|
|
490
549
|
this.gcOptions.inactiveTimeoutMs ??
|
|
491
550
|
defaultInactiveTimeoutMs;
|
|
492
551
|
|
|
552
|
+
// Inactive timeout must be greater than sweep timeout since a node goes from active -> inactive -> sweep ready.
|
|
553
|
+
if (this.sweepTimeoutMs !== undefined && this.inactiveTimeoutMs > this.sweepTimeoutMs) {
|
|
554
|
+
throw new UsageError("inactive timeout should not be greated than the sweep timeout");
|
|
555
|
+
}
|
|
556
|
+
|
|
493
557
|
// Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
|
|
494
558
|
this.testMode = this.mc.config.getBoolean(gcTestModeKey) ?? this.gcOptions.runGCInTestMode === true;
|
|
495
559
|
|
|
@@ -590,6 +654,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
590
654
|
new UnreferencedStateTracker(
|
|
591
655
|
nodeData.unreferencedTimestampMs,
|
|
592
656
|
this.inactiveTimeoutMs,
|
|
657
|
+
this.sweepTimeoutMs,
|
|
593
658
|
currentReferenceTimestampMs,
|
|
594
659
|
),
|
|
595
660
|
);
|
|
@@ -642,6 +707,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
642
707
|
sessionExpiry: this.sessionExpiryTimeoutMs,
|
|
643
708
|
inactiveTimeout: this.inactiveTimeoutMs,
|
|
644
709
|
existing: createParams.existing,
|
|
710
|
+
trackGCState: this.trackGCState,
|
|
645
711
|
...this.gcOptions,
|
|
646
712
|
});
|
|
647
713
|
if (this.isSummarizerClient) {
|
|
@@ -721,6 +787,10 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
721
787
|
this.updateCurrentState(gcData, gcResult, currentReferenceTimestampMs);
|
|
722
788
|
this.runtime.updateUsedRoutes(gcResult.referencedNodeIds, currentReferenceTimestampMs);
|
|
723
789
|
|
|
790
|
+
// Log events for objects that are ready to be deleted by sweep. When we have sweep enabled, we will
|
|
791
|
+
// delete these objects here instead.
|
|
792
|
+
this.logSweepEvents(logger, currentReferenceTimestampMs);
|
|
793
|
+
|
|
724
794
|
// If we are running in GC test mode, delete objects for unused routes. This enables testing scenarios
|
|
725
795
|
// involving access to deleted data.
|
|
726
796
|
if (this.testMode) {
|
|
@@ -730,7 +800,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
730
800
|
// Log pending unreferenced events such as a node being used after inactive. This is done after GC runs and
|
|
731
801
|
// updates its state so that we don't send false positives based on intermediate state. For example, we may get
|
|
732
802
|
// reference to an unreferenced node from another unreferenced node which means the node wasn't revived.
|
|
733
|
-
await this.
|
|
803
|
+
await this.logUnreferencedEvents(logger);
|
|
734
804
|
|
|
735
805
|
return gcStats;
|
|
736
806
|
}
|
|
@@ -873,12 +943,12 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
873
943
|
return;
|
|
874
944
|
}
|
|
875
945
|
|
|
876
|
-
const
|
|
877
|
-
if (
|
|
946
|
+
const nodeStateTracker = this.unreferencedNodesState.get(nodePath);
|
|
947
|
+
if (nodeStateTracker && nodeStateTracker.state !== UnreferencedState.Active) {
|
|
878
948
|
this.inactiveNodeUsed(
|
|
879
949
|
reason,
|
|
880
950
|
nodePath,
|
|
881
|
-
|
|
951
|
+
nodeStateTracker,
|
|
882
952
|
undefined /* fromNodeId */,
|
|
883
953
|
packagePath,
|
|
884
954
|
timestampMs,
|
|
@@ -903,9 +973,9 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
903
973
|
outboundRoutes.push(toNodePath);
|
|
904
974
|
this.newReferencesSinceLastRun.set(fromNodePath, outboundRoutes);
|
|
905
975
|
|
|
906
|
-
const
|
|
907
|
-
if (
|
|
908
|
-
this.inactiveNodeUsed("Revived", toNodePath,
|
|
976
|
+
const nodeStateTracker = this.unreferencedNodesState.get(toNodePath);
|
|
977
|
+
if (nodeStateTracker && nodeStateTracker.state !== UnreferencedState.Active) {
|
|
978
|
+
this.inactiveNodeUsed("Revived", toNodePath, nodeStateTracker, fromNodePath);
|
|
909
979
|
}
|
|
910
980
|
}
|
|
911
981
|
|
|
@@ -966,6 +1036,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
966
1036
|
new UnreferencedStateTracker(
|
|
967
1037
|
currentReferenceTimestampMs,
|
|
968
1038
|
this.inactiveTimeoutMs,
|
|
1039
|
+
this.sweepTimeoutMs,
|
|
969
1040
|
currentReferenceTimestampMs,
|
|
970
1041
|
),
|
|
971
1042
|
);
|
|
@@ -1169,12 +1240,51 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1169
1240
|
}
|
|
1170
1241
|
|
|
1171
1242
|
/**
|
|
1172
|
-
*
|
|
1243
|
+
* For nodes that are ready to sweep, log an event for now. Until we start running sweep which deletes objects,
|
|
1244
|
+
* this will give us a view into how much deleted content a container has.
|
|
1245
|
+
*/
|
|
1246
|
+
private logSweepEvents(logger: ITelemetryLogger, currentReferenceTimestampMs?: number) {
|
|
1247
|
+
if (this.mc.config.getBoolean(disableSweepLogKey) === true
|
|
1248
|
+
|| currentReferenceTimestampMs === undefined
|
|
1249
|
+
|| this.sweepTimeoutMs === undefined) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
this.unreferencedNodesState.forEach((nodeStateTracker, nodeId) => {
|
|
1254
|
+
if (nodeStateTracker.state !== UnreferencedState.SweepReady) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const nodeType = this.runtime.getNodeType(nodeId);
|
|
1259
|
+
if (nodeType !== GCNodeType.DataStore && nodeType !== GCNodeType.Blob) {
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Log deleted event for each node only once to reduce noise in telemetry.
|
|
1264
|
+
const uniqueEventId = `Deleted-${nodeId}`;
|
|
1265
|
+
if (this.loggedUnreferencedEvents.has(uniqueEventId)) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
this.loggedUnreferencedEvents.add(uniqueEventId);
|
|
1269
|
+
logger.sendTelemetryEvent({
|
|
1270
|
+
eventName: "GCObjectDeleted",
|
|
1271
|
+
id: nodeId,
|
|
1272
|
+
type: nodeType,
|
|
1273
|
+
age: currentReferenceTimestampMs - nodeStateTracker.unreferencedTimestampMs,
|
|
1274
|
+
timeout: this.sweepTimeoutMs,
|
|
1275
|
+
completedGCRuns: this.completedRuns,
|
|
1276
|
+
lastSummaryTime: this.getLastSummaryTimestampMs(),
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Called when an inactive node is used after. Queue up an event that will be logged next time GC runs.
|
|
1173
1283
|
*/
|
|
1174
1284
|
private inactiveNodeUsed(
|
|
1175
1285
|
usageType: "Changed" | "Loaded" | "Revived",
|
|
1176
1286
|
nodeId: string,
|
|
1177
|
-
|
|
1287
|
+
nodeStateTracker: UnreferencedStateTracker,
|
|
1178
1288
|
fromNodeId?: string,
|
|
1179
1289
|
packagePath?: readonly string[],
|
|
1180
1290
|
currentReferenceTimestampMs = this.runtime.getCurrentReferenceTimestampMs(),
|
|
@@ -1182,7 +1292,8 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1182
1292
|
) {
|
|
1183
1293
|
// If there is no reference timestamp to work with, no ops have been processed after creation. If so, skip
|
|
1184
1294
|
// logging as nothing interesting would have happened worth logging.
|
|
1185
|
-
|
|
1295
|
+
// If the node is active, skip logging.
|
|
1296
|
+
if (currentReferenceTimestampMs === undefined || nodeStateTracker.state === UnreferencedState.Active) {
|
|
1186
1297
|
return;
|
|
1187
1298
|
}
|
|
1188
1299
|
|
|
@@ -1199,19 +1310,21 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1199
1310
|
return;
|
|
1200
1311
|
}
|
|
1201
1312
|
|
|
1202
|
-
|
|
1203
|
-
const uniqueEventId = `${nodeId}-${usageType}`;
|
|
1313
|
+
const state = nodeStateTracker.state;
|
|
1314
|
+
const uniqueEventId = `${state}-${nodeId}-${usageType}`;
|
|
1204
1315
|
if (this.loggedUnreferencedEvents.has(uniqueEventId)) {
|
|
1205
1316
|
return;
|
|
1206
1317
|
}
|
|
1207
1318
|
this.loggedUnreferencedEvents.add(uniqueEventId);
|
|
1208
1319
|
|
|
1209
|
-
const
|
|
1210
|
-
usageType,
|
|
1320
|
+
const propsToLog = {
|
|
1211
1321
|
id: nodeId,
|
|
1212
1322
|
type: nodeType,
|
|
1213
|
-
|
|
1214
|
-
|
|
1323
|
+
unrefTime: nodeStateTracker.unreferencedTimestampMs,
|
|
1324
|
+
age: currentReferenceTimestampMs - nodeStateTracker.unreferencedTimestampMs,
|
|
1325
|
+
timeout: nodeStateTracker.state === UnreferencedState.Inactive
|
|
1326
|
+
? this.inactiveTimeoutMs
|
|
1327
|
+
: this.sweepTimeoutMs,
|
|
1215
1328
|
completedGCRuns: this.completedRuns,
|
|
1216
1329
|
lastSummaryTime: this.getLastSummaryTimestampMs(),
|
|
1217
1330
|
externalRequest: requestHeaders?.[RuntimeHeaders.externalRequest],
|
|
@@ -1223,42 +1336,37 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1223
1336
|
// For non-summarizer client, log the event now since GC won't run on it. This may result in false positives
|
|
1224
1337
|
// but it's a good signal nonetheless and we can consume it with a grain of salt.
|
|
1225
1338
|
if (this.isSummarizerClient) {
|
|
1226
|
-
this.pendingEventsQueue.push(
|
|
1339
|
+
this.pendingEventsQueue.push({ ...propsToLog, usageType, state });
|
|
1227
1340
|
} else {
|
|
1228
1341
|
this.mc.logger.sendErrorEvent({
|
|
1229
|
-
...
|
|
1230
|
-
eventName:
|
|
1231
|
-
pkg: packagePath ? { value: packagePath.join("/"), tag: TelemetryDataTag.
|
|
1342
|
+
...propsToLog,
|
|
1343
|
+
eventName: `${state}Object_${usageType}`,
|
|
1344
|
+
pkg: packagePath ? { value: packagePath.join("/"), tag: TelemetryDataTag.CodeArtifact } : undefined,
|
|
1232
1345
|
});
|
|
1233
1346
|
}
|
|
1234
1347
|
}
|
|
1235
1348
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1349
|
+
private async logUnreferencedEvents(logger: ITelemetryLogger) {
|
|
1350
|
+
for (const eventProps of this.pendingEventsQueue) {
|
|
1351
|
+
const { usageType, state, ...propsToLog } = eventProps;
|
|
1352
|
+
/**
|
|
1353
|
+
* Revived event is logged only if the node is active. If the node is not active, the reference to it was
|
|
1354
|
+
* from another unreferenced node and this scenario is not interesting to log.
|
|
1355
|
+
* Loaded and Changed events are logged only if the node is not active. If the node is active, it was
|
|
1356
|
+
* revived and a Revived event will be logged for it.
|
|
1357
|
+
*/
|
|
1358
|
+
const nodeStateTracker = this.unreferencedNodesState.get(eventProps.id);
|
|
1359
|
+
const active = nodeStateTracker === undefined || nodeStateTracker.state === UnreferencedState.Active;
|
|
1360
|
+
if ((usageType === "Revived") === active) {
|
|
1361
|
+
const pkg = await this.getNodePackagePath(eventProps.id);
|
|
1362
|
+
const fromPkg = eventProps.fromId ? await this.getNodePackagePath(eventProps.fromId) : undefined;
|
|
1363
|
+
logger.sendErrorEvent({
|
|
1364
|
+
...propsToLog,
|
|
1365
|
+
eventName: `${state}Object_${usageType}`,
|
|
1366
|
+
pkg: pkg ? { value: pkg.join("/"), tag: TelemetryDataTag.CodeArtifact } : undefined,
|
|
1367
|
+
fromPkg: fromPkg ? { value: fromPkg.join("/"), tag: TelemetryDataTag.CodeArtifact } : undefined,
|
|
1368
|
+
});
|
|
1252
1369
|
}
|
|
1253
|
-
|
|
1254
|
-
const pkg = await this.getNodePackagePath(event.id);
|
|
1255
|
-
const fromPkg = event.fromId ? await this.getNodePackagePath(event.fromId) : undefined;
|
|
1256
|
-
logger.sendErrorEvent({
|
|
1257
|
-
...event,
|
|
1258
|
-
eventName: `inactiveObject_${event.usageType}`,
|
|
1259
|
-
pkg: pkg ? { value: pkg.join("/"), tag: TelemetryDataTag.PackageData } : undefined,
|
|
1260
|
-
fromPkg: fromPkg ? { value: fromPkg.join("/"), tag: TelemetryDataTag.PackageData } : undefined,
|
|
1261
|
-
});
|
|
1262
1370
|
}
|
|
1263
1371
|
this.pendingEventsQueue = [];
|
|
1264
1372
|
}
|
package/src/packageVersion.ts
CHANGED
package/src/summaryFormat.ts
CHANGED
|
@@ -187,8 +187,12 @@ export const dataStoreAttributesBlobName = ".component";
|
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
189
|
* Modifies summary tree and stats to put tree under .channels tree.
|
|
190
|
+
*
|
|
191
|
+
* @param summarizeResult - Summary tree and stats to modify
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
190
194
|
* Converts from:
|
|
191
|
-
* ```
|
|
195
|
+
* ```typescript
|
|
192
196
|
* {
|
|
193
197
|
* type: SummaryType.Tree,
|
|
194
198
|
* tree: { a: {...}, b: {...}, c: {...} },
|
|
@@ -197,7 +201,7 @@ export const dataStoreAttributesBlobName = ".component";
|
|
|
197
201
|
*
|
|
198
202
|
* to:
|
|
199
203
|
*
|
|
200
|
-
* ```
|
|
204
|
+
* ```typescript
|
|
201
205
|
* {
|
|
202
206
|
* type: SummaryType.Tree,
|
|
203
207
|
* tree: {
|
|
@@ -209,7 +213,6 @@ export const dataStoreAttributesBlobName = ".component";
|
|
|
209
213
|
* }
|
|
210
214
|
* ```
|
|
211
215
|
* And adds +1 to treeNodeCount in stats.
|
|
212
|
-
* @param summarizeResult - summary tree and stats to modify
|
|
213
216
|
*/
|
|
214
217
|
export function wrapSummaryInChannelsTree(summarizeResult: ISummaryTreeWithStats): void {
|
|
215
218
|
summarizeResult.summary = {
|