@fluidframework/container-runtime 2.0.0-internal.3.0.1 → 2.0.0-internal.3.0.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 (58) hide show
  1. package/dist/containerRuntime.d.ts +1 -1
  2. package/dist/containerRuntime.d.ts.map +1 -1
  3. package/dist/containerRuntime.js +43 -31
  4. package/dist/containerRuntime.js.map +1 -1
  5. package/dist/dataStoreContext.d.ts +12 -0
  6. package/dist/dataStoreContext.d.ts.map +1 -1
  7. package/dist/dataStoreContext.js +43 -5
  8. package/dist/dataStoreContext.js.map +1 -1
  9. package/dist/garbageCollection.d.ts +5 -4
  10. package/dist/garbageCollection.d.ts.map +1 -1
  11. package/dist/garbageCollection.js +15 -6
  12. package/dist/garbageCollection.js.map +1 -1
  13. package/dist/garbageCollectionConstants.d.ts +2 -0
  14. package/dist/garbageCollectionConstants.d.ts.map +1 -1
  15. package/dist/garbageCollectionConstants.js +3 -1
  16. package/dist/garbageCollectionConstants.js.map +1 -1
  17. package/dist/packageVersion.d.ts +1 -1
  18. package/dist/packageVersion.js +1 -1
  19. package/dist/packageVersion.js.map +1 -1
  20. package/dist/summaryFormat.d.ts +19 -0
  21. package/dist/summaryFormat.d.ts.map +1 -1
  22. package/dist/summaryFormat.js.map +1 -1
  23. package/dist/summaryGenerator.d.ts.map +1 -1
  24. package/dist/summaryGenerator.js +1 -1
  25. package/dist/summaryGenerator.js.map +1 -1
  26. package/lib/containerRuntime.d.ts +1 -1
  27. package/lib/containerRuntime.d.ts.map +1 -1
  28. package/lib/containerRuntime.js +43 -31
  29. package/lib/containerRuntime.js.map +1 -1
  30. package/lib/dataStoreContext.d.ts +12 -0
  31. package/lib/dataStoreContext.d.ts.map +1 -1
  32. package/lib/dataStoreContext.js +45 -7
  33. package/lib/dataStoreContext.js.map +1 -1
  34. package/lib/garbageCollection.d.ts +5 -4
  35. package/lib/garbageCollection.d.ts.map +1 -1
  36. package/lib/garbageCollection.js +16 -7
  37. package/lib/garbageCollection.js.map +1 -1
  38. package/lib/garbageCollectionConstants.d.ts +2 -0
  39. package/lib/garbageCollectionConstants.d.ts.map +1 -1
  40. package/lib/garbageCollectionConstants.js +2 -0
  41. package/lib/garbageCollectionConstants.js.map +1 -1
  42. package/lib/packageVersion.d.ts +1 -1
  43. package/lib/packageVersion.js +1 -1
  44. package/lib/packageVersion.js.map +1 -1
  45. package/lib/summaryFormat.d.ts +19 -0
  46. package/lib/summaryFormat.d.ts.map +1 -1
  47. package/lib/summaryFormat.js.map +1 -1
  48. package/lib/summaryGenerator.d.ts.map +1 -1
  49. package/lib/summaryGenerator.js +1 -1
  50. package/lib/summaryGenerator.js.map +1 -1
  51. package/package.json +19 -53
  52. package/src/containerRuntime.ts +78 -49
  53. package/src/dataStoreContext.ts +64 -5
  54. package/src/garbageCollection.ts +24 -9
  55. package/src/garbageCollectionConstants.ts +3 -0
  56. package/src/packageVersion.ts +1 -1
  57. package/src/summaryFormat.ts +22 -0
  58. package/src/summaryGenerator.ts +9 -5
@@ -94,6 +94,7 @@ import {
94
94
  addSummarizeResultToSummary,
95
95
  addTreeToSummary,
96
96
  createRootSummarizerNodeWithGC,
97
+ IFetchSnapshotResult,
97
98
  IRootSummarizerNodeWithGC,
98
99
  RequestParser,
99
100
  create404Response,
@@ -103,6 +104,7 @@ import {
103
104
  seqFromTree,
104
105
  calculateStats,
105
106
  TelemetryContext,
107
+ ReadAndParseBlob,
106
108
  } from "@fluidframework/runtime-utils";
107
109
  import { GCDataBuilder, trimLeadingAndTrailingSlashes } from "@fluidframework/garbage-collector";
108
110
  import { v4 as uuid } from "uuid";
@@ -2175,12 +2177,21 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2175
2177
  fullGC,
2176
2178
  } = options;
2177
2179
 
2180
+ const telemetryContext = new TelemetryContext();
2181
+ // Add the options that are used to generate this summary to the telemetry context.
2182
+ telemetryContext.setAll("fluid_Summarize", "Options", {
2183
+ fullTree,
2184
+ trackState,
2185
+ runGC,
2186
+ fullGC,
2187
+ runSweep,
2188
+ });
2189
+
2178
2190
  let gcStats: IGCStats | undefined;
2179
2191
  if (runGC) {
2180
- gcStats = await this.collectGarbage({ logger: summaryLogger, runSweep, fullGC });
2192
+ gcStats = await this.collectGarbage({ logger: summaryLogger, runSweep, fullGC }, telemetryContext);
2181
2193
  }
2182
2194
 
2183
- const telemetryContext = new TelemetryContext();
2184
2195
  const { stats, summary } = await this.summarizerNode.summarize(fullTree, trackState, telemetryContext);
2185
2196
 
2186
2197
  this.logger.sendTelemetryEvent({ eventName: "SummarizeTelemetry", details: telemetryContext.serialize() });
@@ -2334,8 +2345,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2334
2345
  /** True to generate full GC data */
2335
2346
  fullGC?: boolean;
2336
2347
  },
2348
+ telemetryContext?: ITelemetryContext,
2337
2349
  ): Promise<IGCStats | undefined> {
2338
- return this.garbageCollector.collectGarbage(options);
2350
+ return this.garbageCollector.collectGarbage(options, telemetryContext);
2339
2351
  }
2340
2352
 
2341
2353
  /**
@@ -2835,18 +2847,17 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2835
2847
  // The call to fetch the snapshot is very expensive and not always needed.
2836
2848
  // It should only be done by the summarizerNode, if required.
2837
2849
  // When fetching from storage we will always get the latest version and do not use the ackHandle.
2838
- const snapshotTreeFetcher = async () => {
2850
+ const fetchLatestSnapshot: () => Promise<IFetchSnapshotResult> = async () => {
2839
2851
  const fetchResult = await this.fetchLatestSnapshotFromStorage(
2840
2852
  summaryLogger,
2841
2853
  {
2842
- eventName: "RefreshLatestSummaryGetSnapshot",
2854
+ eventName: "RefreshLatestSummaryAckFetch",
2843
2855
  ackHandle,
2844
- summaryRefSeq,
2845
- fetchLatest: true,
2856
+ targetSequenceNumber: summaryRefSeq,
2846
2857
  },
2858
+ readAndParseBlob,
2847
2859
  );
2848
2860
 
2849
- const latestSnapshotRefSeq = await seqFromTree(fetchResult.snapshotTree, readAndParseBlob);
2850
2861
  /**
2851
2862
  * If the fetched snapshot is older than the one for which the ack was received, close the container.
2852
2863
  * This should never happen because an ack should be sent after the latest summary is updated in the server.
@@ -2857,7 +2868,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2857
2868
  * such cases, the file will be rolled back along with the ack and we will eventually reach a consistent
2858
2869
  * state.
2859
2870
  */
2860
- if (latestSnapshotRefSeq < summaryRefSeq) {
2871
+ if (fetchResult.latestSnapshotRefSeq < summaryRefSeq) {
2861
2872
  const error = DataProcessingError.create(
2862
2873
  "Fetched snapshot is older than the received ack",
2863
2874
  "RefreshLatestSummaryAck",
@@ -2865,44 +2876,36 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2865
2876
  {
2866
2877
  ackHandle,
2867
2878
  summaryRefSeq,
2868
- latestSnapshotRefSeq,
2879
+ latestSnapshotRefSeq: fetchResult.latestSnapshotRefSeq,
2869
2880
  },
2870
2881
  );
2871
2882
  this.closeFn(error);
2872
2883
  throw error;
2873
2884
  }
2874
2885
 
2875
- summaryLogger.sendTelemetryEvent(
2876
- {
2877
- eventName: "LatestSummaryRetrieved",
2878
- ackHandle,
2879
- lastSequenceNumber: latestSnapshotRefSeq,
2880
- targetSequenceNumber: summaryRefSeq,
2881
- });
2882
-
2883
2886
  // In case we had to retrieve the latest snapshot and it is different than summaryRefSeq,
2884
2887
  // wait for the delta manager to catch up before refreshing the latest Summary.
2885
- await this.waitForDeltaManagerToCatchup(latestSnapshotRefSeq,
2886
- summaryLogger);
2888
+ await this.waitForDeltaManagerToCatchup(
2889
+ fetchResult.latestSnapshotRefSeq,
2890
+ summaryLogger,
2891
+ );
2887
2892
 
2888
- return fetchResult.snapshotTree;
2893
+ return {
2894
+ snapshotTree: fetchResult.snapshotTree,
2895
+ snapshotRefSeq: fetchResult.latestSnapshotRefSeq,
2896
+ };
2889
2897
  };
2890
2898
 
2891
2899
  const result = await this.summarizerNode.refreshLatestSummary(
2892
2900
  proposalHandle,
2893
2901
  summaryRefSeq,
2894
- snapshotTreeFetcher,
2902
+ fetchLatestSnapshot,
2895
2903
  readAndParseBlob,
2896
2904
  summaryLogger,
2897
2905
  );
2898
2906
 
2899
2907
  // Notify the garbage collector so it can update its latest summary state.
2900
- await this.garbageCollector.refreshLatestSummary(
2901
- result,
2902
- proposalHandle,
2903
- summaryRefSeq,
2904
- readAndParseBlob,
2905
- );
2908
+ await this.garbageCollector.refreshLatestSummary(proposalHandle, result, readAndParseBlob);
2906
2909
  }
2907
2910
 
2908
2911
  /**
@@ -2914,30 +2917,31 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2914
2917
  private async refreshLatestSummaryAckFromServer(
2915
2918
  summaryLogger: ITelemetryLogger,
2916
2919
  ): Promise<{ latestSnapshotRefSeq: number; latestSnapshotVersionId: string | undefined; }> {
2917
- const { snapshotTree, versionId } = await this.fetchLatestSnapshotFromStorage(
2918
- summaryLogger,
2919
- {
2920
- eventName: "RefreshLatestSummaryGetSnapshot",
2921
- fetchLatest: true,
2922
- },
2923
- );
2924
-
2925
2920
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
2926
- const latestSnapshotRefSeq = await seqFromTree(snapshotTree, readAndParseBlob);
2927
-
2921
+ const { snapshotTree, versionId, latestSnapshotRefSeq } =
2922
+ await this.fetchLatestSnapshotFromStorage(
2923
+ summaryLogger,
2924
+ {
2925
+ eventName: "RefreshLatestSummaryFromServerFetch",
2926
+ },
2927
+ readAndParseBlob,
2928
+ );
2929
+ const fetchLatestSnapshot: IFetchSnapshotResult = {
2930
+ snapshotTree,
2931
+ snapshotRefSeq: latestSnapshotRefSeq,
2932
+ };
2928
2933
  const result = await this.summarizerNode.refreshLatestSummary(
2929
- undefined,
2934
+ undefined /* proposalHandle */,
2930
2935
  latestSnapshotRefSeq,
2931
- async () => snapshotTree,
2936
+ async () => fetchLatestSnapshot,
2932
2937
  readAndParseBlob,
2933
2938
  summaryLogger,
2934
2939
  );
2935
2940
 
2936
2941
  // Notify the garbage collector so it can update its latest summary state.
2937
2942
  await this.garbageCollector.refreshLatestSummary(
2943
+ undefined /* proposalHandle */,
2938
2944
  result,
2939
- undefined,
2940
- latestSnapshotRefSeq,
2941
2945
  readAndParseBlob,
2942
2946
  )
2943
2947
 
@@ -2947,29 +2951,54 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2947
2951
  private async fetchLatestSnapshotFromStorage(
2948
2952
  logger: ITelemetryLogger,
2949
2953
  event: ITelemetryGenericEvent,
2950
- ): Promise<{ snapshotTree: ISnapshotTree; versionId: string; }> {
2954
+ readAndParseBlob: ReadAndParseBlob,
2955
+ ): Promise<{ snapshotTree: ISnapshotTree; versionId: string; latestSnapshotRefSeq: number }> {
2951
2956
  return PerformanceEvent.timedExecAsync(
2952
- logger, event, async (perfEvent: {
2957
+ logger,
2958
+ event,
2959
+ async (perfEvent: {
2953
2960
  end: (arg0: {
2954
2961
  getVersionDuration?: number | undefined;
2955
2962
  getSnapshotDuration?: number | undefined;
2963
+ snapshotRefSeq?: number | undefined;
2964
+ snapshotVersion?: string | undefined;
2956
2965
  }) => void;
2957
2966
  }) => {
2958
- const stats: { getVersionDuration?: number; getSnapshotDuration?: number; } = {};
2967
+ const stats: {
2968
+ getVersionDuration?: number;
2969
+ getSnapshotDuration?: number;
2970
+ snapshotRefSeq?: number;
2971
+ snapshotVersion?: string;
2972
+ } = {};
2959
2973
  const trace = Trace.start();
2960
2974
 
2961
2975
  const versions = await this.storage.getVersions(
2962
- null, 1, "refreshLatestSummaryAckFromServer", FetchSource.noCache);
2963
- assert(!!versions && !!versions[0], 0x137 /* "Failed to get version from storage" */);
2976
+ null,
2977
+ 1,
2978
+ "refreshLatestSummaryAckFromServer",
2979
+ FetchSource.noCache,
2980
+ );
2981
+ assert(
2982
+ !!versions && !!versions[0],
2983
+ 0x137 /* "Failed to get version from storage" */,
2984
+ );
2964
2985
  stats.getVersionDuration = trace.trace().duration;
2965
2986
 
2966
2987
  const maybeSnapshot = await this.storage.getSnapshotTree(versions[0]);
2967
2988
  assert(!!maybeSnapshot, 0x138 /* "Failed to get snapshot from storage" */);
2968
2989
  stats.getSnapshotDuration = trace.trace().duration;
2990
+ const latestSnapshotRefSeq = await seqFromTree(maybeSnapshot, readAndParseBlob);
2991
+ stats.snapshotRefSeq = latestSnapshotRefSeq;
2992
+ stats.snapshotVersion = versions[0].id;
2969
2993
 
2970
2994
  perfEvent.end(stats);
2971
- return { snapshotTree: maybeSnapshot, versionId: versions[0].id };
2972
- });
2995
+ return {
2996
+ snapshotTree: maybeSnapshot,
2997
+ versionId: versions[0].id,
2998
+ latestSnapshotRefSeq,
2999
+ };
3000
+ },
3001
+ );
2973
3002
  }
2974
3003
 
2975
3004
  public notifyAttaching(snapshot: ISnapshotTreeWithBlobContents) {
@@ -61,9 +61,11 @@ import {
61
61
  import {
62
62
  addBlobToSummary,
63
63
  convertSummaryTreeToITree,
64
+ packagePathToTelemetryProperty,
64
65
  } from "@fluidframework/runtime-utils";
65
66
  import {
66
67
  ChildLogger,
68
+ generateStack,
67
69
  loggerToMonitoringContext,
68
70
  LoggingError,
69
71
  MonitoringContext,
@@ -261,6 +263,13 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
261
263
  private readonly thresholdOpsCounter: ThresholdCounter;
262
264
  private static readonly pendingOpsCountThreshold = 1000;
263
265
 
266
+ /**
267
+ * If the summarizer makes local changes, a telemetry event is logged. This has the potential to be very noisy.
268
+ * So, adding a count of how many telemetry events are logged per data store context. This can be
269
+ * controlled via feature flags.
270
+ */
271
+ private localChangesTelemetryCount: number;
272
+
264
273
  // The used routes of this node as per the last GC run. This is used to update the used routes of the channel
265
274
  // if it realizes after GC is run.
266
275
  private lastUsedRoutes: string[] | undefined;
@@ -318,6 +327,10 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
318
327
  this.throwOnTombstoneUsage =
319
328
  this.mc.config.getBoolean(throwOnTombstoneUsageKey) === true &&
320
329
  this.clientDetails.type !== summarizerClientType;
330
+
331
+ // By default, a data store can log maximum 10 local changes telemetry in summarizer.
332
+ this.localChangesTelemetryCount =
333
+ this.mc.config.getNumber("Fluid.Telemetry.LocalChangesTelemetryCount") ?? 10;
321
334
  }
322
335
 
323
336
  public dispose(): void {
@@ -343,8 +356,15 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
343
356
  this._tombstoned = tombstone;
344
357
  }
345
358
 
346
- private rejectDeferredRealize(reason: string, packageName?: string): never {
347
- throw new LoggingError(reason, { packageName: { value: packageName, tag: TelemetryDataTag.CodeArtifact } });
359
+ private rejectDeferredRealize(
360
+ reason: string,
361
+ failedPkgPath?: string,
362
+ fullPackageName?: readonly string[],
363
+ ): never {
364
+ throw new LoggingError(reason, {
365
+ failedPkgPath: { value: failedPkgPath, tag: TelemetryDataTag.CodeArtifact },
366
+ fullPackageName: packagePathToTelemetryProperty(fullPackageName),
367
+ });
348
368
  }
349
369
 
350
370
  public async realize(): Promise<IFluidDataStoreChannel> {
@@ -358,6 +378,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
358
378
  value: this.id,
359
379
  tag: TelemetryDataTag.CodeArtifact,
360
380
  },
381
+ packageName: packagePathToTelemetryProperty(this.pkg),
361
382
  });
362
383
  this.channelDeferred?.reject(errorWrapped);
363
384
  this.logger.sendErrorEvent({ eventName: "RealizeError" }, errorWrapped);
@@ -377,18 +398,22 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
377
398
  let lastPkg: string | undefined;
378
399
  for (const pkg of packages) {
379
400
  if (!registry) {
380
- this.rejectDeferredRealize("No registry for package", lastPkg);
401
+ this.rejectDeferredRealize("No registry for package", lastPkg, packages);
381
402
  }
382
403
  lastPkg = pkg;
383
404
  entry = await registry.get(pkg);
384
405
  if (!entry) {
385
- this.rejectDeferredRealize("Registry does not contain entry for the package", pkg);
406
+ this.rejectDeferredRealize(
407
+ "Registry does not contain entry for the package",
408
+ pkg,
409
+ packages,
410
+ );
386
411
  }
387
412
  registry = entry.IFluidDataStoreRegistry;
388
413
  }
389
414
  const factory = entry?.IFluidDataStoreFactory;
390
415
  if (factory === undefined) {
391
- this.rejectDeferredRealize("Can't find factory for package", lastPkg);
416
+ this.rejectDeferredRealize("Can't find factory for package", lastPkg, packages);
392
417
  }
393
418
 
394
419
  return { factory, registry };
@@ -627,6 +652,10 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
627
652
  content,
628
653
  type,
629
654
  };
655
+
656
+ // Summarizer clients should not submit messages.
657
+ this.identifyLocalChangeInSummarizer("DataStoreMessageSubmittedInSummarizer", type);
658
+
630
659
  this._containerRuntime.submitDataStoreOp(
631
660
  this.id,
632
661
  fluidDataStoreContent,
@@ -800,6 +829,33 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
800
829
  }
801
830
  }
802
831
 
832
+ /**
833
+ * Summarizer client should not have local changes. These changes can become part of the summary and can break
834
+ * eventual consistency. For example, the next summary (say at ref seq# 100) may contain these changes whereas
835
+ * other clients that are up-to-date till seq# 100 may not have them yet.
836
+ */
837
+ protected identifyLocalChangeInSummarizer(eventName: string, type?: string) {
838
+ if (this.clientDetails.type !== summarizerClientType || this.localChangesTelemetryCount <= 0) {
839
+ return;
840
+ }
841
+
842
+ // Log a telemetry if there are local changes in the summarizer. This will give us data on how often
843
+ // this is happening and which data stores do this. The eventual goal is to disallow local changes
844
+ // in the summarizer and the data will help us plan this.
845
+ this.mc.logger.sendTelemetryEvent({
846
+ eventName,
847
+ type,
848
+ fluidDataStoreId: {
849
+ value: this.id,
850
+ tag: TelemetryDataTag.CodeArtifact,
851
+ },
852
+ packageName: packagePathToTelemetryProperty(this.pkg),
853
+ isSummaryInProgress: this.summarizerNode.isSummaryInProgress?.(),
854
+ stack: generateStack(),
855
+ });
856
+ this.localChangesTelemetryCount--;
857
+ }
858
+
803
859
  public getCreateChildSummarizerNodeFn(id: string, createParam: CreateChildSummarizerNodeParam) {
804
860
  return (
805
861
  summarizeInternal: SummarizeInternalFn,
@@ -922,6 +978,9 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
922
978
  props.makeLocallyVisibleFn,
923
979
  );
924
980
 
981
+ // Summarizer client should not create local data stores.
982
+ this.identifyLocalChangeInSummarizer("DataStoreCreatedInSummarizer");
983
+
925
984
  this.snapshotTree = props.snapshotTree;
926
985
  if (props.isRootDataStore === true) {
927
986
  this.setInMemoryRoot();
@@ -63,7 +63,8 @@ import {
63
63
  runSessionExpiryKey,
64
64
  runSweepKey,
65
65
  stableGCVersion,
66
- trackGCStateKey
66
+ trackGCStateKey,
67
+ gcTombstoneGenerationOptionName
67
68
  } from "./garbageCollectionConstants";
68
69
  import { sendGCTombstoneEvent } from "./garbageCollectionTombstoneUtils";
69
70
  import { SweepReadyUsageDetectionHandler } from "./gcSweepReadyUsageDetection";
@@ -76,6 +77,7 @@ import {
76
77
  dataStoreAttributesBlobName,
77
78
  IGCMetadata,
78
79
  ICreateContainerMetadata,
80
+ GCFeatureMatrix,
79
81
  } from "./summaryFormat";
80
82
 
81
83
  /** The statistics of the system state after a garbage collection run. */
@@ -145,6 +147,7 @@ export interface IGarbageCollector {
145
147
  /** Run garbage collection and update the reference / used state of the system. */
146
148
  collectGarbage(
147
149
  options: { logger?: ITelemetryLogger; runSweep?: boolean; fullGC?: boolean; },
150
+ telemetryContext?: ITelemetryContext,
148
151
  ): Promise<IGCStats | undefined>;
149
152
  /** Summarizes the GC data and returns it as a summary tree. */
150
153
  summarize(
@@ -158,9 +161,8 @@ export interface IGarbageCollector {
158
161
  getBaseGCDetails(): Promise<IGarbageCollectionDetailsBase>;
159
162
  /** Called when the latest summary of the system has been refreshed. */
160
163
  refreshLatestSummary(
161
- result: RefreshSummaryResult,
162
164
  proposalHandle: string | undefined,
163
- summaryRefSeq: number,
165
+ result: RefreshSummaryResult,
164
166
  readAndParseBlob: ReadAndParseBlob,
165
167
  ): Promise<void>;
166
168
  /** Called when a node is updated. Used to detect and log when an inactive node is changed or loaded. */
@@ -418,6 +420,9 @@ export class GarbageCollector implements IGarbageCollector {
418
420
  // This is the version of GC data in the latest summary being tracked.
419
421
  private latestSummaryGCVersion: GCVersion;
420
422
 
423
+ // Feature Support info persisted to this container's summary
424
+ private readonly persistedGcFeatureMatrix: GCFeatureMatrix | undefined;
425
+
421
426
  // Keeps track of the GC state from the last run.
422
427
  private gcDataFromLastRun: IGarbageCollectionData | undefined;
423
428
  // Keeps a list of references (edges in the GC graph) between GC runs. Each entry has a node id and a list of
@@ -557,6 +562,7 @@ export class GarbageCollector implements IGarbageCollector {
557
562
  this.sweepTimeoutMs =
558
563
  metadata?.sweepTimeoutMs
559
564
  ?? computeSweepTimeout(this.sessionExpiryTimeoutMs); // Backfill old documents that didn't persist this
565
+ this.persistedGcFeatureMatrix = metadata?.gcFeatureMatrix;
560
566
  } else {
561
567
  // Sweep should not be enabled without enabling GC mark phase. We could silently disable sweep in this
562
568
  // scenario but explicitly failing makes it clearer and promotes correct usage.
@@ -581,6 +587,11 @@ export class GarbageCollector implements IGarbageCollector {
581
587
  this.sweepTimeoutMs =
582
588
  testOverrideSweepTimeoutMs
583
589
  ?? computeSweepTimeout(this.sessionExpiryTimeoutMs);
590
+ if (this.gcOptions[gcTombstoneGenerationOptionName] !== undefined) {
591
+ this.persistedGcFeatureMatrix = {
592
+ tombstoneGeneration: this.gcOptions[gcTombstoneGenerationOptionName],
593
+ };
594
+ }
584
595
  }
585
596
 
586
597
  // If session expiry is enabled, we need to close the container when the session expiry timeout expires.
@@ -950,6 +961,7 @@ export class GarbageCollector implements IGarbageCollector {
950
961
  /** True to generate full GC data */
951
962
  fullGC?: boolean;
952
963
  },
964
+ telemetryContext?: ITelemetryContext,
953
965
  ): Promise<IGCStats | undefined> {
954
966
  const fullGC = options.fullGC ?? (this.gcOptions.runFullGC === true || this.summaryStateNeedsReset);
955
967
  const logger = options.logger
@@ -974,6 +986,9 @@ export class GarbageCollector implements IGarbageCollector {
974
986
  return undefined;
975
987
  }
976
988
 
989
+ // Add the options that are used to run GC to the telemetry context.
990
+ telemetryContext?.setAll("fluid_GC", "Options", { fullGC, runSweep: options.runSweep });
991
+
977
992
  return PerformanceEvent.timedExecAsync(logger, { eventName: "GarbageCollection" }, async (event) => {
978
993
  await this.runPreGCSteps();
979
994
 
@@ -1160,6 +1175,7 @@ export class GarbageCollector implements IGarbageCollector {
1160
1175
  * into the metadata blob. If GC is disabled, the gcFeature is 0.
1161
1176
  */
1162
1177
  gcFeature: this.gcEnabled ? this.currentGCVersion : 0,
1178
+ gcFeatureMatrix: this.persistedGcFeatureMatrix,
1163
1179
  sessionExpiryTimeoutMs: this.sessionExpiryTimeoutMs,
1164
1180
  sweepEnabled: this.sweepEnabled,
1165
1181
  sweepTimeoutMs: this.sweepTimeoutMs,
@@ -1179,9 +1195,8 @@ export class GarbageCollector implements IGarbageCollector {
1179
1195
  * is downloaded and should be used to update the state.
1180
1196
  */
1181
1197
  public async refreshLatestSummary(
1182
- result: RefreshSummaryResult,
1183
1198
  proposalHandle: string | undefined,
1184
- summaryRefSeq: number,
1199
+ result: RefreshSummaryResult,
1185
1200
  readAndParseBlob: ReadAndParseBlob,
1186
1201
  ): Promise<void> {
1187
1202
  // If the latest summary was updated and the summary was tracked, this client is the one that generated this
@@ -1208,8 +1223,8 @@ export class GarbageCollector implements IGarbageCollector {
1208
1223
  }
1209
1224
 
1210
1225
  // If the summary was not tracked by this client, the state should be updated from the downloaded snapshot.
1211
- const snapshot = result.snapshot;
1212
- const metadataBlobId = snapshot.blobs[metadataBlobName];
1226
+ const snapshotTree = result.snapshotTree;
1227
+ const metadataBlobId = snapshotTree.blobs[metadataBlobName];
1213
1228
  if (metadataBlobId) {
1214
1229
  const metadata = await readAndParseBlob<IContainerRuntimeMetadata>(metadataBlobId);
1215
1230
  this.latestSummaryGCVersion = getGCVersion(metadata);
@@ -1223,10 +1238,10 @@ export class GarbageCollector implements IGarbageCollector {
1223
1238
  "No reference timestamp when updating GC state from snapshot",
1224
1239
  "refreshLatestSummary",
1225
1240
  undefined,
1226
- { proposalHandle, summaryRefSeq, details: JSON.stringify(this.configs) },
1241
+ { proposalHandle, summaryRefSeq: result.summaryRefSeq, details: JSON.stringify(this.configs) },
1227
1242
  );
1228
1243
  }
1229
- const gcSnapshotTree = snapshot.trees[gcTreeKey];
1244
+ const gcSnapshotTree = snapshotTree.trees[gcTreeKey];
1230
1245
  // If GC ran in the container that generated this snapshot, it will have a GC tree.
1231
1246
  this.wasGCRunInLatestSummary = gcSnapshotTree !== undefined;
1232
1247
  let latestGCData: IGarbageCollectionSnapshotData | undefined;
@@ -10,6 +10,9 @@ export const stableGCVersion: GCVersion = 1;
10
10
  /** The current version of garbage collection. */
11
11
  export const currentGCVersion: GCVersion = 2;
12
12
 
13
+ /** This undocumented GC Option (on ContainerRuntime Options) allows an app to disable enforcing GC on old documents by incrementing this value */
14
+ export const gcTombstoneGenerationOptionName = "gcTombstoneGeneration";
15
+
13
16
  // Feature gate key to turn GC on / off.
14
17
  export const runGCKey = "Fluid.GarbageCollection.RunGC";
15
18
  // Feature gate key to turn GC sweep on / off.
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.0.0-internal.3.0.1";
9
+ export const pkgVersion = "2.0.0-internal.3.0.2";
@@ -89,15 +89,37 @@ export interface ICreateContainerMetadata {
89
89
  createContainerTimestamp?: number;
90
90
  }
91
91
 
92
+ /** @see IGCMetadata.gcFeatureMatrix */
93
+ export interface GCFeatureMatrix {
94
+ /**
95
+ * The Tombstone Generation value in effect when this file was created.
96
+ * Gives a way for an app to disqualify old files from GC Tombstone enforcement
97
+ * Provided via Container Runtime Options
98
+ */
99
+ tombstoneGeneration?: number;
100
+ }
101
+
92
102
  export type GCVersion = number;
93
103
  export interface IGCMetadata {
94
104
  /**
95
105
  * The version of the GC code that was run to generate the GC data that is written in the summary.
106
+ * If the persisted value doesn't match the current value in the code, saved GC data will be discarded and regenerated from scratch.
96
107
  * Also, used to determine whether GC is enabled for this container or not:
97
108
  * - A value of 0 or undefined means GC is disabled.
98
109
  * - A value greater than 0 means GC is enabled.
99
110
  */
100
111
  readonly gcFeature?: GCVersion;
112
+
113
+ /**
114
+ * Numerical indication of feature support as of file creation time, used to determine feature availability over time.
115
+ * This info may come from multiple sources (FF code, config service, app via Container Runtime Options),
116
+ * and pertains to aspects of the document that may be fixed for its lifetime.
117
+ *
118
+ * For each dimension, if the persisted value is less than the currently provided value,
119
+ * then this file does not support the corresponding feature as currently implemented.
120
+ */
121
+ readonly gcFeatureMatrix?: GCFeatureMatrix;
122
+
101
123
  /**
102
124
  * Tells whether the GC sweep phase is enabled for this container.
103
125
  * - True means sweep phase is enabled.
@@ -205,11 +205,15 @@ export class SummaryGenerator {
205
205
  timeSinceLastSummary,
206
206
  };
207
207
 
208
- const summarizeEvent = PerformanceEvent.start(logger, {
209
- eventName: "Summarize",
210
- refreshLatestAck,
211
- ...summarizeTelemetryProps,
212
- });
208
+ const summarizeEvent = PerformanceEvent.start(
209
+ logger,
210
+ {
211
+ eventName: "Summarize",
212
+ refreshLatestAck,
213
+ ...summarizeTelemetryProps,
214
+ },
215
+ { start: true, end: true, cancel: "generic" },
216
+ );
213
217
 
214
218
  // Helper functions to report failures and return.
215
219
  const getFailMessage =