@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.
@@ -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
- const defaultInactiveTimeoutMs = 7 * 24 * 60 * 60 * 1000; // 7 days
79
- export const defaultSessionExpiryDurationMs = 30 * 24 * 60 * 60 * 1000; // 30 days
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; runGC?: boolean; runSweep?: boolean; fullGC?: boolean; },
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. It also sets
202
- * the node's state to inactive if it remains unreferenced for a given amount of time (inactiveTimeoutMs).
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 _inactive: boolean = false;
206
- public get inactive(): boolean {
207
- return this._inactive;
224
+ private _state: UnreferencedState = UnreferencedState.Active;
225
+ public get state(): UnreferencedState {
226
+ return this._state;
208
227
  }
209
228
 
210
- private timer: Timer | undefined;
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 inactive state. This will happen later
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
- // If the timeout has already expired, the node has become inactive.
230
- if (unreferencedDurationMs > this.inactiveTimeoutMs) {
231
- this._inactive = true;
232
- this.timer?.clear();
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 isn't inactive yet. Restart a timer for the duration remaining for it to become inactive.
275
+ // The node is still active. Start the inactive timer for the remaining duration.
237
276
  const remainingDurationMs = this.inactiveTimeoutMs - unreferencedDurationMs;
238
- if (this.timer === undefined) {
239
- this.timer = new Timer(remainingDurationMs, () => { this._inactive = true; });
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.timer.restart(remainingDurationMs);
291
+ this.inactiveTimer.restart(remainingDurationMs);
242
292
  }
243
293
 
244
- /**
245
- * Stop tracking this node. Reset the unreferenced timer, if any, and reset inactive state.
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.timer?.clear();
249
- this._inactive = false;
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: IUnreferencedEvent[] = [];
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
- && this.mc.config.getBoolean(disableSessionExpiryKey) !== true) {
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
- if (overrideSessionExpiryTimeoutMs !== undefined) {
445
- this.sessionExpiryTimeoutMs = overrideSessionExpiryTimeoutMs;
446
- }
498
+ const timeoutMs = overrideSessionExpiryTimeoutMs ?? this.sessionExpiryTimeoutMs;
447
499
 
448
- const timeoutMs = this.sessionExpiryTimeoutMs;
449
- setLongTimeout(timeoutMs,
450
- () => {
451
- this.runtime.closeFn(new ClientSessionExpiredError(`Client session expired.`, timeoutMs));
452
- },
453
- (timer) => {
454
- this.sessionExpiryTimer = timer;
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. Session expiry and sweep should be enabled for this container. Without session expiry we cannot safely
481
- * delete unreferenced objects. This condition (#2) can be overridden via runSweepKey feature flag.
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
- this.mc.config.getBoolean(runSweepKey) ?? (this.sessionExpiryTimeoutMs !== undefined && this.sweepEnabled)
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.logPendingEvents(logger);
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 unreferencedState = this.unreferencedNodesState.get(nodePath);
877
- if (unreferencedState?.inactive) {
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
- unreferencedState,
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 unreferencedState = this.unreferencedNodesState.get(toNodePath);
907
- if (unreferencedState?.inactive) {
908
- this.inactiveNodeUsed("Revived", toNodePath, unreferencedState, fromNodePath);
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
- * Called when a node is used. If the node is inactive, queue up an event that will be logged next time GC runs.
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
- unreferencedState: UnreferencedStateTracker,
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
- if (currentReferenceTimestampMs === undefined) {
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
- // A particular event is logged for a given node only once so that it is not too noisy.
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 event: IUnreferencedEvent = {
1210
- usageType,
1320
+ const propsToLog = {
1211
1321
  id: nodeId,
1212
1322
  type: nodeType,
1213
- age: currentReferenceTimestampMs - unreferencedState.unreferencedTimestampMs,
1214
- timeout: this.inactiveTimeoutMs,
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(event);
1339
+ this.pendingEventsQueue.push({ ...propsToLog, usageType, state });
1227
1340
  } else {
1228
1341
  this.mc.logger.sendErrorEvent({
1229
- ...event,
1230
- eventName: `inactiveObject-${usageType}`,
1231
- pkg: packagePath ? { value: packagePath.join("/"), tag: TelemetryDataTag.PackageData } : undefined,
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
- * Logs pending unreferenced events if they are still valid.
1238
- */
1239
- private async logPendingEvents(logger: ITelemetryLogger) {
1240
- for (const event of this.pendingEventsQueue) {
1241
- const unreferencedState = this.unreferencedNodesState.get(event.id);
1242
- // Only log revived event if the node is not inactive anymore. If the node is still inactive, the
1243
- // reference was from another unreferenced node and we don't care about logging this scenario.
1244
- if (event.usageType === "Revived" && unreferencedState?.inactive) {
1245
- continue;
1246
- }
1247
-
1248
- // Only log loaded and changed event if the node is still inactive. If the node is not inactive, it was
1249
- // revived and a revived event will be logged for it.
1250
- if (event.usageType !== "Revived" && !unreferencedState?.inactive) {
1251
- continue;
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
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "1.2.0-77818";
9
+ export const pkgVersion = "1.2.1";
@@ -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
- * ```ts
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
- * ```ts
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 = {