@fluidframework/container-runtime 2.0.0-internal.1.2.0.93071 → 2.0.0-internal.1.2.2

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 (128) hide show
  1. package/dist/batchManager.d.ts +7 -2
  2. package/dist/batchManager.d.ts.map +1 -1
  3. package/dist/batchManager.js +19 -17
  4. package/dist/batchManager.js.map +1 -1
  5. package/dist/batchTracker.d.ts +1 -2
  6. package/dist/batchTracker.d.ts.map +1 -1
  7. package/dist/batchTracker.js +1 -2
  8. package/dist/batchTracker.js.map +1 -1
  9. package/dist/containerRuntime.d.ts +10 -5
  10. package/dist/containerRuntime.d.ts.map +1 -1
  11. package/dist/containerRuntime.js +87 -63
  12. package/dist/containerRuntime.js.map +1 -1
  13. package/dist/dataStoreContext.d.ts +14 -5
  14. package/dist/dataStoreContext.d.ts.map +1 -1
  15. package/dist/dataStoreContext.js +19 -11
  16. package/dist/dataStoreContext.js.map +1 -1
  17. package/dist/dataStores.d.ts +6 -2
  18. package/dist/dataStores.d.ts.map +1 -1
  19. package/dist/dataStores.js +7 -9
  20. package/dist/dataStores.js.map +1 -1
  21. package/dist/deltaScheduler.d.ts +6 -4
  22. package/dist/deltaScheduler.d.ts.map +1 -1
  23. package/dist/deltaScheduler.js +6 -4
  24. package/dist/deltaScheduler.js.map +1 -1
  25. package/dist/garbageCollection.d.ts +41 -12
  26. package/dist/garbageCollection.d.ts.map +1 -1
  27. package/dist/garbageCollection.js +176 -98
  28. package/dist/garbageCollection.js.map +1 -1
  29. package/dist/gcSweepReadyUsageDetection.d.ts +53 -0
  30. package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -0
  31. package/dist/gcSweepReadyUsageDetection.js +135 -0
  32. package/dist/gcSweepReadyUsageDetection.js.map +1 -0
  33. package/dist/orderedClientElection.d.ts +28 -10
  34. package/dist/orderedClientElection.d.ts.map +1 -1
  35. package/dist/orderedClientElection.js +14 -4
  36. package/dist/orderedClientElection.js.map +1 -1
  37. package/dist/packageVersion.d.ts +1 -1
  38. package/dist/packageVersion.d.ts.map +1 -1
  39. package/dist/packageVersion.js +1 -1
  40. package/dist/packageVersion.js.map +1 -1
  41. package/dist/pendingStateManager.d.ts.map +1 -1
  42. package/dist/pendingStateManager.js +4 -2
  43. package/dist/pendingStateManager.js.map +1 -1
  44. package/dist/scheduleManager.d.ts +6 -3
  45. package/dist/scheduleManager.d.ts.map +1 -1
  46. package/dist/scheduleManager.js +21 -13
  47. package/dist/scheduleManager.js.map +1 -1
  48. package/dist/summarizerTypes.d.ts +13 -6
  49. package/dist/summarizerTypes.d.ts.map +1 -1
  50. package/dist/summarizerTypes.js.map +1 -1
  51. package/dist/summaryCollection.d.ts.map +1 -1
  52. package/dist/summaryCollection.js +3 -6
  53. package/dist/summaryCollection.js.map +1 -1
  54. package/dist/summaryManager.d.ts +2 -2
  55. package/dist/summaryManager.js +2 -2
  56. package/dist/summaryManager.js.map +1 -1
  57. package/lib/batchManager.d.ts +7 -2
  58. package/lib/batchManager.d.ts.map +1 -1
  59. package/lib/batchManager.js +19 -17
  60. package/lib/batchManager.js.map +1 -1
  61. package/lib/batchTracker.d.ts +1 -2
  62. package/lib/batchTracker.d.ts.map +1 -1
  63. package/lib/batchTracker.js +1 -2
  64. package/lib/batchTracker.js.map +1 -1
  65. package/lib/containerRuntime.d.ts +10 -5
  66. package/lib/containerRuntime.d.ts.map +1 -1
  67. package/lib/containerRuntime.js +87 -63
  68. package/lib/containerRuntime.js.map +1 -1
  69. package/lib/dataStoreContext.d.ts +14 -5
  70. package/lib/dataStoreContext.d.ts.map +1 -1
  71. package/lib/dataStoreContext.js +20 -12
  72. package/lib/dataStoreContext.js.map +1 -1
  73. package/lib/dataStores.d.ts +6 -2
  74. package/lib/dataStores.d.ts.map +1 -1
  75. package/lib/dataStores.js +7 -9
  76. package/lib/dataStores.js.map +1 -1
  77. package/lib/deltaScheduler.d.ts +6 -4
  78. package/lib/deltaScheduler.d.ts.map +1 -1
  79. package/lib/deltaScheduler.js +6 -4
  80. package/lib/deltaScheduler.js.map +1 -1
  81. package/lib/garbageCollection.d.ts +41 -12
  82. package/lib/garbageCollection.d.ts.map +1 -1
  83. package/lib/garbageCollection.js +175 -97
  84. package/lib/garbageCollection.js.map +1 -1
  85. package/lib/gcSweepReadyUsageDetection.d.ts +53 -0
  86. package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -0
  87. package/lib/gcSweepReadyUsageDetection.js +130 -0
  88. package/lib/gcSweepReadyUsageDetection.js.map +1 -0
  89. package/lib/orderedClientElection.d.ts +28 -10
  90. package/lib/orderedClientElection.d.ts.map +1 -1
  91. package/lib/orderedClientElection.js +14 -4
  92. package/lib/orderedClientElection.js.map +1 -1
  93. package/lib/packageVersion.d.ts +1 -1
  94. package/lib/packageVersion.d.ts.map +1 -1
  95. package/lib/packageVersion.js +1 -1
  96. package/lib/packageVersion.js.map +1 -1
  97. package/lib/pendingStateManager.d.ts.map +1 -1
  98. package/lib/pendingStateManager.js +4 -2
  99. package/lib/pendingStateManager.js.map +1 -1
  100. package/lib/scheduleManager.d.ts +6 -3
  101. package/lib/scheduleManager.d.ts.map +1 -1
  102. package/lib/scheduleManager.js +22 -14
  103. package/lib/scheduleManager.js.map +1 -1
  104. package/lib/summarizerTypes.d.ts +13 -6
  105. package/lib/summarizerTypes.d.ts.map +1 -1
  106. package/lib/summarizerTypes.js.map +1 -1
  107. package/lib/summaryCollection.d.ts.map +1 -1
  108. package/lib/summaryCollection.js +3 -6
  109. package/lib/summaryCollection.js.map +1 -1
  110. package/lib/summaryManager.d.ts +2 -2
  111. package/lib/summaryManager.js +2 -2
  112. package/lib/summaryManager.js.map +1 -1
  113. package/package.json +19 -16
  114. package/src/batchManager.ts +22 -19
  115. package/src/batchTracker.ts +1 -2
  116. package/src/containerRuntime.ts +118 -80
  117. package/src/dataStoreContext.ts +20 -10
  118. package/src/dataStores.ts +7 -8
  119. package/src/deltaScheduler.ts +6 -4
  120. package/src/garbageCollection.ts +224 -134
  121. package/src/gcSweepReadyUsageDetection.ts +147 -0
  122. package/src/orderedClientElection.ts +31 -10
  123. package/src/packageVersion.ts +1 -1
  124. package/src/pendingStateManager.ts +4 -2
  125. package/src/scheduleManager.ts +30 -10
  126. package/src/summarizerTypes.ts +14 -6
  127. package/src/summaryCollection.ts +3 -5
  128. package/src/summaryManager.ts +2 -2
@@ -22,6 +22,7 @@ import { mergeStats, SummaryTreeBuilder, } from "@fluidframework/runtime-utils";
22
22
  import { ChildLogger, loggerToMonitoringContext, PerformanceEvent, TelemetryDataTag, } from "@fluidframework/telemetry-utils";
23
23
  import { RuntimeHeaders } from "./containerRuntime";
24
24
  import { getSummaryForDatastores } from "./dataStores";
25
+ import { SweepReadyUsageDetectionHandler } from "./gcSweepReadyUsageDetection";
25
26
  import { getGCVersion, metadataBlobName, dataStoreAttributesBlobName, } from "./summaryFormat";
26
27
  /** This is the current version of garbage collection. */
27
28
  const GCVersion = 1;
@@ -39,12 +40,12 @@ export const gcTestModeKey = "Fluid.GarbageCollection.GCTestMode";
39
40
  const writeAtRootKey = "Fluid.GarbageCollection.WriteDataAtRoot";
40
41
  // Feature gate key to expire a session after a set period of time.
41
42
  export const runSessionExpiryKey = "Fluid.GarbageCollection.RunSessionExpiry";
42
- // Feature gate key to disable expiring session after a set period of time, even if expiry value is present
43
+ // Feature gate key to disable expiring session after a set period of time, even if expiry value is present.
43
44
  export const disableSessionExpiryKey = "Fluid.GarbageCollection.DisableSessionExpiry";
44
45
  // Feature gate key to write the gc blob as a handle if the data is the same.
45
46
  export const trackGCStateKey = "Fluid.GarbageCollection.TrackGCState";
46
47
  // Feature gate key to turn GC sweep log off.
47
- const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
48
+ export const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
48
49
  // One day in milliseconds.
49
50
  export const oneDayMs = 1 * 24 * 60 * 60 * 1000;
50
51
  export const defaultInactiveTimeoutMs = 7 * oneDayMs; // 7 days
@@ -77,10 +78,10 @@ export class UnreferencedStateTracker {
77
78
  constructor(unreferencedTimestampMs,
78
79
  /** The time after which node transitions to Inactive state. */
79
80
  inactiveTimeoutMs,
81
+ /** The current reference timestamp used to track how long this node has been unreferenced for. */
82
+ currentReferenceTimestampMs,
80
83
  /** The time after which node transitions to SweepReady state; undefined if session expiry is disabled. */
81
- sweepTimeoutMs,
82
- /** The current reference timestamp; undefined if no ops have ever been processed which can happen in tests. */
83
- currentReferenceTimestampMs) {
84
+ sweepTimeoutMs) {
84
85
  this.unreferencedTimestampMs = unreferencedTimestampMs;
85
86
  this.inactiveTimeoutMs = inactiveTimeoutMs;
86
87
  this.sweepTimeoutMs = sweepTimeoutMs;
@@ -99,11 +100,7 @@ export class UnreferencedStateTracker {
99
100
  this.sweepTimer.restart(this.sweepTimeoutMs - this.inactiveTimeoutMs);
100
101
  }
101
102
  });
102
- // If there is no current reference timestamp, can't track the node's unreferenced state at this time.
103
- // This will happen later when updateTracking is called with a reference timestamp.
104
- if (currentReferenceTimestampMs !== undefined) {
105
- this.updateTracking(currentReferenceTimestampMs);
106
- }
103
+ this.updateTracking(currentReferenceTimestampMs);
107
104
  }
108
105
  get state() {
109
106
  return this._state;
@@ -145,8 +142,12 @@ export class UnreferencedStateTracker {
145
142
  * its state across summaries.
146
143
  *
147
144
  * Node - represented as nodeId, it's a node on the GC graph
145
+ *
148
146
  * Outbound Route - a path from one node to another node, think `nodeA` -\> `nodeB`
147
+ *
149
148
  * Graph - all nodes with their respective routes
149
+ *
150
+ * ```
150
151
  * GC Graph
151
152
  *
152
153
  * Node
@@ -156,6 +157,7 @@ export class UnreferencedStateTracker {
156
157
  * / \\
157
158
  * Node Node
158
159
  * NodeId = "dds1" NodeId = "dds2"
160
+ * ```
159
161
  */
160
162
  export class GarbageCollector {
161
163
  constructor(createParams) {
@@ -166,10 +168,12 @@ export class GarbageCollector {
166
168
  this._writeDataAtRoot = true;
167
169
  /**
168
170
  * Tells whether the initial GC state needs to be reset. This can happen under 2 conditions:
171
+ *
169
172
  * 1. The base snapshot contains GC state but GC is disabled. This will happen the first time GC is disabled after
170
- * it was enabled before. GC state needs to be removed from summary and all nodes should be marked referenced.
173
+ * it was enabled before. GC state needs to be removed from summary and all nodes should be marked referenced.
174
+ *
171
175
  * 2. The base snapshot does not have GC state but GC is enabled. This will happen the very first time GC runs on
172
- * a document and the first time GC is enabled after is was disabled before.
176
+ * a document and the first time GC is enabled after is was disabled before.
173
177
  *
174
178
  * Note that the state needs reset only for the very first time summary is generated by this client. After that, the
175
179
  * state will be up-to-date and this flag will be reset.
@@ -194,10 +198,12 @@ export class GarbageCollector {
194
198
  this.gcOptions = createParams.gcOptions;
195
199
  this.getNodePackagePath = createParams.getNodePackagePath;
196
200
  this.getLastSummaryTimestampMs = createParams.getLastSummaryTimestampMs;
201
+ this.activeConnection = createParams.activeConnection;
197
202
  const baseSnapshot = createParams.baseSnapshot;
198
203
  const metadata = createParams.metadata;
199
204
  const readAndParseBlob = createParams.readAndParseBlob;
200
205
  this.mc = loggerToMonitoringContext(ChildLogger.create(createParams.baseLogger, "GarbageCollector", { all: { completedGCRuns: () => this.completedRuns } }));
206
+ this.sweepReadyUsageHandler = new SweepReadyUsageDetectionHandler(createParams.getContainerDiagnosticId(), this.mc, this.runtime.closeFn);
201
207
  let prevSummaryGCVersion;
202
208
  /**
203
209
  * The following GC state is enabled during container creation and cannot be changed throughout its lifetime:
@@ -256,8 +262,11 @@ export class GarbageCollector {
256
262
  this.latestSummaryGCVersion = prevSummaryGCVersion !== null && prevSummaryGCVersion !== void 0 ? prevSummaryGCVersion : this.currentGCVersion;
257
263
  /**
258
264
  * Whether GC should run or not. The following conditions have to be met to run sweep:
265
+ *
259
266
  * 1. GC should be enabled for this container.
267
+ *
260
268
  * 2. GC should not be disabled via disableGC GC option.
269
+ *
261
270
  * These conditions can be overridden via runGCKey feature flag.
262
271
  */
263
272
  this.shouldRunGC = (_d = this.mc.config.getBoolean(runGCKey)) !== null && _d !== void 0 ? _d : (
@@ -267,10 +276,13 @@ export class GarbageCollector {
267
276
  && !this.gcOptions.disableGC);
268
277
  /**
269
278
  * Whether sweep should run or not. The following conditions have to be met to run sweep:
279
+ *
270
280
  * 1. Overall GC or mark phase must be enabled (this.shouldRunGC).
281
+ *
271
282
  * 2. Sweep timeout should be available. Without this, we wouldn't know when an object should be deleted.
283
+ *
272
284
  * 3. Sweep should be enabled for this container (this.sweepEnabled). This can be overridden via runSweep
273
- * feature flag.
285
+ * feature flag.
274
286
  */
275
287
  this.shouldRunSweep = false; // disable while TEMPORARY measure hardcoding snapshotCacheExpiryMs is here
276
288
  // this.shouldRunGC
@@ -300,52 +312,59 @@ export class GarbageCollector {
300
312
  if (baseSnapshot === undefined) {
301
313
  return undefined;
302
314
  }
303
- // For newer documents, GC data should be present in the GC tree in the root of the snapshot.
304
- const gcSnapshotTree = baseSnapshot.trees[gcTreeKey];
305
- if (gcSnapshotTree !== undefined) {
306
- // If the GC tree is written at root, we should also do the same.
307
- this._writeDataAtRoot = true;
308
- const baseGCState = await getGCStateFromSnapshot(gcSnapshotTree, readAndParseBlob);
309
- if (this.trackGCState) {
310
- this.latestSerializedSummaryState = JSON.stringify(generateSortedGCState(baseGCState));
311
- }
312
- return baseGCState;
313
- }
314
- // back-compat - Older documents will have the GC blobs in each data store's summary tree. Get them and
315
- // consolidate into IGarbageCollectionState format.
316
- // Add a node for the root node that is not present in older snapshot format.
317
- const gcState = { gcNodes: { "/": { outboundRoutes: [] } } };
318
- const dataStoreSnapshotTree = getSummaryForDatastores(baseSnapshot, metadata);
319
- assert(dataStoreSnapshotTree !== undefined, 0x2a8 /* "Expected data store snapshot tree in base snapshot" */);
320
- for (const [dsId, dsSnapshotTree] of Object.entries(dataStoreSnapshotTree.trees)) {
321
- const blobId = dsSnapshotTree.blobs[gcBlobKey];
322
- if (blobId === undefined) {
323
- continue;
324
- }
325
- const gcSummaryDetails = await readAndParseBlob(blobId);
326
- // If there are no nodes for this data store, skip it.
327
- if (((_a = gcSummaryDetails.gcData) === null || _a === void 0 ? void 0 : _a.gcNodes) === undefined) {
328
- continue;
329
- }
330
- const dsRootId = `/${dsId}`;
331
- // Since we used to write GC data at data store level, we won't have an entry for the root ("/").
332
- // Construct that entry by adding root data store ids to its outbound routes.
333
- const initialSnapshotDetails = await readAndParseBlob(dsSnapshotTree.blobs[dataStoreAttributesBlobName]);
334
- if (initialSnapshotDetails.isRootDataStore) {
335
- gcState.gcNodes["/"].outboundRoutes.push(dsRootId);
315
+ try {
316
+ // For newer documents, GC data should be present in the GC tree in the root of the snapshot.
317
+ const gcSnapshotTree = baseSnapshot.trees[gcTreeKey];
318
+ if (gcSnapshotTree !== undefined) {
319
+ // If the GC tree is written at root, we should also do the same.
320
+ this._writeDataAtRoot = true;
321
+ const baseGCState = await getGCStateFromSnapshot(gcSnapshotTree, readAndParseBlob);
322
+ if (this.trackGCState) {
323
+ this.latestSerializedSummaryState = JSON.stringify(generateSortedGCState(baseGCState));
324
+ }
325
+ return baseGCState;
336
326
  }
337
- for (const [id, outboundRoutes] of Object.entries(gcSummaryDetails.gcData.gcNodes)) {
338
- // Prefix the data store id to the GC node ids to make them relative to the root from being
339
- // relative to the data store. Similar to how its done in DataStore::getGCData.
340
- const rootId = id === "/" ? dsRootId : `${dsRootId}${id}`;
341
- gcState.gcNodes[rootId] = { outboundRoutes: Array.from(outboundRoutes) };
327
+ // back-compat - Older documents will have the GC blobs in each data store's summary tree. Get them and
328
+ // consolidate into IGarbageCollectionState format.
329
+ // Add a node for the root node that is not present in older snapshot format.
330
+ const gcState = { gcNodes: { "/": { outboundRoutes: [] } } };
331
+ const dataStoreSnapshotTree = getSummaryForDatastores(baseSnapshot, metadata);
332
+ assert(dataStoreSnapshotTree !== undefined, 0x2a8 /* "Expected data store snapshot tree in base snapshot" */);
333
+ for (const [dsId, dsSnapshotTree] of Object.entries(dataStoreSnapshotTree.trees)) {
334
+ const blobId = dsSnapshotTree.blobs[gcBlobKey];
335
+ if (blobId === undefined) {
336
+ continue;
337
+ }
338
+ const gcSummaryDetails = await readAndParseBlob(blobId);
339
+ // If there are no nodes for this data store, skip it.
340
+ if (((_a = gcSummaryDetails.gcData) === null || _a === void 0 ? void 0 : _a.gcNodes) === undefined) {
341
+ continue;
342
+ }
343
+ const dsRootId = `/${dsId}`;
344
+ // Since we used to write GC data at data store level, we won't have an entry for the root ("/").
345
+ // Construct that entry by adding root data store ids to its outbound routes.
346
+ const initialSnapshotDetails = await readAndParseBlob(dsSnapshotTree.blobs[dataStoreAttributesBlobName]);
347
+ if (initialSnapshotDetails.isRootDataStore) {
348
+ gcState.gcNodes["/"].outboundRoutes.push(dsRootId);
349
+ }
350
+ for (const [id, outboundRoutes] of Object.entries(gcSummaryDetails.gcData.gcNodes)) {
351
+ // Prefix the data store id to the GC node ids to make them relative to the root from being
352
+ // relative to the data store. Similar to how its done in DataStore::getGCData.
353
+ const rootId = id === "/" ? dsRootId : `${dsRootId}${id}`;
354
+ gcState.gcNodes[rootId] = { outboundRoutes: Array.from(outboundRoutes) };
355
+ }
356
+ assert(gcState.gcNodes[dsRootId] !== undefined, 0x2a9 /* GC nodes for data store not in GC blob */);
357
+ gcState.gcNodes[dsRootId].unreferencedTimestampMs = gcSummaryDetails.unrefTimestamp;
342
358
  }
343
- assert(gcState.gcNodes[dsRootId] !== undefined, 0x2a9 /* GC nodes for data store not in GC blob */);
344
- gcState.gcNodes[dsRootId].unreferencedTimestampMs = gcSummaryDetails.unrefTimestamp;
359
+ // If there is only one node (root node just added above), either GC is disabled or we are loading from
360
+ // the first summary generated by detached container. In both cases, GC was not run - return undefined.
361
+ return Object.keys(gcState.gcNodes).length === 1 ? undefined : gcState;
362
+ }
363
+ catch (error) {
364
+ const dpe = DataProcessingError.wrapIfUnrecognized(error, "FailedToInitializeGC");
365
+ dpe.addTelemetryProperties({ gcConfigs: JSON.stringify(this.configs) });
366
+ throw dpe;
345
367
  }
346
- // If there is only one node (root node just added above), either GC is disabled or we are loading from the
347
- // very first summary generated by detached container. In both cases, GC was not run - return undefined.
348
- return Object.keys(gcState.gcNodes).length === 1 ? undefined : gcState;
349
368
  });
350
369
  /**
351
370
  * Set up the initializer which initializes the base GC state from the base snapshot. Note that the reference
@@ -354,14 +373,36 @@ export class GarbageCollector {
354
373
  */
355
374
  this.initializeBaseStateP = new LazyPromise(async () => {
356
375
  const currentReferenceTimestampMs = this.runtime.getCurrentReferenceTimestampMs();
376
+ /**
377
+ * If there is no current reference timestamp, skip initialization. We need the current timestamp to track
378
+ * how long objects have been unreferenced and if they can be deleted.
379
+ *
380
+ * Note that the only scenario where there is no reference timestamp is when no ops have ever been processed
381
+ * for this container and it is in read mode. In this scenario, there is no point in running GC anyway
382
+ * because references in the container do not change without any ops, i.e., there is nothing to collect.
383
+ */
384
+ if (currentReferenceTimestampMs === undefined) {
385
+ // Log an event so we can evaluate how often we run into this scenario.
386
+ this.mc.logger.sendErrorEvent({
387
+ eventName: "GarbageCollectorInitializedWithoutTimestamp",
388
+ gcConfigs: JSON.stringify(this.configs),
389
+ });
390
+ return;
391
+ }
357
392
  const baseState = await baseSummaryStateP;
393
+ /**
394
+ * The base state will not be present if the container is loaded from:
395
+ * 1. The first summary created by the detached container.
396
+ * 2. A summary that was generated with GC disabled.
397
+ * 3. A summary that was generated before GC even existed.
398
+ */
358
399
  if (baseState === undefined) {
359
400
  return;
360
401
  }
361
402
  const gcNodes = {};
362
403
  for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
363
404
  if (nodeData.unreferencedTimestampMs !== undefined) {
364
- this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(nodeData.unreferencedTimestampMs, this.inactiveTimeoutMs, this.sweepTimeoutMs, currentReferenceTimestampMs));
405
+ this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(nodeData.unreferencedTimestampMs, this.inactiveTimeoutMs, currentReferenceTimestampMs, this.sweepTimeoutMs));
365
406
  }
366
407
  gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
367
408
  }
@@ -397,19 +438,10 @@ export class GarbageCollector {
397
438
  });
398
439
  // Log all the GC options and the state determined by the garbage collector. This is interesting only for the
399
440
  // summarizer client since it is the only one that runs GC. It also helps keep the telemetry less noisy.
400
- 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));
401
441
  if (this.isSummarizerClient) {
402
442
  this.mc.logger.sendTelemetryEvent({
403
443
  eventName: "GarbageCollectorLoaded",
404
- gcConfigs: gcConfigProps,
405
- });
406
- }
407
- // Initialize the base state that is used to detect when inactive objects are used.
408
- if (this.shouldRunGC) {
409
- this.initializeBaseStateP.catch((error) => {
410
- const dpe = DataProcessingError.wrapIfUnrecognized(error, "FailedToInitializeGC");
411
- dpe.addTelemetryProperties({ gcConfigs: gcConfigProps });
412
- throw dpe;
444
+ gcConfigs: JSON.stringify(this.configs),
413
445
  });
414
446
  }
415
447
  }
@@ -418,11 +450,16 @@ export class GarbageCollector {
418
450
  }
419
451
  /**
420
452
  * Tells whether the GC state needs to be reset in the next summary. We need to do this if:
453
+ *
421
454
  * 1. GC was enabled and is now disabled. The GC state needs to be removed and everything becomes referenced.
455
+ *
422
456
  * 2. GC was disabled and is now enabled. The GC state needs to be regenerated and added to summary.
457
+ *
423
458
  * 3. The GC version in the latest summary is different from the current GC version. This can happen if:
424
- * 3.1. The summary this client loaded with has data from a different GC version.
425
- * 3.2. This client's latest summary was updated from a snapshot that has a different GC version.
459
+ *
460
+ * 3.1. The summary this client loaded with has data from a different GC version.
461
+ *
462
+ * 3.2. This client's latest summary was updated from a snapshot that has a different GC version.
426
463
  */
427
464
  get summaryStateNeedsReset() {
428
465
  return this.initialStateNeedsReset ||
@@ -431,21 +468,63 @@ export class GarbageCollector {
431
468
  get writeDataAtRoot() {
432
469
  return this._writeDataAtRoot;
433
470
  }
471
+ /** Returns a list of all the configurations for garbage collection. */
472
+ get configs() {
473
+ return 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, trackGCState: this.trackGCState }, this.gcOptions);
474
+ }
475
+ /**
476
+ * Called when the connection state of the runtime changes, i.e., it connects or disconnects. GC subscribes to this
477
+ * to initialize the base state for non-summarizer clients so that they can track inactive / sweep ready nodes.
478
+ * @param connected - Whether the runtime connected / disconnected.
479
+ * @param clientId - The clientId of this runtime.
480
+ */
481
+ setConnectionState(connected, clientId) {
482
+ /**
483
+ * For non-summarizer clients, initialize the base state when the container becomes active, i.e., it transitions
484
+ * to "write" mode. This will ensure that the container's own join op is processed and there is a recent
485
+ * reference timestamp that will be used to update the state of unreferenced nodes. Also, all trailing ops which
486
+ * could affect the GC state will have been processed.
487
+ *
488
+ * Ideally, this initialization should only be done for summarizer client. However, we are currently rolling out
489
+ * sweep in phases and we want to track when inactive and sweep ready objects are used in any client.
490
+ */
491
+ if (this.activeConnection() && !this.isSummarizerClient && this.shouldRunGC) {
492
+ this.initializeBaseStateP.catch((error) => { });
493
+ }
494
+ }
434
495
  /**
435
496
  * Runs garbage collection and updates the reference / used state of the nodes in the container.
436
- * @returns the number of data stores that have been marked as unreferenced.
497
+ * @returns stats of the GC run or undefined if GC did not run.
437
498
  */
438
499
  async collectGarbage(options) {
439
- const { fullGC = this.gcOptions.runFullGC === true || this.summaryStateNeedsReset, } = options;
500
+ var _a;
501
+ const fullGC = (_a = options.fullGC) !== null && _a !== void 0 ? _a : (this.gcOptions.runFullGC === true || this.summaryStateNeedsReset);
440
502
  const logger = options.logger
441
503
  ? ChildLogger.create(options.logger, undefined, { all: { completedGCRuns: () => this.completedRuns } })
442
504
  : this.mc.logger;
505
+ /**
506
+ * If there is no current reference timestamp, skip running GC. We need the current timestamp to track
507
+ * how long objects have been unreferenced and if they should be deleted.
508
+ *
509
+ * Note that the only scenario where GC is called and there is no reference timestamp is when no ops have ever
510
+ * been processed for this container and it is in read mode. In this scenario, there is no point in running GC
511
+ * anyway because references in the container do not change without any ops, i.e., there is nothing to collect.
512
+ */
513
+ const currentReferenceTimestampMs = this.runtime.getCurrentReferenceTimestampMs();
514
+ if (currentReferenceTimestampMs === undefined) {
515
+ // Log an event so we can evaluate how often we run into this scenario.
516
+ logger.sendErrorEvent({
517
+ eventName: "CollectGarbageCalledWithoutTimestamp",
518
+ gcConfigs: JSON.stringify(this.configs),
519
+ });
520
+ return undefined;
521
+ }
443
522
  return PerformanceEvent.timedExecAsync(logger, { eventName: "GarbageCollection" }, async (event) => {
444
523
  await this.runPreGCSteps();
445
524
  // Get the runtime's GC data and run GC on the reference graph in it.
446
525
  const gcData = await this.runtime.getGCData(fullGC);
447
526
  const gcResult = runGarbageCollection(gcData.gcNodes, ["/"]);
448
- const gcStats = await this.runPostGCSteps(gcData, gcResult, logger);
527
+ const gcStats = await this.runPostGCSteps(gcData, gcResult, logger, currentReferenceTimestampMs);
449
528
  event.end(Object.assign({}, gcStats));
450
529
  this.completedRuns++;
451
530
  return gcStats;
@@ -457,7 +536,7 @@ export class GarbageCollector {
457
536
  // Let the runtime update its pending state before GC runs.
458
537
  await this.runtime.updateStateBeforeGC();
459
538
  }
460
- async runPostGCSteps(gcData, gcResult, logger) {
539
+ async runPostGCSteps(gcData, gcResult, logger, currentReferenceTimestampMs) {
461
540
  // Generate statistics from the current run. This is done before updating the current state because it
462
541
  // generates some of its data based on previous state of the system.
463
542
  const gcStats = this.generateStats(gcResult);
@@ -465,7 +544,6 @@ export class GarbageCollector {
465
544
  // the current run. We need to identify than and update their unreferenced state if needed.
466
545
  this.updateStateSinceLastRun(gcData, logger);
467
546
  // Update the current state and update the runtime of all routes or ids that used as per the GC run.
468
- const currentReferenceTimestampMs = this.runtime.getCurrentReferenceTimestampMs();
469
547
  this.updateCurrentState(gcData, gcResult, currentReferenceTimestampMs);
470
548
  this.runtime.updateUsedRoutes(gcResult.referencedNodeIds, currentReferenceTimestampMs);
471
549
  // Log events for objects that are ready to be deleted by sweep. When we have sweep enabled, we will
@@ -645,14 +723,6 @@ export class GarbageCollector {
645
723
  this.unreferencedNodesState.delete(nodeId);
646
724
  }
647
725
  }
648
- /**
649
- * If there is no current reference time, skip tracking when a node becomes unreferenced. This would happen
650
- * if no ops have been processed ever and we still try to run GC. If so, there is nothing interesting to track
651
- * anyway.
652
- */
653
- if (currentReferenceTimestampMs === undefined) {
654
- return;
655
- }
656
726
  /**
657
727
  * If a node became unreferenced in this run, start tracking it.
658
728
  * If a node was already unreferenced, update its tracking information. Since the current reference time is
@@ -661,7 +731,7 @@ export class GarbageCollector {
661
731
  for (const nodeId of gcResult.deletedNodeIds) {
662
732
  const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
663
733
  if (nodeStateTracker === undefined) {
664
- this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(currentReferenceTimestampMs, this.inactiveTimeoutMs, this.sweepTimeoutMs, currentReferenceTimestampMs));
734
+ this.unreferencedNodesState.set(nodeId, new UnreferencedStateTracker(currentReferenceTimestampMs, this.inactiveTimeoutMs, currentReferenceTimestampMs, this.sweepTimeoutMs));
665
735
  }
666
736
  else {
667
737
  nodeStateTracker.updateTracking(currentReferenceTimestampMs);
@@ -704,14 +774,19 @@ export class GarbageCollector {
704
774
  * run, and then add the references since last run.
705
775
  *
706
776
  * Note on why we need to combine the data from previous run, current run and all references in between -
777
+ *
707
778
  * 1. We need data from last run because some of its references may have been deleted since then. If those
708
- * references added new outbound references before getting deleted, we need to detect them.
779
+ * references added new outbound references before getting deleted, we need to detect them.
780
+ *
709
781
  * 2. We need new outbound references since last run because some of them may have been deleted later. If those
710
- * references added new outbound references before getting deleted, we need to detect them.
782
+ * references added new outbound references before getting deleted, we need to detect them.
783
+ *
711
784
  * 3. We need data from the current run because currently we may not detect when DDSes are referenced:
712
- * - We don't require DDSes handles to be stored in a referenced DDS. For this, we need GC at DDS level
713
- * which is tracked by https://github.com/microsoft/FluidFramework/issues/8470.
714
- * - A new data store may have "root" DDSes already created and we don't detect them today.
785
+ *
786
+ * - We don't require DDSes handles to be stored in a referenced DDS. For this, we need GC at DDS level
787
+ * which is tracked by https://github.com/microsoft/FluidFramework/issues/8470.
788
+ *
789
+ * - A new data store may have "root" DDSes already created and we don't detect them today.
715
790
  */
716
791
  const gcDataSuperSet = concatGarbageCollectionData(this.previousGCDataFromLastRun, currentGCData);
717
792
  this.newReferencesSinceLastRun.forEach((outboundRoutes, sourceNodeId) => {
@@ -839,9 +914,7 @@ export class GarbageCollector {
839
914
  * this will give us a view into how much deleted content a container has.
840
915
  */
841
916
  logSweepEvents(logger, currentReferenceTimestampMs) {
842
- if (this.mc.config.getBoolean(disableSweepLogKey) === true
843
- || currentReferenceTimestampMs === undefined
844
- || this.sweepTimeoutMs === undefined) {
917
+ if (this.mc.config.getBoolean(disableSweepLogKey) === true || this.sweepTimeoutMs === undefined) {
845
918
  return;
846
919
  }
847
920
  this.unreferencedNodesState.forEach((nodeStateTracker, nodeId) => {
@@ -879,11 +952,6 @@ export class GarbageCollector {
879
952
  if (currentReferenceTimestampMs === undefined || nodeStateTracker.state === UnreferencedState.Active) {
880
953
  return;
881
954
  }
882
- // For non-summarizer clients, only log "Loaded" type events since these objects may not be loaded in the
883
- // summarizer clients if they are based off of user actions (such as scrolling to content for these objects).
884
- if (!this.isSummarizerClient && usageType !== "Loaded") {
885
- return;
886
- }
887
955
  // We only care about data stores and attachment blobs for this telemetry since GC only marks these objects
888
956
  // as unreferenced. Also, if an inactive DDS is used, the corresponding data store store will also be used.
889
957
  const nodeType = this.runtime.getNodeType(nodeId);
@@ -917,7 +985,17 @@ export class GarbageCollector {
917
985
  this.pendingEventsQueue.push(Object.assign(Object.assign({}, propsToLog), { usageType, state }));
918
986
  }
919
987
  else {
920
- this.mc.logger.sendErrorEvent(Object.assign(Object.assign({}, propsToLog), { eventName: `${state}Object_${usageType}`, pkg: packagePath ? { value: packagePath.join("/"), tag: TelemetryDataTag.CodeArtifact } : undefined }));
988
+ // For non-summarizer clients, only log "Loaded" type events since these objects may not be loaded in the
989
+ // summarizer clients if they are based off of user actions (such as scrolling to content for these objects)
990
+ if (usageType === "Loaded") {
991
+ this.mc.logger.sendErrorEvent(Object.assign(Object.assign({}, propsToLog), { eventName: `${state}Object_${usageType}`, pkg: packagePath ? { value: packagePath.join("/"), tag: TelemetryDataTag.CodeArtifact } : undefined }));
992
+ }
993
+ // If SweepReady Usage Detection is enabed, the handler may close the interactive container.
994
+ // Once Sweep is fully implemented, this will be removed since the objects will be gone
995
+ // and errors will arise elsewhere in the runtime
996
+ if (state === UnreferencedState.SweepReady) {
997
+ this.sweepReadyUsageHandler.usageDetectedInInteractiveClient(Object.assign(Object.assign({}, propsToLog), { usageType }));
998
+ }
921
999
  }
922
1000
  }
923
1001
  async logUnreferencedEvents(logger) {