@fluidframework/container-runtime 2.0.0-dev.1.4.5.105745 → 2.0.0-dev.2.2.0.111723

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 (168) hide show
  1. package/.eslintrc.js +1 -1
  2. package/dist/batchManager.d.ts +11 -6
  3. package/dist/batchManager.d.ts.map +1 -1
  4. package/dist/batchManager.js +23 -13
  5. package/dist/batchManager.js.map +1 -1
  6. package/dist/containerRuntime.d.ts +74 -20
  7. package/dist/containerRuntime.d.ts.map +1 -1
  8. package/dist/containerRuntime.js +190 -137
  9. package/dist/containerRuntime.js.map +1 -1
  10. package/dist/dataStore.d.ts.map +1 -1
  11. package/dist/dataStore.js +6 -0
  12. package/dist/dataStore.js.map +1 -1
  13. package/dist/dataStoreContext.d.ts +14 -21
  14. package/dist/dataStoreContext.d.ts.map +1 -1
  15. package/dist/dataStoreContext.js +71 -57
  16. package/dist/dataStoreContext.js.map +1 -1
  17. package/dist/dataStoreContexts.js +1 -1
  18. package/dist/dataStoreContexts.js.map +1 -1
  19. package/dist/dataStores.d.ts +11 -10
  20. package/dist/dataStores.d.ts.map +1 -1
  21. package/dist/dataStores.js +50 -20
  22. package/dist/dataStores.js.map +1 -1
  23. package/dist/garbageCollection.d.ts +36 -19
  24. package/dist/garbageCollection.d.ts.map +1 -1
  25. package/dist/garbageCollection.js +207 -121
  26. package/dist/garbageCollection.js.map +1 -1
  27. package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -1
  28. package/dist/gcSweepReadyUsageDetection.js +3 -12
  29. package/dist/gcSweepReadyUsageDetection.js.map +1 -1
  30. package/dist/index.d.ts +4 -6
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -5
  33. package/dist/index.js.map +1 -1
  34. package/dist/opCompressor.d.ts +18 -0
  35. package/dist/opCompressor.d.ts.map +1 -0
  36. package/dist/opCompressor.js +50 -0
  37. package/dist/opCompressor.js.map +1 -0
  38. package/dist/opDecompressor.d.ts +20 -0
  39. package/dist/opDecompressor.d.ts.map +1 -0
  40. package/dist/opDecompressor.js +72 -0
  41. package/dist/opDecompressor.js.map +1 -0
  42. package/dist/packageVersion.d.ts +1 -1
  43. package/dist/packageVersion.js +1 -1
  44. package/dist/packageVersion.js.map +1 -1
  45. package/dist/pendingStateManager.d.ts +6 -26
  46. package/dist/pendingStateManager.d.ts.map +1 -1
  47. package/dist/pendingStateManager.js +42 -62
  48. package/dist/pendingStateManager.js.map +1 -1
  49. package/dist/runningSummarizer.d.ts +3 -2
  50. package/dist/runningSummarizer.d.ts.map +1 -1
  51. package/dist/runningSummarizer.js +10 -3
  52. package/dist/runningSummarizer.js.map +1 -1
  53. package/dist/scheduleManager.js.map +1 -1
  54. package/dist/summarizer.js +7 -2
  55. package/dist/summarizer.js.map +1 -1
  56. package/dist/summarizerClientElection.js +1 -1
  57. package/dist/summarizerClientElection.js.map +1 -1
  58. package/dist/summarizerHeuristics.d.ts.map +1 -1
  59. package/dist/summarizerHeuristics.js +0 -3
  60. package/dist/summarizerHeuristics.js.map +1 -1
  61. package/dist/summarizerTypes.d.ts +19 -2
  62. package/dist/summarizerTypes.d.ts.map +1 -1
  63. package/dist/summarizerTypes.js.map +1 -1
  64. package/dist/summaryFormat.d.ts +4 -2
  65. package/dist/summaryFormat.d.ts.map +1 -1
  66. package/dist/summaryFormat.js.map +1 -1
  67. package/dist/summaryGenerator.d.ts.map +1 -1
  68. package/dist/summaryGenerator.js +3 -2
  69. package/dist/summaryGenerator.js.map +1 -1
  70. package/dist/summaryManager.d.ts.map +1 -1
  71. package/dist/summaryManager.js +10 -6
  72. package/dist/summaryManager.js.map +1 -1
  73. package/garbageCollection.md +27 -22
  74. package/lib/batchManager.d.ts +11 -6
  75. package/lib/batchManager.d.ts.map +1 -1
  76. package/lib/batchManager.js +23 -13
  77. package/lib/batchManager.js.map +1 -1
  78. package/lib/containerRuntime.d.ts +74 -20
  79. package/lib/containerRuntime.d.ts.map +1 -1
  80. package/lib/containerRuntime.js +189 -136
  81. package/lib/containerRuntime.js.map +1 -1
  82. package/lib/dataStore.d.ts.map +1 -1
  83. package/lib/dataStore.js +6 -0
  84. package/lib/dataStore.js.map +1 -1
  85. package/lib/dataStoreContext.d.ts +14 -21
  86. package/lib/dataStoreContext.d.ts.map +1 -1
  87. package/lib/dataStoreContext.js +75 -61
  88. package/lib/dataStoreContext.js.map +1 -1
  89. package/lib/dataStoreContexts.js +1 -1
  90. package/lib/dataStoreContexts.js.map +1 -1
  91. package/lib/dataStores.d.ts +11 -10
  92. package/lib/dataStores.d.ts.map +1 -1
  93. package/lib/dataStores.js +53 -23
  94. package/lib/dataStores.js.map +1 -1
  95. package/lib/garbageCollection.d.ts +36 -19
  96. package/lib/garbageCollection.d.ts.map +1 -1
  97. package/lib/garbageCollection.js +208 -122
  98. package/lib/garbageCollection.js.map +1 -1
  99. package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -1
  100. package/lib/gcSweepReadyUsageDetection.js +3 -12
  101. package/lib/gcSweepReadyUsageDetection.js.map +1 -1
  102. package/lib/index.d.ts +4 -6
  103. package/lib/index.d.ts.map +1 -1
  104. package/lib/index.js +2 -4
  105. package/lib/index.js.map +1 -1
  106. package/lib/opCompressor.d.ts +18 -0
  107. package/lib/opCompressor.d.ts.map +1 -0
  108. package/lib/opCompressor.js +46 -0
  109. package/lib/opCompressor.js.map +1 -0
  110. package/lib/opDecompressor.d.ts +20 -0
  111. package/lib/opDecompressor.d.ts.map +1 -0
  112. package/lib/opDecompressor.js +68 -0
  113. package/lib/opDecompressor.js.map +1 -0
  114. package/lib/packageVersion.d.ts +1 -1
  115. package/lib/packageVersion.js +1 -1
  116. package/lib/packageVersion.js.map +1 -1
  117. package/lib/pendingStateManager.d.ts +6 -26
  118. package/lib/pendingStateManager.d.ts.map +1 -1
  119. package/lib/pendingStateManager.js +42 -62
  120. package/lib/pendingStateManager.js.map +1 -1
  121. package/lib/runningSummarizer.d.ts +3 -2
  122. package/lib/runningSummarizer.d.ts.map +1 -1
  123. package/lib/runningSummarizer.js +10 -3
  124. package/lib/runningSummarizer.js.map +1 -1
  125. package/lib/scheduleManager.js.map +1 -1
  126. package/lib/summarizer.js +7 -2
  127. package/lib/summarizer.js.map +1 -1
  128. package/lib/summarizerClientElection.js +1 -1
  129. package/lib/summarizerClientElection.js.map +1 -1
  130. package/lib/summarizerHeuristics.d.ts.map +1 -1
  131. package/lib/summarizerHeuristics.js +0 -3
  132. package/lib/summarizerHeuristics.js.map +1 -1
  133. package/lib/summarizerTypes.d.ts +19 -2
  134. package/lib/summarizerTypes.d.ts.map +1 -1
  135. package/lib/summarizerTypes.js.map +1 -1
  136. package/lib/summaryFormat.d.ts +4 -2
  137. package/lib/summaryFormat.d.ts.map +1 -1
  138. package/lib/summaryFormat.js.map +1 -1
  139. package/lib/summaryGenerator.d.ts.map +1 -1
  140. package/lib/summaryGenerator.js +3 -2
  141. package/lib/summaryGenerator.js.map +1 -1
  142. package/lib/summaryManager.d.ts.map +1 -1
  143. package/lib/summaryManager.js +10 -6
  144. package/lib/summaryManager.js.map +1 -1
  145. package/package.json +37 -63
  146. package/prettier.config.cjs +8 -0
  147. package/src/batchManager.ts +32 -15
  148. package/src/containerRuntime.ts +260 -156
  149. package/src/dataStore.ts +13 -1
  150. package/src/dataStoreContext.ts +100 -76
  151. package/src/dataStoreContexts.ts +1 -1
  152. package/src/dataStores.ts +61 -23
  153. package/src/garbageCollection.ts +257 -126
  154. package/src/gcSweepReadyUsageDetection.ts +2 -10
  155. package/src/index.ts +4 -4
  156. package/src/opCompressor.ts +59 -0
  157. package/src/opDecompressor.ts +82 -0
  158. package/src/packageVersion.ts +1 -1
  159. package/src/pendingStateManager.ts +57 -96
  160. package/src/runningSummarizer.ts +11 -3
  161. package/src/scheduleManager.ts +1 -0
  162. package/src/summarizer.ts +6 -6
  163. package/src/summarizerClientElection.ts +1 -1
  164. package/src/summarizerHeuristics.ts +0 -3
  165. package/src/summarizerTypes.ts +20 -7
  166. package/src/summaryFormat.ts +4 -2
  167. package/src/summaryGenerator.ts +3 -2
  168. package/src/summaryManager.ts +18 -7
@@ -25,15 +25,18 @@ import {
25
25
  ISummarizeResult,
26
26
  ITelemetryContext,
27
27
  IGarbageCollectionNodeData,
28
+ ISummaryTreeWithStats,
28
29
  } from "@fluidframework/runtime-definitions";
29
30
  import {
30
31
  mergeStats,
32
+ packagePathToTelemetryProperty,
31
33
  ReadAndParseBlob,
32
34
  RefreshSummaryResult,
33
35
  SummaryTreeBuilder,
34
36
  } from "@fluidframework/runtime-utils";
35
37
  import {
36
38
  ChildLogger,
39
+ generateStack,
37
40
  loggerToMonitoringContext,
38
41
  MonitoringContext,
39
42
  PerformanceEvent,
@@ -51,6 +54,7 @@ import {
51
54
  ReadFluidDataStoreAttributes,
52
55
  dataStoreAttributesBlobName,
53
56
  IGCMetadata,
57
+ ICreateContainerMetadata,
54
58
  } from "./summaryFormat";
55
59
 
56
60
  /** This is the current version of garbage collection. */
@@ -60,6 +64,8 @@ const GCVersion = 1;
60
64
  export const gcTreeKey = "gc";
61
65
  // They prefix for GC blobs in the GC tree in summary.
62
66
  export const gcBlobPrefix = "__gc";
67
+ // The key for tombstone blob in the GC tree in summary.
68
+ export const gcTombstoneBlobKey = "__tombstones";
63
69
 
64
70
  // Feature gate key to turn GC on / off.
65
71
  export const runGCKey = "Fluid.GarbageCollection.RunGC";
@@ -67,16 +73,16 @@ export const runGCKey = "Fluid.GarbageCollection.RunGC";
67
73
  export const runSweepKey = "Fluid.GarbageCollection.RunSweep";
68
74
  // Feature gate key to turn GC test mode on / off.
69
75
  export const gcTestModeKey = "Fluid.GarbageCollection.GCTestMode";
70
- // Feature gate key to write GC data at the root of the summary tree.
71
- const writeAtRootKey = "Fluid.GarbageCollection.WriteDataAtRoot";
72
76
  // Feature gate key to expire a session after a set period of time.
73
77
  export const runSessionExpiryKey = "Fluid.GarbageCollection.RunSessionExpiry";
74
- // Feature gate key to disable expiring session after a set period of time, even if expiry value is present.
75
- export const disableSessionExpiryKey = "Fluid.GarbageCollection.DisableSessionExpiry";
76
78
  // Feature gate key to write the gc blob as a handle if the data is the same.
77
79
  export const trackGCStateKey = "Fluid.GarbageCollection.TrackGCState";
78
80
  // Feature gate key to turn GC sweep log off.
79
81
  export const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
82
+ // Feature gate key to disable the tombstone feature, i.e., tombstone information is not read / written into summary.
83
+ export const disableTombstoneKey = "Fluid.GarbageCollection.DisableTombstone";
84
+ // Feature gate to enable throwing an error when tombstone object is used.
85
+ export const throwOnTombstoneUsageKey = "Fluid.GarbageCollection.ThrowOnTombstoneUsage";
80
86
 
81
87
  // One day in milliseconds.
82
88
  export const oneDayMs = 1 * 24 * 60 * 60 * 1000;
@@ -126,9 +132,9 @@ export interface IGarbageCollectionRuntime {
126
132
  /** Returns the garbage collection data of the runtime. */
127
133
  getGCData(fullGC?: boolean): Promise<IGarbageCollectionData>;
128
134
  /** After GC has run, called to notify the runtime of routes that are used in it. */
129
- updateUsedRoutes(usedRoutes: string[], gcTimestamp?: number): void;
130
- /** After GC has run, called to delete objects in the runtime whose routes are unused. */
131
- deleteUnusedRoutes(unusedRoutes: string[]): void;
135
+ updateUsedRoutes(usedRoutes: string[]): void;
136
+ /** After GC has run, called to notify the runtime of routes that are unused in it. */
137
+ updateUnusedRoutes(unusedRoutes: string[], tombstone: boolean): void;
132
138
  /** Returns a referenced timestamp to be used to track unreferenced nodes. */
133
139
  getCurrentReferenceTimestampMs(): number | undefined;
134
140
  /** Returns the type of the GC node. */
@@ -143,9 +149,9 @@ export interface IGarbageCollector {
143
149
  readonly shouldRunGC: boolean;
144
150
  /** Tells whether the GC state in summary needs to be reset in the next summary. */
145
151
  readonly summaryStateNeedsReset: boolean;
146
- /** Tells whether GC data should be written to the root of the summary tree. */
147
- readonly writeDataAtRoot: boolean;
148
152
  readonly trackGCState: boolean;
153
+ /** Initialize the state from the base snapshot after its creation. */
154
+ initializeBaseState(): Promise<void>;
149
155
  /** Run garbage collection and update the reference / used state of the system. */
150
156
  collectGarbage(
151
157
  options: { logger?: ITelemetryLogger; runSweep?: boolean; fullGC?: boolean; },
@@ -183,6 +189,7 @@ export interface IGarbageCollectorCreateParams {
183
189
  readonly baseLogger: ITelemetryLogger;
184
190
  readonly existing: boolean;
185
191
  readonly metadata: IContainerRuntimeMetadata | undefined;
192
+ readonly createContainerMetadata: ICreateContainerMetadata;
186
193
  readonly baseSnapshot: ISnapshotTree | undefined;
187
194
  readonly isSummarizerClient: boolean;
188
195
  readonly getNodePackagePath: (nodePath: string) => Promise<readonly string[] | undefined>;
@@ -190,7 +197,6 @@ export interface IGarbageCollectorCreateParams {
190
197
  readonly readAndParseBlob: ReadAndParseBlob;
191
198
  readonly activeConnection: () => boolean;
192
199
  readonly getContainerDiagnosticId: () => string;
193
- readonly snapshotCacheExpiryMs?: number;
194
200
  }
195
201
 
196
202
  /** The state of node that is unreferenced. */
@@ -220,6 +226,22 @@ interface IUnreferencedEventProps {
220
226
  viaHandle?: boolean;
221
227
  }
222
228
 
229
+ /**
230
+ * The GC data that is tracked for a summary that is submitted.
231
+ */
232
+ interface IGCSummaryTrackingData {
233
+ serializedGCState: string | undefined;
234
+ serializedTombstones: string | undefined;
235
+ }
236
+
237
+ /**
238
+ * The GC data that is read from a snapshot. It contains the GC state and tombstone state.
239
+ */
240
+ interface IGCSnapshotData {
241
+ gcState: IGarbageCollectionState;
242
+ tombstones: string[] | undefined;
243
+ }
244
+
223
245
  /**
224
246
  * Helper class that tracks the state of an unreferenced node such as the time it was unreferenced and if it can
225
247
  * be deleted by the sweep phase.
@@ -376,16 +398,9 @@ export class GarbageCollector implements IGarbageCollector {
376
398
  public readonly trackGCState: boolean;
377
399
 
378
400
  private readonly testMode: boolean;
401
+ private readonly tombstoneMode: boolean;
379
402
  private readonly mc: MonitoringContext;
380
403
 
381
- /**
382
- * Tells whether the GC data should be written to the root of the summary tree.
383
- */
384
- private _writeDataAtRoot: boolean = true;
385
- public get writeDataAtRoot(): boolean {
386
- return this._writeDataAtRoot;
387
- }
388
-
389
404
  /**
390
405
  * Tells whether the initial GC state needs to be reset. This can happen under 2 conditions:
391
406
  *
@@ -407,20 +422,24 @@ export class GarbageCollector implements IGarbageCollector {
407
422
 
408
423
  // Keeps track of the GC state from the last run.
409
424
  private previousGCDataFromLastRun: IGarbageCollectionData | undefined;
425
+ // Keeps a list of references (edges in the GC graph) between GC runs. Each entry has a node id and a list of
426
+ // outbound routes from that node.
427
+ private readonly newReferencesSinceLastRun: Map<string, string[]> = new Map();
428
+ private tombstones: string[] = [];
429
+
410
430
  /**
411
- * Keeps track of the serialized GC blob from the latest summary successfully submitted to the server.
431
+ * Keeps track of the GC data from the latest summary successfully submitted to and acked from the server.
412
432
  */
413
- private latestSerializedSummaryState: string | undefined;
433
+ private latestSummaryData: IGCSummaryTrackingData | undefined;
414
434
  /**
415
- * Keeps track of the serialized GC blob from the last GC run of the client.
435
+ * Keeps track of the GC data from the last summary submitted to the server but not yet acked.
416
436
  */
417
- private pendingSerializedSummaryState: string | undefined;
418
- // Keeps a list of references (edges in the GC graph) between GC runs. Each entry has a node id and a list of
419
- // outbound routes from that node.
420
- private readonly newReferencesSinceLastRun: Map<string, string[]> = new Map();
437
+ private pendingSummaryData: IGCSummaryTrackingData | undefined;
421
438
 
422
- // Promise when resolved initializes the base state of the nodes from the base summary state.
423
- private readonly initializeBaseStateP: Promise<void>;
439
+ // Promise when resolved returns the GC data data in the base snapshot.
440
+ private readonly baseSnapshotDataP: Promise<IGCSnapshotData | undefined>;
441
+ // Promise when resolved initializes the GC state from the data in the base snapshot.
442
+ private readonly initializeGCStateFromBaseSnapshotP: Promise<void>;
424
443
  // The map of data store ids to their GC details in the base summary returned in getDataStoreGCDetails().
425
444
  private readonly baseGCDetailsP: Promise<Map<string, IGarbageCollectionDetailsBase>>;
426
445
  // Map of node ids to their unreferenced state tracker.
@@ -438,6 +457,7 @@ export class GarbageCollector implements IGarbageCollector {
438
457
  private completedRuns = 0;
439
458
 
440
459
  private readonly runtime: IGarbageCollectionRuntime;
460
+ private readonly createContainerMetadata: ICreateContainerMetadata;
441
461
  private readonly gcOptions: IGCRuntimeOptions;
442
462
  private readonly isSummarizerClient: boolean;
443
463
 
@@ -462,9 +482,10 @@ export class GarbageCollector implements IGarbageCollector {
462
482
  sweepEnabled: this.sweepEnabled,
463
483
  runGC: this.shouldRunGC,
464
484
  runSweep: this.shouldRunSweep,
465
- writeAtRoot: this._writeDataAtRoot,
466
485
  testMode: this.testMode,
486
+ tombstoneMode: this.tombstoneMode,
467
487
  sessionExpiry: this.sessionExpiryTimeoutMs,
488
+ sweepTimeout: this.sweepTimeoutMs,
468
489
  inactiveTimeout: this.inactiveTimeoutMs,
469
490
  trackGCState: this.trackGCState,
470
491
  ...this.gcOptions,
@@ -478,6 +499,7 @@ export class GarbageCollector implements IGarbageCollector {
478
499
  this.runtime = createParams.runtime;
479
500
  this.isSummarizerClient = createParams.isSummarizerClient;
480
501
  this.gcOptions = createParams.gcOptions;
502
+ this.createContainerMetadata = createParams.createContainerMetadata;
481
503
  this.getNodePackagePath = createParams.getNodePackagePath;
482
504
  this.getLastSummaryTimestampMs = createParams.getLastSummaryTimestampMs;
483
505
  this.activeConnection = createParams.activeConnection;
@@ -498,6 +520,21 @@ export class GarbageCollector implements IGarbageCollector {
498
520
 
499
521
  let prevSummaryGCVersion: number | undefined;
500
522
 
523
+ /**
524
+ * Sweep timeout is the time after which unreferenced content can be swept.
525
+ * Sweep timeout = session expiry timeout + snapshot cache expiry timeout + one day buffer.
526
+ *
527
+ * The snapshot cache expiry timeout cannot be known precisely but the upper bound is 5 days.
528
+ * The buffer is added to account for any clock skew or other edge cases.
529
+ * We use server timestamps throughout so the skew should be minimal but make it 1 day to be safe.
530
+ */
531
+ function computeSweepTimeout(sessionExpiryTimeoutMs: number | undefined) {
532
+ const maxSnapshotCacheExpiryMs = 5 * oneDayMs;
533
+ const bufferMs = oneDayMs;
534
+ return sessionExpiryTimeoutMs &&
535
+ (sessionExpiryTimeoutMs + maxSnapshotCacheExpiryMs + bufferMs);
536
+ }
537
+
501
538
  /**
502
539
  * The following GC state is enabled during container creation and cannot be changed throughout its lifetime:
503
540
  * 1. Whether running GC mark phase is allowed or not.
@@ -512,6 +549,9 @@ export class GarbageCollector implements IGarbageCollector {
512
549
  this.gcEnabled = prevSummaryGCVersion > 0;
513
550
  this.sweepEnabled = metadata?.sweepEnabled ?? false;
514
551
  this.sessionExpiryTimeoutMs = metadata?.sessionExpiryTimeoutMs;
552
+ this.sweepTimeoutMs =
553
+ metadata?.sweepTimeoutMs
554
+ ?? computeSweepTimeout(this.sessionExpiryTimeoutMs); // Backfill old documents that didn't persist this
515
555
  } else {
516
556
  // Sweep should not be enabled without enabling GC mark phase. We could silently disable sweep in this
517
557
  // scenario but explicitly failing makes it clearer and promotes correct usage.
@@ -519,20 +559,28 @@ export class GarbageCollector implements IGarbageCollector {
519
559
  throw new UsageError("GC sweep phase cannot be enabled without enabling GC mark phase");
520
560
  }
521
561
 
562
+ // This Test Override only applies for new containers
563
+ const testOverrideSweepTimeoutMs =
564
+ this.mc.config.getNumber("Fluid.GarbageCollection.TestOverride.SweepTimeoutMs");
565
+
522
566
  // For new documents, GC is enabled by default. It can be explicitly disabled by setting the gcAllowed
523
567
  // flag in GC options to false.
524
568
  this.gcEnabled = this.gcOptions.gcAllowed !== false;
525
569
  // The sweep phase has to be explicitly enabled by setting the sweepAllowed flag in GC options to true.
526
- this.sweepEnabled = this.gcOptions.sweepAllowed === true;
570
+ // ...unless we're using the TestOverride
571
+ this.sweepEnabled = this.gcOptions.sweepAllowed === true || testOverrideSweepTimeoutMs !== undefined;
527
572
 
528
573
  // Set the Session Expiry only if the flag is enabled and GC is enabled.
529
574
  if (this.mc.config.getBoolean(runSessionExpiryKey) && this.gcEnabled) {
530
575
  this.sessionExpiryTimeoutMs = this.gcOptions.sessionExpiryTimeoutMs ?? defaultSessionExpiryDurationMs;
531
576
  }
577
+ this.sweepTimeoutMs =
578
+ testOverrideSweepTimeoutMs
579
+ ?? computeSweepTimeout(this.sessionExpiryTimeoutMs);
532
580
  }
533
581
 
534
582
  // If session expiry is enabled, we need to close the container when the session expiry timeout expires.
535
- if (this.sessionExpiryTimeoutMs !== undefined && this.mc.config.getBoolean(disableSessionExpiryKey) !== true) {
583
+ if (this.sessionExpiryTimeoutMs !== undefined) {
536
584
  // If Test Override config is set, override Session Expiry timeout.
537
585
  const overrideSessionExpiryTimeoutMs =
538
586
  this.mc.config.getNumber("Fluid.GarbageCollection.TestOverride.SessionExpiryMs");
@@ -543,21 +591,6 @@ export class GarbageCollector implements IGarbageCollector {
543
591
  () => { this.runtime.closeFn(new ClientSessionExpiredError(`Client session expired.`, timeoutMs)); },
544
592
  );
545
593
  this.sessionExpiryTimer.start();
546
-
547
- // TEMPORARY: Hardcode a default of 5 days which will be >= the policy's value in the ODSP driver.
548
- // This unblocks the Sweep Log (see logSweepEvents function).
549
- // This will be removed before sweep is fully implemented.
550
- const snapshotCacheExpiryMs = createParams.snapshotCacheExpiryMs ?? 5 * 24 * 60 * 60 * 1000;
551
-
552
- /**
553
- * Sweep timeout is the time after which unreferenced content can be swept.
554
- * Sweep timeout = session expiry timeout + snapshot cache expiry timeout + one day buffer. The buffer is
555
- * added to account for any clock skew. We use server timestamps throughout so the skew should be minimal
556
- * but make it one day to be safe.
557
- */
558
- if (snapshotCacheExpiryMs !== undefined) {
559
- this.sweepTimeoutMs = this.sessionExpiryTimeoutMs + snapshotCacheExpiryMs + oneDayMs;
560
- }
561
594
  }
562
595
 
563
596
  // For existing document, the latest summary is the one that we loaded from. So, use its GC version as the
@@ -584,16 +617,16 @@ export class GarbageCollector implements IGarbageCollector {
584
617
  * Whether sweep should run or not. The following conditions have to be met to run sweep:
585
618
  *
586
619
  * 1. Overall GC or mark phase must be enabled (this.shouldRunGC).
587
- *
588
620
  * 2. Sweep timeout should be available. Without this, we wouldn't know when an object should be deleted.
589
- *
590
- * 3. Sweep should be enabled for this container (this.sweepEnabled). This can be overridden via runSweep
621
+ * 3. The driver must implement the policy limiting the age of snapshots used for loading. Otherwise
622
+ * the Sweep Timeout calculation is not valid. We use the persisted value to ensure consistency over time.
623
+ * 4. Sweep should be enabled for this container (this.sweepEnabled). This can be overridden via runSweep
591
624
  * feature flag.
592
625
  */
593
- this.shouldRunSweep = false; // disable while TEMPORARY measure hardcoding snapshotCacheExpiryMs is here
594
- // this.shouldRunGC
595
- // && this.sweepTimeoutMs !== undefined
596
- // && (this.mc.config.getBoolean(runSweepKey) ?? this.sweepEnabled);
626
+ this.shouldRunSweep =
627
+ this.shouldRunGC
628
+ && this.sweepTimeoutMs !== undefined
629
+ && (this.mc.config.getBoolean(runSweepKey) ?? this.sweepEnabled);
597
630
 
598
631
  this.trackGCState = this.mc.config.getBoolean(trackGCStateKey) === true;
599
632
 
@@ -609,20 +642,17 @@ export class GarbageCollector implements IGarbageCollector {
609
642
 
610
643
  // Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
611
644
  this.testMode = this.mc.config.getBoolean(gcTestModeKey) ?? this.gcOptions.runGCInTestMode === true;
645
+ // Whether we are running in tombstone mode. This is true by default unless disabled via feature flags.
646
+ this.tombstoneMode = this.mc.config.getBoolean(disableTombstoneKey) !== true;
612
647
 
613
- // GC state is written into root of the summary tree by default. Can be overridden via feature flag for now.
614
- this._writeDataAtRoot = this.mc.config.getBoolean(writeAtRootKey) ?? true;
615
-
616
- if (this._writeDataAtRoot) {
617
- // The GC state needs to be reset if the base snapshot contains GC tree and GC is disabled or it doesn't
618
- // contain GC tree and GC is enabled.
619
- const gcTreePresent = baseSnapshot?.trees[gcTreeKey] !== undefined;
620
- this.initialStateNeedsReset = gcTreePresent !== this.shouldRunGC;
621
- }
648
+ // The GC state needs to be reset if the base snapshot contains GC tree and GC is disabled or it doesn't
649
+ // contain GC tree and GC is enabled.
650
+ const gcTreePresent = baseSnapshot?.trees[gcTreeKey] !== undefined;
651
+ this.initialStateNeedsReset = gcTreePresent !== this.shouldRunGC;
622
652
 
623
- // Get the GC state from the GC blob in the base snapshot. Use LazyPromise because we only want to do
624
- // this once since it involves fetching blobs from storage which is expensive.
625
- const baseSummaryStateP = new LazyPromise<IGarbageCollectionState | undefined>(async () => {
653
+ // Get the GC data from the base snapshot. Use LazyPromise because we only want to do this once since it
654
+ // it involves fetching blobs from storage which is expensive.
655
+ this.baseSnapshotDataP = new LazyPromise<IGCSnapshotData | undefined>(async () => {
626
656
  if (baseSnapshot === undefined) {
627
657
  return undefined;
628
658
  }
@@ -631,16 +661,10 @@ export class GarbageCollector implements IGarbageCollector {
631
661
  // For newer documents, GC data should be present in the GC tree in the root of the snapshot.
632
662
  const gcSnapshotTree = baseSnapshot.trees[gcTreeKey];
633
663
  if (gcSnapshotTree !== undefined) {
634
- // If the GC tree is written at root, we should also do the same.
635
- this._writeDataAtRoot = true;
636
- const baseGCState = await getGCStateFromSnapshot(
664
+ return getGCDataFromSnapshot(
637
665
  gcSnapshotTree,
638
666
  readAndParseBlob,
639
667
  );
640
- if (this.trackGCState) {
641
- this.latestSerializedSummaryState = JSON.stringify(generateSortedGCState(baseGCState));
642
- }
643
- return baseGCState;
644
668
  }
645
669
 
646
670
  // back-compat - Older documents will have the GC blobs in each data store's summary tree. Get them and
@@ -684,7 +708,7 @@ export class GarbageCollector implements IGarbageCollector {
684
708
  }
685
709
  // If there is only one node (root node just added above), either GC is disabled or we are loading from
686
710
  // the first summary generated by detached container. In both cases, GC was not run - return undefined.
687
- return Object.keys(gcState.gcNodes).length === 1 ? undefined : gcState;
711
+ return Object.keys(gcState.gcNodes).length === 1 ? undefined : { gcState, tombstones: undefined };
688
712
  } catch (error) {
689
713
  const dpe = DataProcessingError.wrapIfUnrecognized(
690
714
  error,
@@ -696,11 +720,11 @@ export class GarbageCollector implements IGarbageCollector {
696
720
  });
697
721
 
698
722
  /**
699
- * Set up the initializer which initializes the base GC state from the base snapshot. Note that the reference
700
- * timestamp maybe from old ops which were not summarized and stored in the file. So, the unreferenced state
701
- * may be out of date. This is fine because the state is updated every time GC runs based on the time then.
723
+ * Set up the initializer which initializes the GC state from the data in base snapshot. This is done when
724
+ * connected in write mode or when GC runs the first time. It sets up all unreferenced nodes from the base
725
+ * GC state and updates their inactive or sweep ready state.
702
726
  */
703
- this.initializeBaseStateP = new LazyPromise<void>(async () => {
727
+ this.initializeGCStateFromBaseSnapshotP = new LazyPromise<void>(async () => {
704
728
  const currentReferenceTimestampMs = this.runtime.getCurrentReferenceTimestampMs();
705
729
  /**
706
730
  * If there is no current reference timestamp, skip initialization. We need the current timestamp to track
@@ -719,19 +743,19 @@ export class GarbageCollector implements IGarbageCollector {
719
743
  return;
720
744
  }
721
745
 
722
- const baseState = await baseSummaryStateP;
746
+ const baseSnapshotData = await this.baseSnapshotDataP;
723
747
  /**
724
- * The base state will not be present if the container is loaded from:
748
+ * The base snapshot data will not be present if the container is loaded from:
725
749
  * 1. The first summary created by the detached container.
726
750
  * 2. A summary that was generated with GC disabled.
727
751
  * 3. A summary that was generated before GC even existed.
728
752
  */
729
- if (baseState === undefined) {
753
+ if (baseSnapshotData === undefined) {
730
754
  return;
731
755
  }
732
756
 
733
757
  const gcNodes: { [id: string]: string[]; } = {};
734
- for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
758
+ for (const [nodeId, nodeData] of Object.entries(baseSnapshotData.gcState.gcNodes)) {
735
759
  if (nodeData.unreferencedTimestampMs !== undefined) {
736
760
  this.unreferencedNodesState.set(
737
761
  nodeId,
@@ -746,18 +770,26 @@ export class GarbageCollector implements IGarbageCollector {
746
770
  gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
747
771
  }
748
772
  this.previousGCDataFromLastRun = { gcNodes };
773
+
774
+ // If tracking state across summaries, update latest summary data from the base snapshot's GC data.
775
+ if (this.trackGCState) {
776
+ this.latestSummaryData = {
777
+ serializedGCState: JSON.stringify(generateSortedGCState(baseSnapshotData.gcState)),
778
+ serializedTombstones: JSON.stringify(baseSnapshotData.tombstones),
779
+ };
780
+ }
749
781
  });
750
782
 
751
783
  // Get the GC details for each node from the GC state in the base summary. This is returned in getBaseGCDetails
752
784
  // which the caller uses to initialize each node's GC state.
753
785
  this.baseGCDetailsP = new LazyPromise<Map<string, IGarbageCollectionDetailsBase>>(async () => {
754
- const baseState = await baseSummaryStateP;
755
- if (baseState === undefined) {
786
+ const baseSnapshotData = await this.baseSnapshotDataP;
787
+ if (baseSnapshotData === undefined) {
756
788
  return new Map();
757
789
  }
758
790
 
759
791
  const gcNodes: { [id: string]: string[]; } = {};
760
- for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
792
+ for (const [nodeId, nodeData] of Object.entries(baseSnapshotData.gcState.gcNodes)) {
761
793
  gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
762
794
  }
763
795
  // Run GC on the nodes in the base summary to get the routes used in each node in the container.
@@ -768,7 +800,7 @@ export class GarbageCollector implements IGarbageCollector {
768
800
  const baseGCDetailsMap = unpackChildNodesGCDetails({ gcData: { gcNodes }, usedRoutes });
769
801
  // Currently, the nodes may write the GC data. So, we need to update its base GC details with the
770
802
  // unreferenced timestamp. Once we start writing the GC data here, we won't need to do this anymore.
771
- for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
803
+ for (const [nodeId, nodeData] of Object.entries(baseSnapshotData.gcState.gcNodes)) {
772
804
  if (nodeData.unreferencedTimestampMs !== undefined) {
773
805
  const dataStoreGCDetails = baseGCDetailsMap.get(nodeId.slice(1));
774
806
  if (dataStoreGCDetails !== undefined) {
@@ -789,6 +821,27 @@ export class GarbageCollector implements IGarbageCollector {
789
821
  }
790
822
  }
791
823
 
824
+ /**
825
+ * Called during container initialization. Initialize the tombstone state so that object are marked as tombstones
826
+ * before they are loaded or used. This is important to get accurate information of whether tombstoned object are
827
+ * in use or not.
828
+ */
829
+ public async initializeBaseState(): Promise<void> {
830
+ const baseSnapshotData = await this.baseSnapshotDataP;
831
+ /**
832
+ * The base snapshot data or tombstone state will not be present if the container is loaded from:
833
+ * 1. The first summary created by the detached container.
834
+ * 2. A summary that was generated with GC disabled.
835
+ * 3. A summary that was generated before GC even existed.
836
+ * 4. A summary that was generated with tombstone feature disabled.
837
+ */
838
+ if (!this.tombstoneMode || baseSnapshotData?.tombstones === undefined) {
839
+ return;
840
+ }
841
+ this.tombstones = baseSnapshotData.tombstones;
842
+ this.runtime.updateUnusedRoutes(this.tombstones, true /* tombstone */);
843
+ }
844
+
792
845
  /**
793
846
  * Called when the connection state of the runtime changes, i.e., it connects or disconnects. GC subscribes to this
794
847
  * to initialize the base state for non-summarizer clients so that they can track inactive / sweep ready nodes.
@@ -797,16 +850,20 @@ export class GarbageCollector implements IGarbageCollector {
797
850
  */
798
851
  public setConnectionState(connected: boolean, clientId?: string | undefined): void {
799
852
  /**
800
- * For non-summarizer clients, initialize the base state when the container becomes active, i.e., it transitions
853
+ * For all clients, initialize the base state when the container becomes active, i.e., it transitions
801
854
  * to "write" mode. This will ensure that the container's own join op is processed and there is a recent
802
855
  * reference timestamp that will be used to update the state of unreferenced nodes. Also, all trailing ops which
803
856
  * could affect the GC state will have been processed.
804
857
  *
858
+ * If GC is up-to-date for the client and the summarizing client, there will be an doubling of both
859
+ * InactiveObject_Loaded and SweepReady_Loaded errors, as there will be one from the sending client and one from
860
+ * the receiving summarizer client.
861
+ *
805
862
  * Ideally, this initialization should only be done for summarizer client. However, we are currently rolling out
806
863
  * sweep in phases and we want to track when inactive and sweep ready objects are used in any client.
807
864
  */
808
- if (this.activeConnection() && !this.isSummarizerClient && this.shouldRunGC) {
809
- this.initializeBaseStateP.catch((error) => {});
865
+ if (this.activeConnection() && this.shouldRunGC) {
866
+ this.initializeGCStateFromBaseSnapshotP.catch((error) => {});
810
867
  }
811
868
  }
812
869
 
@@ -855,15 +912,15 @@ export class GarbageCollector implements IGarbageCollector {
855
912
  const gcResult = runGarbageCollection(gcData.gcNodes, ["/"]);
856
913
 
857
914
  const gcStats = await this.runPostGCSteps(gcData, gcResult, logger, currentReferenceTimestampMs);
858
- event.end({ ...gcStats });
915
+ event.end({ ...gcStats, timestamp: currentReferenceTimestampMs });
859
916
  this.completedRuns++;
860
917
  return gcStats;
861
918
  }, { end: true, cancel: "error" });
862
919
  }
863
920
 
864
921
  private async runPreGCSteps() {
865
- // Ensure that base state has been initialized.
866
- await this.initializeBaseStateP;
922
+ // Ensure that state has been initialized from the base snapshot data.
923
+ await this.initializeGCStateFromBaseSnapshotP;
867
924
  // Let the runtime update its pending state before GC runs.
868
925
  await this.runtime.updateStateBeforeGC();
869
926
  }
@@ -884,7 +941,7 @@ export class GarbageCollector implements IGarbageCollector {
884
941
 
885
942
  // Update the current state and update the runtime of all routes or ids that used as per the GC run.
886
943
  this.updateCurrentState(gcData, gcResult, currentReferenceTimestampMs);
887
- this.runtime.updateUsedRoutes(gcResult.referencedNodeIds, currentReferenceTimestampMs);
944
+ this.runtime.updateUsedRoutes(gcResult.referencedNodeIds);
888
945
 
889
946
  // Log events for objects that are ready to be deleted by sweep. When we have sweep enabled, we will
890
947
  // delete these objects here instead.
@@ -893,7 +950,12 @@ export class GarbageCollector implements IGarbageCollector {
893
950
  // If we are running in GC test mode, delete objects for unused routes. This enables testing scenarios
894
951
  // involving access to deleted data.
895
952
  if (this.testMode) {
896
- this.runtime.deleteUnusedRoutes(gcResult.deletedNodeIds);
953
+ this.runtime.updateUnusedRoutes(gcResult.deletedNodeIds, false /* tombstone */);
954
+ } else if (this.tombstoneMode) {
955
+ // If we are running in GC tombstone mode, tombstone objects for unused routes. This enables testing
956
+ // scenarios involving access to "deleted" data without actually deleting the data from summaries.
957
+ // Note: we will not tombstone in test mode
958
+ this.runtime.updateUnusedRoutes(this.tombstones, true /* tombstone */);
897
959
  }
898
960
 
899
961
  // Log pending unreferenced events such as a node being used after inactive. This is done after GC runs and
@@ -926,35 +988,76 @@ export class GarbageCollector implements IGarbageCollector {
926
988
  };
927
989
  }
928
990
 
929
- const newSerializedSummaryState = JSON.stringify(generateSortedGCState(gcState));
991
+ const serializedGCState = JSON.stringify(generateSortedGCState(gcState));
992
+ const serializedTombstones = this.tombstoneMode
993
+ ? (this.tombstones.length > 0 ? JSON.stringify(this.tombstones.sort()) : undefined)
994
+ : undefined;
930
995
 
931
996
  /**
932
- * As an optimization if the GC tree hasn't changed and we're tracking the gc state, return a tree handle
933
- * instead of returning the whole GC tree. If there are changes, then we want to return the whole tree.
997
+ * Incremental summary of GC data - If any of the GC state or tombstone state hasn't changed since the last
998
+ * summary, send summary handles for them. Otherwise, send the data in summary blobs.
934
999
  */
935
1000
  if (this.trackGCState) {
936
- this.pendingSerializedSummaryState = newSerializedSummaryState;
937
- if (
938
- this.latestSerializedSummaryState !== undefined &&
939
- this.latestSerializedSummaryState === newSerializedSummaryState &&
940
- !fullTree &&
941
- trackState
942
- ) {
943
- const stats = mergeStats();
944
- stats.handleNodeCount++;
945
- return {
946
- summary: {
947
- type: SummaryType.Handle,
948
- handle: `/${gcTreeKey}`,
949
- handleType: SummaryType.Tree,
950
- },
951
- stats,
952
- };
1001
+ this.pendingSummaryData = { serializedGCState, serializedTombstones };
1002
+ if (trackState && !fullTree && this.latestSummaryData !== undefined) {
1003
+ // If neither GC state or tombstone state changed, send a summary handle for the entire GC data.
1004
+ if (this.latestSummaryData.serializedGCState === serializedGCState
1005
+ && this.latestSummaryData.serializedTombstones === serializedTombstones) {
1006
+ const stats = mergeStats();
1007
+ stats.handleNodeCount++;
1008
+ return {
1009
+ summary: {
1010
+ type: SummaryType.Handle,
1011
+ handle: `/${gcTreeKey}`,
1012
+ handleType: SummaryType.Tree,
1013
+ },
1014
+ stats,
1015
+ };
1016
+ }
1017
+
1018
+ // If either or both of GC state or tombstone state changed, build a GC summary tree.
1019
+ return this.buildGCSummaryTree(serializedGCState, serializedTombstones, true /* trackState */);
953
1020
  }
954
1021
  }
1022
+ // If not tracking GC state, build a GC summary tree without any summary handles.
1023
+ return this.buildGCSummaryTree(serializedGCState, serializedTombstones, false /* trackState */);
1024
+ }
955
1025
 
1026
+ /**
1027
+ * Builds the GC summary tree which contains GC state and tombstone state.
1028
+ * If trackState is false, both GC state and tombstone state are written as summary blobs.
1029
+ * If trackState is true, summary blob is written for GC state or tombstone state if they changed.
1030
+ * @param serializedGCState - The GC state serialized as string.
1031
+ * @param serializedTombstones - THe tombstone state serialized as string.
1032
+ * @param trackState - Whether we are tracking GC state across summaries.
1033
+ * @returns the GC summary tree.
1034
+ */
1035
+ private buildGCSummaryTree(
1036
+ serializedGCState: string,
1037
+ serializedTombstones: string | undefined,
1038
+ trackState: boolean,
1039
+ ): ISummaryTreeWithStats {
1040
+ const gcStateBlobKey = `${gcBlobPrefix}_root`;
956
1041
  const builder = new SummaryTreeBuilder();
957
- builder.addBlob(`${gcBlobPrefix}_root`, newSerializedSummaryState);
1042
+
1043
+ // If the GC state hasn't changed, write a summary handle, else write a summary blob for it.
1044
+ if (this.latestSummaryData?.serializedGCState === serializedGCState && trackState) {
1045
+ builder.addHandle(gcStateBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcStateBlobKey}`);
1046
+ } else {
1047
+ builder.addBlob(gcStateBlobKey, serializedGCState);
1048
+ }
1049
+
1050
+ // If there is no tombstone data, return only the GC state.
1051
+ if (serializedTombstones === undefined) {
1052
+ return builder.getSummaryTree();
1053
+ }
1054
+
1055
+ // If the tombstone state hasn't changed, write a summary handle, else write a summary blob for it.
1056
+ if (this.latestSummaryData?.serializedTombstones === serializedTombstones && trackState) {
1057
+ builder.addHandle(gcTombstoneBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcTombstoneBlobKey}`);
1058
+ } else {
1059
+ builder.addBlob(gcTombstoneBlobKey, serializedTombstones);
1060
+ }
958
1061
  return builder.getSummaryTree();
959
1062
  }
960
1063
 
@@ -967,6 +1070,7 @@ export class GarbageCollector implements IGarbageCollector {
967
1070
  gcFeature: this.gcEnabled ? this.currentGCVersion : 0,
968
1071
  sessionExpiryTimeoutMs: this.sessionExpiryTimeoutMs,
969
1072
  sweepEnabled: this.sweepEnabled,
1073
+ sweepTimeoutMs: this.sweepTimeoutMs,
970
1074
  };
971
1075
  }
972
1076
 
@@ -996,8 +1100,8 @@ export class GarbageCollector implements IGarbageCollector {
996
1100
  this.latestSummaryGCVersion = this.currentGCVersion;
997
1101
  this.initialStateNeedsReset = false;
998
1102
  if (this.trackGCState) {
999
- this.latestSerializedSummaryState = this.pendingSerializedSummaryState;
1000
- this.pendingSerializedSummaryState = undefined;
1103
+ this.latestSummaryData = this.pendingSummaryData;
1104
+ this.pendingSummaryData = undefined;
1001
1105
  }
1002
1106
  return;
1003
1107
  }
@@ -1012,15 +1116,18 @@ export class GarbageCollector implements IGarbageCollector {
1012
1116
 
1013
1117
  const gcSnapshotTree = snapshot.trees[gcTreeKey];
1014
1118
  if (gcSnapshotTree !== undefined && this.trackGCState) {
1015
- const latestGCState = await getGCStateFromSnapshot(
1119
+ const latestGCData = await getGCDataFromSnapshot(
1016
1120
  gcSnapshotTree,
1017
1121
  readAndParseBlob,
1018
1122
  );
1019
- this.latestSerializedSummaryState = JSON.stringify(generateSortedGCState(latestGCState));
1123
+ this.latestSummaryData = {
1124
+ serializedGCState: JSON.stringify(generateSortedGCState(latestGCData.gcState)),
1125
+ serializedTombstones: JSON.stringify(latestGCData.tombstones),
1126
+ };
1020
1127
  } else {
1021
- this.latestSerializedSummaryState = undefined;
1128
+ this.latestSummaryData = undefined;
1022
1129
  }
1023
- this.pendingSerializedSummaryState = undefined;
1130
+ this.pendingSummaryData = undefined;
1024
1131
  }
1025
1132
 
1026
1133
  /**
@@ -1098,6 +1205,7 @@ export class GarbageCollector implements IGarbageCollector {
1098
1205
  currentReferenceTimestampMs: number,
1099
1206
  ) {
1100
1207
  this.previousGCDataFromLastRun = cloneGCData(gcData);
1208
+ this.tombstones = [];
1101
1209
  this.newReferencesSinceLastRun.clear();
1102
1210
 
1103
1211
  // Iterate through the referenced nodes and stop tracking if they were unreferenced before.
@@ -1130,6 +1238,12 @@ export class GarbageCollector implements IGarbageCollector {
1130
1238
  );
1131
1239
  } else {
1132
1240
  nodeStateTracker.updateTracking(currentReferenceTimestampMs);
1241
+ if (this.tombstoneMode && nodeStateTracker.state === UnreferencedState.SweepReady) {
1242
+ const nodeType = this.runtime.getNodeType(nodeId);
1243
+ if (nodeType === GCNodeType.DataStore || nodeType === GCNodeType.Blob) {
1244
+ this.tombstones.push(nodeId);
1245
+ }
1246
+ }
1133
1247
  }
1134
1248
  }
1135
1249
  }
@@ -1155,7 +1269,7 @@ export class GarbageCollector implements IGarbageCollector {
1155
1269
  this.newReferencesSinceLastRun,
1156
1270
  );
1157
1271
 
1158
- if (this.writeDataAtRoot && missingExplicitReferences.length > 0) {
1272
+ if (missingExplicitReferences.length > 0) {
1159
1273
  missingExplicitReferences.forEach((missingExplicitReference) => {
1160
1274
  const event: ITelemetryPerformanceEvent = {
1161
1275
  eventName: "gcUnknownOutboundReferences",
@@ -1412,6 +1526,7 @@ export class GarbageCollector implements IGarbageCollector {
1412
1526
  : this.sweepTimeoutMs,
1413
1527
  completedGCRuns: this.completedRuns,
1414
1528
  lastSummaryTime: this.getLastSummaryTimestampMs(),
1529
+ ...this.createContainerMetadata,
1415
1530
  externalRequest: requestHeaders?.[RuntimeHeaders.externalRequest],
1416
1531
  viaHandle: requestHeaders?.[RuntimeHeaders.viaHandle],
1417
1532
  fromId: fromNodeId,
@@ -1420,16 +1535,21 @@ export class GarbageCollector implements IGarbageCollector {
1420
1535
  // For summarizer client, queue the event so it is logged the next time GC runs if the event is still valid.
1421
1536
  // For non-summarizer client, log the event now since GC won't run on it. This may result in false positives
1422
1537
  // but it's a good signal nonetheless and we can consume it with a grain of salt.
1538
+ // Inactive errors are usages of Objects that are unreferenced for at least a period of 7 days.
1539
+ // SweepReady errors are usages of Objects that will be deleted by GC Sweep!
1423
1540
  if (this.isSummarizerClient) {
1424
1541
  this.pendingEventsQueue.push({ ...propsToLog, usageType, state });
1425
1542
  } else {
1426
1543
  // For non-summarizer clients, only log "Loaded" type events since these objects may not be loaded in the
1427
1544
  // summarizer clients if they are based off of user actions (such as scrolling to content for these objects)
1545
+ // Events generated:
1546
+ // InactiveObject_Loaded, SweepReadyObject_Loaded
1428
1547
  if (usageType === "Loaded") {
1429
1548
  this.mc.logger.sendErrorEvent({
1430
1549
  ...propsToLog,
1431
1550
  eventName: `${state}Object_${usageType}`,
1432
- pkg: packagePath ? { value: packagePath.join("/"), tag: TelemetryDataTag.CodeArtifact } : undefined,
1551
+ pkg: packagePathToTelemetryProperty(packagePath),
1552
+ stack: generateStack(),
1433
1553
  });
1434
1554
  }
1435
1555
 
@@ -1443,6 +1563,11 @@ export class GarbageCollector implements IGarbageCollector {
1443
1563
  }
1444
1564
 
1445
1565
  private async logUnreferencedEvents(logger: ITelemetryLogger) {
1566
+ // Events sent come only from the summarizer client. In between summaries, events are pushed to a queue and at
1567
+ // summary time they are then logged.
1568
+ // Events generated:
1569
+ // InactiveObject_Loaded, InactiveObject_Changed, InactiveObject_Revived
1570
+ // SweepReadyObject_Loaded, SweepReadyObject_Changed, SweepReadyObject_Revived
1446
1571
  for (const eventProps of this.pendingEventsQueue) {
1447
1572
  const { usageType, state, ...propsToLog } = eventProps;
1448
1573
  /**
@@ -1469,15 +1594,21 @@ export class GarbageCollector implements IGarbageCollector {
1469
1594
  }
1470
1595
 
1471
1596
  /**
1472
- * Gets the garbage collection state from the given snapshot tree. The GC state may be written into multiple blobs.
1473
- * Merge the GC state from all such blobs and return the merged GC state.
1597
+ * Gets the base garbage collection state from the given snapshot tree. It contains GC state and tombstone state.
1598
+ * The GC state may be written into multiple blobs. Merge the GC state from all such blobs into one.
1474
1599
  */
1475
- async function getGCStateFromSnapshot(
1600
+ async function getGCDataFromSnapshot(
1476
1601
  gcSnapshotTree: ISnapshotTree,
1477
1602
  readAndParseBlob: ReadAndParseBlob,
1478
- ): Promise<IGarbageCollectionState> {
1603
+ ): Promise<IGCSnapshotData> {
1479
1604
  let rootGCState: IGarbageCollectionState = { gcNodes: {} };
1605
+ let tombstones: string[] | undefined;
1480
1606
  for (const key of Object.keys(gcSnapshotTree.blobs)) {
1607
+ if (key === gcTombstoneBlobKey) {
1608
+ tombstones = await readAndParseBlob<string[]>(gcSnapshotTree.blobs[key]);
1609
+ continue;
1610
+ }
1611
+
1481
1612
  // Skip blobs that do not start with the GC prefix.
1482
1613
  if (!key.startsWith(gcBlobPrefix)) {
1483
1614
  continue;
@@ -1492,7 +1623,7 @@ async function getGCStateFromSnapshot(
1492
1623
  // Merge the GC state of this blob into the root GC state.
1493
1624
  rootGCState = concatGarbageCollectionStates(rootGCState, gcState);
1494
1625
  }
1495
- return rootGCState;
1626
+ return { gcState: rootGCState, tombstones };
1496
1627
  }
1497
1628
 
1498
1629
  function generateSortedGCState(gcState: IGarbageCollectionState): IGarbageCollectionState {