@fluidframework/container-runtime 0.59.2001 → 0.59.3000

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 (143) hide show
  1. package/.eslintrc.js +0 -1
  2. package/dist/batchTracker.js +1 -1
  3. package/dist/batchTracker.js.map +1 -1
  4. package/dist/blobManager.d.ts +8 -1
  5. package/dist/blobManager.d.ts.map +1 -1
  6. package/dist/blobManager.js +27 -17
  7. package/dist/blobManager.js.map +1 -1
  8. package/dist/connectionTelemetry.js +8 -8
  9. package/dist/connectionTelemetry.js.map +1 -1
  10. package/dist/containerHandleContext.js +1 -1
  11. package/dist/containerHandleContext.js.map +1 -1
  12. package/dist/containerRuntime.d.ts +27 -17
  13. package/dist/containerRuntime.d.ts.map +1 -1
  14. package/dist/containerRuntime.js +152 -176
  15. package/dist/containerRuntime.js.map +1 -1
  16. package/dist/dataStore.js +1 -1
  17. package/dist/dataStore.js.map +1 -1
  18. package/dist/dataStoreContext.d.ts.map +1 -1
  19. package/dist/dataStoreContext.js +44 -44
  20. package/dist/dataStoreContext.js.map +1 -1
  21. package/dist/dataStoreContexts.d.ts +2 -2
  22. package/dist/dataStoreContexts.d.ts.map +1 -1
  23. package/dist/dataStoreContexts.js +8 -8
  24. package/dist/dataStoreContexts.js.map +1 -1
  25. package/dist/dataStores.d.ts +6 -4
  26. package/dist/dataStores.d.ts.map +1 -1
  27. package/dist/dataStores.js +50 -37
  28. package/dist/dataStores.js.map +1 -1
  29. package/dist/garbageCollection.d.ts +23 -23
  30. package/dist/garbageCollection.d.ts.map +1 -1
  31. package/dist/garbageCollection.js +81 -50
  32. package/dist/garbageCollection.js.map +1 -1
  33. package/dist/opTelemetry.js +1 -1
  34. package/dist/opTelemetry.js.map +1 -1
  35. package/dist/orderedClientElection.d.ts.map +1 -1
  36. package/dist/orderedClientElection.js +2 -2
  37. package/dist/orderedClientElection.js.map +1 -1
  38. package/dist/packageVersion.d.ts +1 -1
  39. package/dist/packageVersion.js +1 -1
  40. package/dist/packageVersion.js.map +1 -1
  41. package/dist/pendingStateManager.js +17 -17
  42. package/dist/pendingStateManager.js.map +1 -1
  43. package/dist/runWhileConnectedCoordinator.js +1 -1
  44. package/dist/runWhileConnectedCoordinator.js.map +1 -1
  45. package/dist/runningSummarizer.d.ts.map +1 -1
  46. package/dist/runningSummarizer.js +7 -6
  47. package/dist/runningSummarizer.js.map +1 -1
  48. package/dist/summarizer.d.ts.map +1 -1
  49. package/dist/summarizer.js +4 -3
  50. package/dist/summarizer.js.map +1 -1
  51. package/dist/summarizerClientElection.js.map +1 -1
  52. package/dist/summarizerHeuristics.d.ts +1 -1
  53. package/dist/summarizerHeuristics.d.ts.map +1 -1
  54. package/dist/summarizerHeuristics.js +1 -1
  55. package/dist/summarizerHeuristics.js.map +1 -1
  56. package/dist/summarizerTypes.d.ts +4 -2
  57. package/dist/summarizerTypes.d.ts.map +1 -1
  58. package/dist/summarizerTypes.js.map +1 -1
  59. package/dist/summaryCollection.js +2 -2
  60. package/dist/summaryCollection.js.map +1 -1
  61. package/dist/summaryFormat.d.ts +37 -11
  62. package/dist/summaryFormat.d.ts.map +1 -1
  63. package/dist/summaryFormat.js +12 -4
  64. package/dist/summaryFormat.js.map +1 -1
  65. package/dist/summaryGenerator.d.ts.map +1 -1
  66. package/dist/summaryGenerator.js +6 -4
  67. package/dist/summaryGenerator.js.map +1 -1
  68. package/dist/summaryManager.js +5 -5
  69. package/dist/summaryManager.js.map +1 -1
  70. package/dist/throttler.js +2 -2
  71. package/dist/throttler.js.map +1 -1
  72. package/lib/blobManager.d.ts +8 -1
  73. package/lib/blobManager.d.ts.map +1 -1
  74. package/lib/blobManager.js +19 -9
  75. package/lib/blobManager.js.map +1 -1
  76. package/lib/containerRuntime.d.ts +27 -17
  77. package/lib/containerRuntime.d.ts.map +1 -1
  78. package/lib/containerRuntime.js +71 -95
  79. package/lib/containerRuntime.js.map +1 -1
  80. package/lib/dataStore.js.map +1 -1
  81. package/lib/dataStoreContext.d.ts.map +1 -1
  82. package/lib/dataStoreContext.js.map +1 -1
  83. package/lib/dataStoreContexts.d.ts +2 -2
  84. package/lib/dataStoreContexts.d.ts.map +1 -1
  85. package/lib/dataStoreContexts.js +2 -2
  86. package/lib/dataStoreContexts.js.map +1 -1
  87. package/lib/dataStores.d.ts +6 -4
  88. package/lib/dataStores.d.ts.map +1 -1
  89. package/lib/dataStores.js +27 -14
  90. package/lib/dataStores.js.map +1 -1
  91. package/lib/garbageCollection.d.ts +23 -23
  92. package/lib/garbageCollection.d.ts.map +1 -1
  93. package/lib/garbageCollection.js +68 -37
  94. package/lib/garbageCollection.js.map +1 -1
  95. package/lib/opTelemetry.js.map +1 -1
  96. package/lib/orderedClientElection.d.ts.map +1 -1
  97. package/lib/orderedClientElection.js.map +1 -1
  98. package/lib/packageVersion.d.ts +1 -1
  99. package/lib/packageVersion.js +1 -1
  100. package/lib/packageVersion.js.map +1 -1
  101. package/lib/pendingStateManager.js.map +1 -1
  102. package/lib/runningSummarizer.d.ts.map +1 -1
  103. package/lib/runningSummarizer.js +4 -3
  104. package/lib/runningSummarizer.js.map +1 -1
  105. package/lib/summarizer.d.ts.map +1 -1
  106. package/lib/summarizer.js +1 -0
  107. package/lib/summarizer.js.map +1 -1
  108. package/lib/summarizerClientElection.js.map +1 -1
  109. package/lib/summarizerHeuristics.d.ts +1 -1
  110. package/lib/summarizerHeuristics.d.ts.map +1 -1
  111. package/lib/summarizerHeuristics.js +1 -1
  112. package/lib/summarizerHeuristics.js.map +1 -1
  113. package/lib/summarizerTypes.d.ts +4 -2
  114. package/lib/summarizerTypes.d.ts.map +1 -1
  115. package/lib/summarizerTypes.js.map +1 -1
  116. package/lib/summaryCollection.js.map +1 -1
  117. package/lib/summaryFormat.d.ts +37 -11
  118. package/lib/summaryFormat.d.ts.map +1 -1
  119. package/lib/summaryFormat.js +10 -2
  120. package/lib/summaryFormat.js.map +1 -1
  121. package/lib/summaryGenerator.d.ts.map +1 -1
  122. package/lib/summaryGenerator.js +2 -0
  123. package/lib/summaryGenerator.js.map +1 -1
  124. package/lib/summaryManager.js.map +1 -1
  125. package/lib/throttler.js.map +1 -1
  126. package/package.json +26 -20
  127. package/src/blobManager.ts +23 -11
  128. package/src/containerRuntime.ts +111 -139
  129. package/src/dataStoreContext.ts +8 -11
  130. package/src/dataStoreContexts.ts +5 -5
  131. package/src/dataStores.ts +35 -17
  132. package/src/garbageCollection.ts +100 -57
  133. package/src/orderedClientElection.ts +5 -10
  134. package/src/packageVersion.ts +1 -1
  135. package/src/pendingStateManager.ts +2 -2
  136. package/src/runningSummarizer.ts +8 -9
  137. package/src/summarizer.ts +2 -2
  138. package/src/summarizerHeuristics.ts +1 -1
  139. package/src/summarizerTypes.ts +8 -6
  140. package/src/summaryFormat.ts +38 -11
  141. package/src/summaryGenerator.ts +7 -5
  142. package/src/summaryManager.ts +2 -2
  143. package/src/throttler.ts +1 -1
package/src/dataStores.ts CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  } from "./dataStoreContext";
51
51
  import { IContainerRuntimeMetadata, nonDataStorePaths, rootHasIsolatedChannels } from "./summaryFormat";
52
52
  import { IDataStoreAliasMessage, isDataStoreAliasMessage } from "./dataStore";
53
+ import { GCNodeType } from "./garbageCollection";
53
54
 
54
55
  type PendingAliasResolve = (success: boolean) => void;
55
56
 
@@ -90,8 +91,8 @@ export class DataStores implements IDisposable {
90
91
  private readonly deleteChildSummarizerNodeFn: (id: string) => void,
91
92
  baseLogger: ITelemetryBaseLogger,
92
93
  getBaseGCDetails: () => Promise<Map<string, IGarbageCollectionDetailsBase>>,
93
- private readonly dataStoreChanged: (
94
- dataStorePath: string, timestampMs: number, packagePath?: readonly string[]) => void,
94
+ private readonly gcNodeUpdated: (
95
+ nodePath: string, timestampMs: number, packagePath?: readonly string[]) => void,
95
96
  private readonly aliasMap: Map<string, string>,
96
97
  private readonly writeGCDataAtRoot: boolean,
97
98
  private readonly contexts: DataStoreContexts = new DataStoreContexts(baseLogger),
@@ -272,8 +273,8 @@ export class DataStores implements IDisposable {
272
273
  return false;
273
274
  }
274
275
 
275
- const currentContext = this.contexts.get(aliasMessage.internalId);
276
- if (currentContext === undefined) {
276
+ const context = this.contexts.get(aliasMessage.internalId);
277
+ if (context === undefined) {
277
278
  this.logger.sendErrorEvent({
278
279
  eventName: "AliasFluidDataStoreNotFound",
279
280
  fluidDataStoreId: aliasMessage.internalId,
@@ -281,8 +282,15 @@ export class DataStores implements IDisposable {
281
282
  return false;
282
283
  }
283
284
 
284
- this.aliasMap.set(aliasMessage.alias, currentContext.id);
285
- currentContext.setInMemoryRoot();
285
+ const handle = new FluidObjectHandle(
286
+ context,
287
+ aliasMessage.internalId,
288
+ this.runtime.IFluidHandleContext,
289
+ );
290
+ this.runtime.addedGCOutboundReference(this.containerRuntimeHandle, handle);
291
+
292
+ this.aliasMap.set(aliasMessage.alias, context.id);
293
+ context.setInMemoryRoot();
286
294
  return true;
287
295
  }
288
296
 
@@ -319,8 +327,7 @@ export class DataStores implements IDisposable {
319
327
  public createDetachedDataStoreCore(
320
328
  pkg: Readonly<string[]>,
321
329
  isRoot: boolean,
322
- id = uuid()): IFluidDataStoreContextDetached
323
- {
330
+ id = uuid()): IFluidDataStoreContextDetached {
324
331
  const context = new LocalDetachedFluidDataStoreContext({
325
332
  id,
326
333
  pkg,
@@ -363,7 +370,7 @@ export class DataStores implements IDisposable {
363
370
  return context;
364
371
  }
365
372
 
366
- public get disposed() {return this.disposeOnce.evaluated;}
373
+ public get disposed() { return this.disposeOnce.evaluated; }
367
374
  public readonly dispose = () => this.disposeOnce.value;
368
375
 
369
376
  public resubmitDataStoreOp(content: any, localOpMetadata: unknown) {
@@ -393,8 +400,9 @@ export class DataStores implements IDisposable {
393
400
  assert(!!context, 0x162 /* "There should be a store context for the op" */);
394
401
  context.process(transformed, local, localMessageMetadata);
395
402
 
396
- // Notify that a data store changed. This is used to detect if a deleted data store is being used.
397
- this.dataStoreChanged(
403
+ // Notify that a GC node for the data store changed. This is used to detect if a deleted data store is
404
+ // being used.
405
+ this.gcNodeUpdated(
398
406
  `/${envelope.address}`,
399
407
  message.timestamp,
400
408
  context.isLoaded ? context.packagePath : undefined,
@@ -421,7 +429,10 @@ export class DataStores implements IDisposable {
421
429
  assert(!local, 0x163 /* "Missing datastore for local signal" */);
422
430
  this.logger.sendTelemetryEvent({
423
431
  eventName: "SignalFluidDataStoreNotFound",
424
- fluidDataStoreId: address,
432
+ fluidDataStoreId: {
433
+ value: address,
434
+ tag: TelemetryDataTag.PackageData,
435
+ },
425
436
  });
426
437
  return;
427
438
  }
@@ -633,14 +644,21 @@ export class DataStores implements IDisposable {
633
644
  }
634
645
 
635
646
  /**
636
- * Called by GC to know if a node is a data store or not. Data store ids are of the format "/dataStoreId".
647
+ * Called by GC to determine if a node is for a data store or for an object within a data store (for e.g. DDS).
648
+ * @returns the GC node type if the node belongs to a data store or object within data store, undefined otherwise.
637
649
  */
638
- public isDataStoreNode(nodePath: string): boolean {
650
+ public getGCNodeType(nodePath: string): GCNodeType | undefined {
639
651
  const pathParts = nodePath.split("/");
640
- if (pathParts.length === 2 && this.contexts.has(pathParts[1])) {
641
- return true;
652
+ if (!this.contexts.has(pathParts[1])) {
653
+ return undefined;
654
+ }
655
+
656
+ // Data stores paths are of the format "/dataStoreId".
657
+ // Sub data store paths are of the format "/dataStoreId/subPath/...".
658
+ if (pathParts.length === 2) {
659
+ return GCNodeType.DataStore;
642
660
  }
643
- return false;
661
+ return GCNodeType.SubDataStore;
644
662
  }
645
663
  }
646
664
 
@@ -6,7 +6,7 @@
6
6
  import { ITelemetryLogger, ITelemetryPerformanceEvent } from "@fluidframework/common-definitions";
7
7
  import { assert, LazyPromise, Timer } from "@fluidframework/common-utils";
8
8
  import { ICriticalContainerError } from "@fluidframework/container-definitions";
9
- import { ClientSessionExpiredError, DataProcessingError } from "@fluidframework/container-utils";
9
+ import { ClientSessionExpiredError, DataProcessingError, UsageError } from "@fluidframework/container-utils";
10
10
  import { IRequestHeader } from "@fluidframework/core-interfaces";
11
11
  import {
12
12
  cloneGCData,
@@ -46,6 +46,7 @@ import {
46
46
  metadataBlobName,
47
47
  ReadFluidDataStoreAttributes,
48
48
  dataStoreAttributesBlobName,
49
+ IGCMetadata,
49
50
  } from "./summaryFormat";
50
51
 
51
52
  /** This is the current version of garbage collection. */
@@ -65,7 +66,9 @@ const runSweepKey = "Fluid.GarbageCollection.RunSweep";
65
66
  // Feature gate key to write GC data at the root of the summary tree.
66
67
  const writeAtRootKey = "Fluid.GarbageCollection.WriteDataAtRoot";
67
68
  // Feature gate key to expire a session after a set period of time.
68
- const runSessionExpiry = "Fluid.GarbageCollection.RunSessionExpiry";
69
+ const runSessionExpiryKey = "Fluid.GarbageCollection.RunSessionExpiry";
70
+ // Feature gate key to disable expiring session after a set period of time, even if expiry value is present
71
+ const disableSessionExpiryKey = "Fluid.GarbageCollection.DisableSessionExpiry";
69
72
  // Feature gate key to log error messages if GC reference validation fails.
70
73
  const logUnknownOutboundReferencesKey = "Fluid.GarbageCollection.LogUnknownOutboundReferences";
71
74
 
@@ -98,9 +101,11 @@ export interface IGCStats {
98
101
  export const GCNodeType = {
99
102
  // Nodes that are for data stores.
100
103
  DataStore: "DataStore",
104
+ // Nodes that are within a data store. For example, DDS nodes.
105
+ SubDataStore: "SubDataStore",
101
106
  // Nodes that are for attachment blobs, i.e., blobs uploaded via BlobManager.
102
107
  Blob: "Blob",
103
- // Nodes that are neither data store not blobs. For example, root node and DDS nodes.
108
+ // Nodes that are neither of the above. For example, root node.
104
109
  Other: "Other",
105
110
  };
106
111
  export type GCNodeType = typeof GCNodeType[keyof typeof GCNodeType];
@@ -138,24 +143,18 @@ export interface IGarbageCollectionRuntime {
138
143
  export interface IGarbageCollector {
139
144
  /** Tells whether GC should run or not. */
140
145
  readonly shouldRunGC: boolean;
141
- /** The time in ms to expire a session for a client for gc. */
142
- readonly sessionExpiryTimeoutMs: number | undefined;
143
- /**
144
- * This tracks two things:
145
- * 1. Whether GC is enabled - If this is 0, GC is disabled. If this is greater than 0, GC is enabled.
146
- * 2. If GC is enabled, the version of GC used to generate the GC data written in a summary.
147
- */
148
- readonly gcSummaryFeatureVersion: number;
149
146
  /** Tells whether the GC state in summary needs to be reset in the next summary. */
150
147
  readonly summaryStateNeedsReset: boolean;
151
148
  /** Tells whether GC data should be written to the root of the summary tree. */
152
149
  readonly writeDataAtRoot: boolean;
153
150
  /** Run garbage collection and update the reference / used state of the system. */
154
151
  collectGarbage(
155
- options: { logger?: ITelemetryLogger, runGC?: boolean, runSweep?: boolean, fullGC?: boolean },
152
+ options: { logger?: ITelemetryLogger; runGC?: boolean; runSweep?: boolean; fullGC?: boolean; },
156
153
  ): Promise<IGCStats>;
157
154
  /** Summarizes the GC data and returns it as a summary tree. */
158
155
  summarize(): ISummaryTreeWithStats | undefined;
156
+ /** Returns the garbage collector specific metadata to be written into the summary. */
157
+ getMetadata(): IGCMetadata;
159
158
  /** Returns a map of each node id to its base GC details in the base summary. */
160
159
  getBaseGCDetails(): Promise<Map<string, IGarbageCollectionDetailsBase>>;
161
160
  /** Called when the latest summary of the system has been refreshed. */
@@ -231,15 +230,15 @@ class UnreferencedStateTracker {
231
230
  * its state across summaries.
232
231
  *
233
232
  * Node - represented as nodeId, it's a node on the GC graph
234
- * Outbound Route - a path from one node to another node, think `nodeA` -> `nodeB`
233
+ * Outbound Route - a path from one node to another node, think `nodeA` -\> `nodeB`
235
234
  * Graph - all nodes with their respective routes
236
235
  * GC Graph
237
236
  *
238
237
  * Node
239
238
  * NodeId = "datastore1"
240
- * / \
239
+ * / \\
241
240
  * OutboundRoute OutboundRoute
242
- * / \
241
+ * / \\
243
242
  * Node Node
244
243
  * NodeId = "dds1" NodeId = "dds2"
245
244
  */
@@ -268,24 +267,10 @@ export class GarbageCollector implements IGarbageCollector {
268
267
  );
269
268
  }
270
269
 
271
- /**
272
- * Tells whether GC should be run based on the GC options and local storage flags.
273
- */
274
- public readonly shouldRunGC: boolean;
275
-
276
270
  /**
277
271
  * The time in ms to expire a session for a client for gc.
278
272
  */
279
- public readonly sessionExpiryTimeoutMs: number | undefined;
280
-
281
- /**
282
- * This tracks two things:
283
- * 1. Whether GC is enabled - If this is 0, GC is disabled. If this is greater than 0, GC is enabled.
284
- * 2. If GC is enabled, the version of GC used to generate the GC data written in a summary.
285
- */
286
- public get gcSummaryFeatureVersion(): number {
287
- return this.gcEnabled ? this.currentGCVersion : 0;
288
- }
273
+ private readonly sessionExpiryTimeoutMs: number | undefined;
289
274
 
290
275
  /**
291
276
  * Tells whether the GC state needs to be reset in the next summary. We need to do this if:
@@ -305,7 +290,23 @@ export class GarbageCollector implements IGarbageCollector {
305
290
  * throughout its lifetime.
306
291
  */
307
292
  private readonly gcEnabled: boolean;
293
+ /**
294
+ * Tracks if sweep phase is enabled for this document. This is specified during document creation and doesn't change
295
+ * throughout its lifetime.
296
+ */
297
+ private readonly sweepEnabled: boolean;
298
+
299
+ /**
300
+ * Tracks if GC should run or not. Even if GC is enabled for a document (see gcEnabled), it can be explicitly
301
+ * disabled via runtime options or feature flags.
302
+ */
303
+ public readonly shouldRunGC: boolean;
304
+ /**
305
+ * Tracks if sweep phase should run or not. Even if the sweep phase is enabled for a document (see sweepEnabled), it
306
+ * can be explicitly disabled via feature flags. It also won't run if session expiry is not enabled.
307
+ */
308
308
  private readonly shouldRunSweep: boolean;
309
+
309
310
  private readonly testMode: boolean;
310
311
  private readonly mc: MonitoringContext;
311
312
 
@@ -377,26 +378,47 @@ export class GarbageCollector implements IGarbageCollector {
377
378
 
378
379
  let prevSummaryGCVersion: number | undefined;
379
380
 
380
- // GC can only be enabled during creation. After that, it can never be enabled again. So, for existing
381
- // documents, we get this information from the metadata blob. Similarly the session timeout should be
382
- // consistent across all clients, thus we grab it as well from the metadata blob, and set it once on creation.
381
+ /**
382
+ * The following GC state is enabled during container creation and cannot be changed throughout its lifetime:
383
+ * 1. Whether running GC mark phase is allowed or not.
384
+ * 2. Whether running GC sweep phase is allowed or not.
385
+ * 3. Whether GC session expiry is enabled or not.
386
+ * For existing containers, we get this information from the metadata blob of its summary.
387
+ */
383
388
  if (existing) {
384
389
  prevSummaryGCVersion = getGCVersion(metadata);
385
390
  // Existing documents which did not have metadata blob or had GC disabled have version as 0. For all
386
391
  // other existing documents, GC is enabled.
387
392
  this.gcEnabled = prevSummaryGCVersion > 0;
393
+ this.sweepEnabled = metadata?.sweepEnabled ?? false;
388
394
  this.sessionExpiryTimeoutMs = metadata?.sessionExpiryTimeoutMs;
389
395
  } else {
390
- // For new documents, GC has to be explicitly enabled via the gcAllowed flag in GC options.
396
+ // Sweep should not be enabled without enabling GC mark phase. We could silently disable sweep in this
397
+ // scenario but explicitly failing makes it clearer and promotes correct usage.
398
+ if (gcOptions.sweepAllowed && !gcOptions.gcAllowed) {
399
+ throw new UsageError("GC sweep phase cannot be enabled without enabling GC mark phase");
400
+ }
401
+
402
+ // For new documents, GC has to be explicitly enabled via the flags in GC options.
391
403
  this.gcEnabled = gcOptions.gcAllowed === true;
404
+ this.sweepEnabled = gcOptions.sweepAllowed === true;
405
+
392
406
  // Set the Session Expiry only if the flag is enabled or the test option is set.
393
- if (this.mc.config.getBoolean(runSessionExpiry) && this.gcEnabled) {
407
+ if (this.mc.config.getBoolean(runSessionExpiryKey) && this.gcEnabled) {
394
408
  this.sessionExpiryTimeoutMs = defaultSessionExpiryDurationMs;
395
409
  }
396
410
  }
397
411
 
398
412
  // If session expiry is enabled, we need to close the container when the timeout expires
399
- if (this.sessionExpiryTimeoutMs !== undefined) {
413
+ if (this.sessionExpiryTimeoutMs !== undefined
414
+ && this.mc.config.getBoolean(disableSessionExpiryKey) !== true) {
415
+ // If Test Override config is set, override Session Expiry timeout
416
+ const overrideSessionExpiryTimeoutMs =
417
+ this.mc.config.getNumber("Fluid.GarbageCollection.TestOverride.SessionExpiryMs");
418
+ if (overrideSessionExpiryTimeoutMs !== undefined) {
419
+ this.sessionExpiryTimeoutMs = overrideSessionExpiryTimeoutMs;
420
+ }
421
+
400
422
  const timeoutMs = this.sessionExpiryTimeoutMs;
401
423
  setLongTimeout(timeoutMs,
402
424
  () => {
@@ -411,7 +433,12 @@ export class GarbageCollector implements IGarbageCollector {
411
433
  // latest tracked GC version. For new documents, we will be writing the first summary with the current version.
412
434
  this.latestSummaryGCVersion = prevSummaryGCVersion ?? this.currentGCVersion;
413
435
 
414
- // Whether GC should run or not. Can override with localStorage flag.
436
+ /**
437
+ * Whether GC should run or not. The following conditions have to be met to run sweep:
438
+ * 1. GC should be enabled for this container.
439
+ * 2. GC should not be disabled via disableGC GC option.
440
+ * These conditions can be overridden via runGCKey feature flag.
441
+ */
415
442
  this.shouldRunGC = this.mc.config.getBoolean(runGCKey) ?? (
416
443
  // GC must be enabled for the document.
417
444
  this.gcEnabled
@@ -419,11 +446,15 @@ export class GarbageCollector implements IGarbageCollector {
419
446
  && !gcOptions.disableGC
420
447
  );
421
448
 
422
- // Whether GC sweep phase should run or not. If this is false, only GC mark phase is run. Can override with
423
- // localStorage flag.
424
- this.shouldRunSweep = this.shouldRunGC &&
425
- (this.mc.config.getBoolean(runSweepKey) ?? gcOptions.runSweep === true)
426
- && this.sessionExpiryTimer !== undefined;
449
+ /**
450
+ * Whether sweep should run or not. The following conditions have to be met to run sweep:
451
+ * 1. Overall GC or mark phase must be enabled (this.shouldRunGC).
452
+ * 2. Session expiry and sweep should be enabled for this container. Without session expiry we cannot safely
453
+ * delete unreferenced objects. This condition (#2) can be overridden via runSweepKey feature flag.
454
+ */
455
+ this.shouldRunSweep = this.shouldRunGC && (
456
+ this.mc.config.getBoolean(runSweepKey) ?? (this.sessionExpiryTimeoutMs !== undefined && this.sweepEnabled)
457
+ );
427
458
 
428
459
  // Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
429
460
  this.testMode = this.mc.config.getBoolean(gcTestModeKey) ?? gcOptions.runGCInTestMode === true;
@@ -514,7 +545,7 @@ export class GarbageCollector implements IGarbageCollector {
514
545
  return;
515
546
  }
516
547
 
517
- const gcNodes: { [ id: string ]: string[] } = {};
548
+ const gcNodes: { [ id: string ]: string[]; } = {};
518
549
  for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
519
550
  if (nodeData.unreferencedTimestampMs !== undefined) {
520
551
  this.unreferencedNodesState.set(
@@ -539,7 +570,7 @@ export class GarbageCollector implements IGarbageCollector {
539
570
  return new Map();
540
571
  }
541
572
 
542
- const gcNodes: { [ id: string ]: string[] } = {};
573
+ const gcNodes: { [ id: string ]: string[]; } = {};
543
574
  for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
544
575
  gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
545
576
  }
@@ -548,7 +579,7 @@ export class GarbageCollector implements IGarbageCollector {
548
579
  // each node in the summary.
549
580
  const usedRoutes = runGarbageCollection(
550
581
  gcNodes,
551
- [ "/" ],
582
+ ["/"],
552
583
  this.mc.logger,
553
584
  ).referencedNodeIds;
554
585
 
@@ -593,11 +624,11 @@ export class GarbageCollector implements IGarbageCollector {
593
624
  public async collectGarbage(
594
625
  options: {
595
626
  /** Logger to use for logging GC events */
596
- logger?: ITelemetryLogger,
627
+ logger?: ITelemetryLogger;
597
628
  /** True to run GC sweep phase after the mark phase */
598
- runSweep?: boolean,
629
+ runSweep?: boolean;
599
630
  /** True to generate full GC data */
600
- fullGC?: boolean,
631
+ fullGC?: boolean;
601
632
  },
602
633
  ): Promise<IGCStats> {
603
634
  const {
@@ -616,14 +647,14 @@ export class GarbageCollector implements IGarbageCollector {
616
647
  const gcData = await this.runtime.getGCData(fullGC);
617
648
  const gcResult = runGarbageCollection(
618
649
  gcData.gcNodes,
619
- [ "/" ],
650
+ ["/"],
620
651
  logger,
621
652
  );
622
- const gcStats = this.generateStatsAndLogEvents(gcResult);
653
+ const gcStats = this.generateStatsAndLogEvents(gcResult, logger);
623
654
 
624
655
  // Update the state since the last GC run. There can be nodes that were referenced between the last and
625
656
  // the current run. We need to identify than and update their unreferenced state if needed.
626
- this.updateStateSinceLastRun(gcData);
657
+ this.updateStateSinceLastRun(gcData, logger);
627
658
 
628
659
  // Update the current state of the system based on the GC run.
629
660
  const currentReferenceTimestampMs = this.runtime.getCurrentReferenceTimestampMs();
@@ -669,6 +700,18 @@ export class GarbageCollector implements IGarbageCollector {
669
700
  return builder.getSummaryTree();
670
701
  }
671
702
 
703
+ public getMetadata(): IGCMetadata {
704
+ return {
705
+ /**
706
+ * If GC is enabled, the GC data is written using the current GC version and that is the gcFeature that goes
707
+ * into the metadata blob. If GC is disabled, the gcFeature is 0.
708
+ */
709
+ gcFeature: this.gcEnabled ? this.currentGCVersion : 0,
710
+ sessionExpiryTimeoutMs: this.sessionExpiryTimeoutMs,
711
+ sweepEnabled: this.sweepEnabled,
712
+ };
713
+ }
714
+
672
715
  /**
673
716
  * Returns a map of node ids to their base GC details generated from the base summary. This is used by the caller
674
717
  * to initialize the GC state of the nodes.
@@ -840,7 +883,7 @@ export class GarbageCollector implements IGarbageCollector {
840
883
  * This function identifies nodes that were referenced since last run and removes their unreferenced state, if any.
841
884
  * If these nodes are currently unreferenced, they will be assigned new unreferenced state by the current run.
842
885
  */
843
- private updateStateSinceLastRun(currentGCData: IGarbageCollectionData) {
886
+ private updateStateSinceLastRun(currentGCData: IGarbageCollectionData, logger: ITelemetryLogger) {
844
887
  // If we haven't run GC before there is nothing to do.
845
888
  if (this.previousGCDataFromLastRun === undefined) {
846
889
  return;
@@ -855,7 +898,7 @@ export class GarbageCollector implements IGarbageCollector {
855
898
 
856
899
  // The following log will be enabled once this issue is resolved:
857
900
  // https://github.com/microsoft/FluidFramework/issues/8878.
858
- if(this.mc.config.getBoolean(logUnknownOutboundReferencesKey) === true
901
+ if (this.mc.config.getBoolean(logUnknownOutboundReferencesKey) === true
859
902
  && missingExplicitReferences.length > 0) {
860
903
  missingExplicitReferences.forEach((missingExplicitReference) => {
861
904
  const event: ITelemetryPerformanceEvent = {
@@ -863,7 +906,7 @@ export class GarbageCollector implements IGarbageCollector {
863
906
  gcNodeId: missingExplicitReference[0],
864
907
  gcRoutes: JSON.stringify(missingExplicitReference[1]),
865
908
  };
866
- this.mc.logger.sendPerformanceEvent(event);
909
+ logger.sendPerformanceEvent(event);
867
910
  });
868
911
  }
869
912
 
@@ -902,7 +945,7 @@ export class GarbageCollector implements IGarbageCollector {
902
945
  * unreferenced, stop tracking them and remove from unreferenced list.
903
946
  * Some of these nodes may be unreferenced now and if so, the current run will add unreferenced state for them.
904
947
  */
905
- const gcResult = runGarbageCollection(gcDataSuperSet.gcNodes, ["/"], this.mc.logger);
948
+ const gcResult = runGarbageCollection(gcDataSuperSet.gcNodes, ["/"], logger);
906
949
  for (const nodeId of gcResult.referencedNodeIds) {
907
950
  const nodeStateTracker = this.unreferencedNodesState.get(nodeId);
908
951
  if (nodeStateTracker !== undefined) {
@@ -973,13 +1016,13 @@ export class GarbageCollector implements IGarbageCollector {
973
1016
  * @param gcResult - The result of a GC run.
974
1017
  * @returns the GC stats of the GC run.
975
1018
  */
976
- private generateStatsAndLogEvents(gcResult: IGCResult): IGCStats {
1019
+ private generateStatsAndLogEvents(gcResult: IGCResult, logger: ITelemetryLogger): IGCStats {
977
1020
  // Log pending events for unreferenced nodes after GC has run. We should have the package data available for
978
1021
  // them now since the GC run should have loaded these nodes.
979
1022
  let event = this.pendingEventsQueue.shift();
980
1023
  while (event !== undefined) {
981
1024
  const pkg = this.getNodePackagePath(event.id);
982
- this.mc.logger.sendErrorEvent({
1025
+ logger.sendErrorEvent({
983
1026
  ...event,
984
1027
  pkg: pkg ? { value: `/${pkg.join("/")}`, tag: TelemetryDataTag.PackageData } : undefined,
985
1028
  });
@@ -417,8 +417,7 @@ export class OrderedClientElection
417
417
  // Note that we allow a summarizer client to supercede an interactive client as elected client.
418
418
  if (this._electedClient === undefined || (!electedClientIsSummarizer && newClientIsSummarizer)) {
419
419
  this.tryElectingClient(client, sequenceNumber);
420
- }
421
- else if (this._electedParent === undefined && !newClientIsSummarizer) {
420
+ } else if (this._electedParent === undefined && !newClientIsSummarizer) {
422
421
  // This is an odd case. If the _electedClient is set, the _electedParent should be as well.
423
422
  this.tryElectingParent(client, sequenceNumber);
424
423
  }
@@ -443,16 +442,14 @@ export class OrderedClientElection
443
442
  throw new UsageError("Elected client should be a summarizer client 1");
444
443
  }
445
444
  this.tryElectingClient(this._electedParent, sequenceNumber);
446
- }
447
- else {
445
+ } else {
448
446
  // 2. The _electedClient is an interactive client that has left the quorum.
449
447
  // Automatically shift to next oldest client.
450
448
  const nextClient = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
451
449
  this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
452
450
  this.tryElectingClient(nextClient, sequenceNumber);
453
451
  }
454
- }
455
- else if (this._electedParent === client) {
452
+ } else if (this._electedParent === client) {
456
453
  // Removing the _electedParent (but not _electedClient).
457
454
  // Shift to the next oldest parent, but do not replace the _electedClient,
458
455
  // which is a summarizer that is still doing work.
@@ -478,8 +475,7 @@ export class OrderedClientElection
478
475
  this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
479
476
  if (this._electedClient === undefined || this._electedClient === this._electedParent) {
480
477
  this.tryElectingClient(nextClient, sequenceNumber);
481
- }
482
- else {
478
+ } else {
483
479
  // The _electedClient is a summarizer and should not be replaced until it leaves the quorum.
484
480
  // Changing the _electedParent will stop the summarizer.
485
481
  this.tryElectingParent(nextClient, sequenceNumber);
@@ -493,8 +489,7 @@ export class OrderedClientElection
493
489
  const firstClient = this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
494
490
  if (this._electedClient === undefined || this._electedClient === this._electedParent) {
495
491
  this.tryElectingClient(firstClient, sequenceNumber);
496
- }
497
- else {
492
+ } else {
498
493
  // The _electedClient is a summarizer and should not be replaced until it leaves the quorum.
499
494
  // Changing the _electedParent will stop the summarizer.
500
495
  this.tryElectingParent(firstClient, sequenceNumber);
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "0.59.2001";
9
+ export const pkgVersion = "0.59.3000";
@@ -114,7 +114,7 @@ export class PendingStateManager implements IDisposable {
114
114
  pendingStates: this.pendingStates.toArray().map(
115
115
  // delete localOpMetadata since it may not be serializable
116
116
  // and will be regenerated by applyStashedOp()
117
- (state) => state.type === "message" ? {...state, localOpMetadata: undefined } : state),
117
+ (state) => state.type === "message" ? { ...state, localOpMetadata: undefined } : state),
118
118
  };
119
119
  }
120
120
  }
@@ -288,7 +288,7 @@ export class PendingStateManager implements IDisposable {
288
288
  assert(message.type === state.messageType, 0x28c /* "different message type" */);
289
289
  assert(message.clientSequenceNumber === state.clientSequenceNumber || !isOriginalClientId,
290
290
  0x28d /* "client sequence number doesn't match" */);
291
- switch(message.type) {
291
+ switch (message.type) {
292
292
  case ContainerMessageType.Attach:
293
293
  assert(message.contents.id === state.content.id, 0x28e /* "datastore ID doesn't match" */);
294
294
  break;
@@ -261,9 +261,10 @@ export class RunningSummarizer implements IDisposable {
261
261
  this.summaryCollection.unsetPendingAckTimerTimeoutCallback();
262
262
 
263
263
  if (waitStartResult.result === "done" && waitStartResult.value !== undefined) {
264
- this.heuristicData.initialize({
264
+ this.heuristicData.updateWithLastSummaryAckInfo({
265
265
  refSequenceNumber: waitStartResult.value.summaryOp.referenceSequenceNumber,
266
- summaryTime: waitStartResult.value.summaryOp.timestamp,
266
+ // This will be the Summarizer starting point so only use timestamps from client's machine.
267
+ summaryTime: Date.now(),
267
268
  summarySequenceNumber: waitStartResult.value.summaryOp.sequenceNumber,
268
269
  });
269
270
  }
@@ -277,7 +278,7 @@ export class RunningSummarizer implements IDisposable {
277
278
  * @returns - result of action.
278
279
  */
279
280
  private async lockedSummaryAction<T>(action: () => Promise<T>) {
280
- assert (this.summarizingLock === undefined, 0x25b /* "Caller is responsible for checking lock" */);
281
+ assert(this.summarizingLock === undefined, 0x25b /* "Caller is responsible for checking lock" */);
281
282
 
282
283
  const summarizingLock = new Deferred<void>();
283
284
  this.summarizingLock = summarizingLock.promise;
@@ -311,8 +312,7 @@ export class RunningSummarizer implements IDisposable {
311
312
  summarizeProps: ISummarizeTelemetryProperties,
312
313
  options: ISummarizeOptions,
313
314
  cancellationToken = this.cancellationToken,
314
- resultsBuilder = new SummarizeResultBuilder()): ISummarizeResults
315
- {
315
+ resultsBuilder = new SummarizeResultBuilder()): ISummarizeResults {
316
316
  this.lockedSummaryAction(async () => {
317
317
  const summarizeResult = this.generator.summarize(
318
318
  summarizeProps,
@@ -334,8 +334,7 @@ export class RunningSummarizer implements IDisposable {
334
334
  /** Heuristics summarize attempt. */
335
335
  private trySummarize(
336
336
  reason: SummarizeReason,
337
- cancellationToken = this.cancellationToken): void
338
- {
337
+ cancellationToken = this.cancellationToken): void {
339
338
  if (this.summarizingLock !== undefined) {
340
339
  // lockedSummaryAction() will retry heuristic-based summary at the end of current attempt
341
340
  // if it's still needed
@@ -344,7 +343,7 @@ export class RunningSummarizer implements IDisposable {
344
343
  }
345
344
 
346
345
  this.lockedSummaryAction(async () => {
347
- const attempts: (ISummarizeOptions & { delaySeconds?: number })[] = [
346
+ const attempts: (ISummarizeOptions & { delaySeconds?: number; })[] = [
348
347
  { refreshLatestAck: false, fullTree: false },
349
348
  { refreshLatestAck: true, fullTree: false },
350
349
  { refreshLatestAck: true, fullTree: false, delaySeconds: 2 * 60 },
@@ -379,7 +378,7 @@ export class RunningSummarizer implements IDisposable {
379
378
  this.logger.sendPerformanceEvent({
380
379
  eventName: "SummarizeAttemptDelay",
381
380
  duration: delaySeconds,
382
- reason: overrideDelaySeconds !== undefined ? "nack with retryAfter" : undefined,
381
+ summaryNackDelay: overrideDelaySeconds !== undefined,
383
382
  ...summarizeProps,
384
383
  });
385
384
  await delay(delaySeconds * 1000);
package/src/summarizer.ts CHANGED
@@ -242,6 +242,7 @@ export class Summarizer extends EventEmitter implements ISummarizer {
242
242
  eventName: "RunningSummarizer",
243
243
  onBehalfOf,
244
244
  initSummarySeqNumber: this.runtime.deltaManager.initialSequenceNumber,
245
+ config: JSON.stringify(this.configurationGetter()),
245
246
  });
246
247
 
247
248
  // Summarizing container ID (with clientType === "summarizer")
@@ -359,8 +360,7 @@ export class Summarizer extends EventEmitter implements ISummarizer {
359
360
  });
360
361
 
361
362
  return builder.build();
362
- }
363
- catch (error) {
363
+ } catch (error) {
364
364
  throw SummarizingWarning.wrap(error, false /* logged */, this.logger);
365
365
  }
366
366
  };
@@ -29,7 +29,7 @@ export class SummarizeHeuristicData implements ISummarizeHeuristicData {
29
29
  this._lastSuccessfulSummary = { ...attemptBaseline };
30
30
  }
31
31
 
32
- public initialize(lastSummary: Readonly<ISummarizeAttempt>) {
32
+ public updateWithLastSummaryAckInfo(lastSummary: Readonly<ISummarizeAttempt>) {
33
33
  this._lastAttempt = lastSummary;
34
34
  this._lastSuccessfulSummary = { ...lastSummary };
35
35
  }