@fluidframework/container-runtime 1.1.0-76254 → 1.2.0-78837

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