@fluidframework/container-runtime 2.0.0-internal.4.3.0 → 2.0.0-internal.4.4.0

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 (103) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/blobManager.d.ts.map +1 -1
  3. package/dist/blobManager.js +3 -2
  4. package/dist/blobManager.js.map +1 -1
  5. package/dist/containerRuntime.d.ts.map +1 -1
  6. package/dist/containerRuntime.js +13 -7
  7. package/dist/containerRuntime.js.map +1 -1
  8. package/dist/dataStoreContext.d.ts.map +1 -1
  9. package/dist/dataStoreContext.js +1 -2
  10. package/dist/dataStoreContext.js.map +1 -1
  11. package/dist/gc/garbageCollection.d.ts +55 -43
  12. package/dist/gc/garbageCollection.d.ts.map +1 -1
  13. package/dist/gc/garbageCollection.js +219 -203
  14. package/dist/gc/garbageCollection.js.map +1 -1
  15. package/dist/gc/gcConfigs.d.ts.map +1 -1
  16. package/dist/gc/gcConfigs.js +8 -10
  17. package/dist/gc/gcConfigs.js.map +1 -1
  18. package/dist/gc/gcDefinitions.d.ts +2 -0
  19. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  20. package/dist/gc/gcDefinitions.js.map +1 -1
  21. package/dist/gc/gcHelpers.d.ts +11 -1
  22. package/dist/gc/gcHelpers.d.ts.map +1 -1
  23. package/dist/gc/gcHelpers.js +18 -3
  24. package/dist/gc/gcHelpers.js.map +1 -1
  25. package/dist/gc/gcSummaryStateTracker.d.ts +6 -2
  26. package/dist/gc/gcSummaryStateTracker.d.ts.map +1 -1
  27. package/dist/gc/gcSummaryStateTracker.js +16 -6
  28. package/dist/gc/gcSummaryStateTracker.js.map +1 -1
  29. package/dist/gc/gcTelemetry.d.ts +2 -2
  30. package/dist/gc/gcTelemetry.d.ts.map +1 -1
  31. package/dist/gc/gcTelemetry.js +42 -22
  32. package/dist/gc/gcTelemetry.js.map +1 -1
  33. package/dist/gc/index.d.ts +1 -2
  34. package/dist/gc/index.d.ts.map +1 -1
  35. package/dist/gc/index.js +2 -5
  36. package/dist/gc/index.js.map +1 -1
  37. package/dist/packageVersion.d.ts +1 -1
  38. package/dist/packageVersion.js +1 -1
  39. package/dist/packageVersion.js.map +1 -1
  40. package/dist/scheduleManager.js +15 -4
  41. package/dist/scheduleManager.js.map +1 -1
  42. package/lib/blobManager.d.ts.map +1 -1
  43. package/lib/blobManager.js +3 -2
  44. package/lib/blobManager.js.map +1 -1
  45. package/lib/containerRuntime.d.ts.map +1 -1
  46. package/lib/containerRuntime.js +13 -7
  47. package/lib/containerRuntime.js.map +1 -1
  48. package/lib/dataStoreContext.d.ts.map +1 -1
  49. package/lib/dataStoreContext.js +1 -2
  50. package/lib/dataStoreContext.js.map +1 -1
  51. package/lib/gc/garbageCollection.d.ts +55 -43
  52. package/lib/gc/garbageCollection.d.ts.map +1 -1
  53. package/lib/gc/garbageCollection.js +219 -203
  54. package/lib/gc/garbageCollection.js.map +1 -1
  55. package/lib/gc/gcConfigs.d.ts.map +1 -1
  56. package/lib/gc/gcConfigs.js +8 -10
  57. package/lib/gc/gcConfigs.js.map +1 -1
  58. package/lib/gc/gcDefinitions.d.ts +2 -0
  59. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  60. package/lib/gc/gcDefinitions.js.map +1 -1
  61. package/lib/gc/gcHelpers.d.ts +11 -1
  62. package/lib/gc/gcHelpers.d.ts.map +1 -1
  63. package/lib/gc/gcHelpers.js +16 -2
  64. package/lib/gc/gcHelpers.js.map +1 -1
  65. package/lib/gc/gcSummaryStateTracker.d.ts +6 -2
  66. package/lib/gc/gcSummaryStateTracker.d.ts.map +1 -1
  67. package/lib/gc/gcSummaryStateTracker.js +16 -6
  68. package/lib/gc/gcSummaryStateTracker.js.map +1 -1
  69. package/lib/gc/gcTelemetry.d.ts +2 -2
  70. package/lib/gc/gcTelemetry.d.ts.map +1 -1
  71. package/lib/gc/gcTelemetry.js +43 -23
  72. package/lib/gc/gcTelemetry.js.map +1 -1
  73. package/lib/gc/index.d.ts +1 -2
  74. package/lib/gc/index.d.ts.map +1 -1
  75. package/lib/gc/index.js +1 -2
  76. package/lib/gc/index.js.map +1 -1
  77. package/lib/packageVersion.d.ts +1 -1
  78. package/lib/packageVersion.js +1 -1
  79. package/lib/packageVersion.js.map +1 -1
  80. package/lib/scheduleManager.js +15 -4
  81. package/lib/scheduleManager.js.map +1 -1
  82. package/package.json +15 -16
  83. package/src/blobManager.ts +3 -2
  84. package/src/containerRuntime.ts +10 -4
  85. package/src/dataStoreContext.ts +1 -2
  86. package/src/gc/garbageCollection.ts +274 -257
  87. package/src/gc/gcConfigs.ts +12 -11
  88. package/src/gc/gcDefinitions.ts +2 -0
  89. package/src/gc/gcHelpers.ts +20 -2
  90. package/src/gc/gcSummaryStateTracker.ts +19 -7
  91. package/src/gc/gcTelemetry.ts +52 -37
  92. package/src/gc/index.ts +1 -5
  93. package/src/packageVersion.ts +1 -1
  94. package/src/scheduleManager.ts +19 -7
  95. package/dist/gc/gcSweepReadyUsageDetection.d.ts +0 -53
  96. package/dist/gc/gcSweepReadyUsageDetection.d.ts.map +0 -1
  97. package/dist/gc/gcSweepReadyUsageDetection.js +0 -130
  98. package/dist/gc/gcSweepReadyUsageDetection.js.map +0 -1
  99. package/lib/gc/gcSweepReadyUsageDetection.d.ts +0 -53
  100. package/lib/gc/gcSweepReadyUsageDetection.d.ts.map +0 -1
  101. package/lib/gc/gcSweepReadyUsageDetection.js +0 -125
  102. package/lib/gc/gcSweepReadyUsageDetection.js.map +0 -1
  103. package/src/gc/gcSweepReadyUsageDetection.ts +0 -145
@@ -204,6 +204,8 @@ export interface IGarbageCollector {
204
204
  readonly shouldRunGC: boolean;
205
205
  /** Tells whether the GC state in summary needs to be reset in the next summary. */
206
206
  readonly summaryStateNeedsReset: boolean;
207
+ /** The count of data stores whose GC state updated since the last summary. */
208
+ readonly updatedDSCountSinceLastSummary: number;
207
209
  /** Initialize the state from the base snapshot after its creation. */
208
210
  initializeBaseState(): Promise<void>;
209
211
  /** Run garbage collection and update the reference / used state of the system. */
@@ -12,6 +12,7 @@ import {
12
12
  IGarbageCollectionData,
13
13
  IGarbageCollectionDetailsBase,
14
14
  } from "@fluidframework/runtime-definitions";
15
+ import { TelemetryDataTag } from "@fluidframework/telemetry-utils";
15
16
  import { GCFeatureMatrix, GCVersion, IGCMetadata } from "./gcDefinitions";
16
17
  import {
17
18
  IGarbageCollectionNodeData,
@@ -151,12 +152,19 @@ export function concatGarbageCollectionStates(
151
152
  /**
152
153
  * Helper function that clones the GC data.
153
154
  * @param gcData - The GC data to clone.
155
+ * @param filter - Optional function to filter out node ids not to be included in the cloned GC data. Returns
156
+ * true to filter out nodes.
154
157
  * @returns a clone of the given GC data.
155
158
  */
156
- export function cloneGCData(gcData: IGarbageCollectionData): IGarbageCollectionData {
159
+ export function cloneGCData(
160
+ gcData: IGarbageCollectionData,
161
+ filter?: (id: string) => boolean,
162
+ ): IGarbageCollectionData {
157
163
  const clonedGCNodes: { [id: string]: string[] } = {};
158
164
  for (const [id, outboundRoutes] of Object.entries(gcData.gcNodes)) {
159
- clonedGCNodes[id] = Array.from(outboundRoutes);
165
+ if (filter?.(id) !== true) {
166
+ clonedGCNodes[id] = Array.from(outboundRoutes);
167
+ }
160
168
  }
161
169
  return {
162
170
  gcNodes: clonedGCNodes,
@@ -296,3 +304,13 @@ export function unpackChildNodesGCDetails(gcDetails: IGarbageCollectionDetailsBa
296
304
  export function trimLeadingAndTrailingSlashes(str: string) {
297
305
  return str.replace(/^\/+|\/+$/g, "");
298
306
  }
307
+
308
+ /**
309
+ * Tags the passed value as a CodeArtifact and returns the tagged value.
310
+ */
311
+ export function tagAsCodeArtifact(value: string) {
312
+ return {
313
+ value,
314
+ tag: TelemetryDataTag.CodeArtifact,
315
+ };
316
+ }
@@ -14,7 +14,7 @@ import {
14
14
  } from "@fluidframework/runtime-definitions";
15
15
  import { mergeStats, ReadAndParseBlob, SummaryTreeBuilder } from "@fluidframework/runtime-utils";
16
16
  import { IContainerRuntimeMetadata, metadataBlobName, RefreshSummaryResult } from "../summary";
17
- import { GCVersion } from "./gcDefinitions";
17
+ import { GCVersion, IGCStats } from "./gcDefinitions";
18
18
  import { getGCDataFromSnapshot, generateSortedGCState, getGCVersion } from "./gcHelpers";
19
19
  import { IGarbageCollectionSnapshotData, IGarbageCollectionState } from "./gcSummaryDefinitions";
20
20
  import { IGarbageCollectorConfigs } from ".";
@@ -37,8 +37,6 @@ export interface IGCSummaryTrackingData {
37
37
  * On summarize, it decides whether to write new state or re-use previous summary's state.
38
38
  */
39
39
  export class GCSummaryStateTracker {
40
- // The current version of GC running.
41
- public readonly currentGCVersion: GCVersion = this.configs.gcVersionInEffect;
42
40
  // This is the version of GC data in the latest summary being tracked.
43
41
  private latestSummaryGCVersion: GCVersion;
44
42
 
@@ -50,6 +48,10 @@ export class GCSummaryStateTracker {
50
48
  // Tracks whether there was GC was run in latest summary being tracked.
51
49
  private wasGCRunInLatestSummary: boolean;
52
50
 
51
+ // Tracks the count of data stores whose state updated since the last summary, i.e., they went from referenced
52
+ // to unreferenced or vice-versa.
53
+ public updatedDSCountSinceLastSummary: number = 0;
54
+
53
55
  constructor(
54
56
  // Tells whether GC should run or not.
55
57
  private readonly configs: Pick<
@@ -62,7 +64,8 @@ export class GCSummaryStateTracker {
62
64
  this.wasGCRunInLatestSummary = wasGCRunInBaseSnapshot;
63
65
  // For existing document, the latest summary is the one that we loaded from. So, use its GC version as the
64
66
  // latest tracked GC version. For new documents, we will be writing the first summary with the current version.
65
- this.latestSummaryGCVersion = this.configs.gcVersionInBaseSnapshot ?? this.currentGCVersion;
67
+ this.latestSummaryGCVersion =
68
+ this.configs.gcVersionInBaseSnapshot ?? this.configs.gcVersionInEffect;
66
69
  }
67
70
 
68
71
  /**
@@ -101,7 +104,8 @@ export class GCSummaryStateTracker {
101
104
  public get doesSummaryStateNeedReset(): boolean {
102
105
  return (
103
106
  this.doesGCStateNeedReset ||
104
- (this.configs.shouldRunGC && this.latestSummaryGCVersion !== this.currentGCVersion)
107
+ (this.configs.shouldRunGC &&
108
+ this.latestSummaryGCVersion !== this.configs.gcVersionInEffect)
105
109
  );
106
110
  }
107
111
 
@@ -283,9 +287,10 @@ export class GCSummaryStateTracker {
283
287
  // If the summary was tracked by this client, it was the one that generated the summary in the first place.
284
288
  // Update latest state from pending.
285
289
  if (result.wasSummaryTracked) {
286
- this.latestSummaryGCVersion = this.currentGCVersion;
290
+ this.latestSummaryGCVersion = this.configs.gcVersionInEffect;
287
291
  this.latestSummaryData = this.pendingSummaryData;
288
292
  this.pendingSummaryData = undefined;
293
+ this.updatedDSCountSinceLastSummary = 0;
289
294
  return undefined;
290
295
  }
291
296
 
@@ -311,7 +316,7 @@ export class GCSummaryStateTracker {
311
316
  // in the snapshot cannot be interpreted correctly. Set everything to undefined except for deletedNodes
312
317
  // because irrespective of GC versions, these nodes have been deleted and cannot be brought back. The
313
318
  // deletedNodes info is needed to identify when these nodes are used.
314
- if (getGCVersion(metadata) !== this.currentGCVersion) {
319
+ if (getGCVersion(metadata) !== this.configs.gcVersionInEffect) {
315
320
  snapshotData = {
316
321
  gcState: undefined,
317
322
  tombstones: undefined,
@@ -326,4 +331,11 @@ export class GCSummaryStateTracker {
326
331
  };
327
332
  return snapshotData;
328
333
  }
334
+
335
+ /**
336
+ * Called to update the state from a GC run's stats. Used to update the count of data stores whose state updated.
337
+ */
338
+ public updateStateFromGCRunStats(stats: IGCStats) {
339
+ this.updatedDSCountSinceLastSummary += stats.updatedDataStoreCount;
340
+ }
329
341
  }
@@ -6,11 +6,7 @@
6
6
  import { ITelemetryGenericEvent, ITelemetryLogger } from "@fluidframework/common-definitions";
7
7
  import { IGarbageCollectionData } from "@fluidframework/runtime-definitions";
8
8
  import { packagePathToTelemetryProperty } from "@fluidframework/runtime-utils";
9
- import {
10
- generateStack,
11
- MonitoringContext,
12
- TelemetryDataTag,
13
- } from "@fluidframework/telemetry-utils";
9
+ import { generateStack, MonitoringContext } from "@fluidframework/telemetry-utils";
14
10
  import { ICreateContainerMetadata } from "../summary";
15
11
  import {
16
12
  disableSweepLogKey,
@@ -23,6 +19,7 @@ import {
23
19
  runSweepKey,
24
20
  } from "./gcDefinitions";
25
21
  import { UnreferencedStateTracker } from "./gcUnreferencedStateTracker";
22
+ import { tagAsCodeArtifact } from "./gcHelpers";
26
23
 
27
24
  type NodeUsageType = "Changed" | "Loaded" | "Revived";
28
25
 
@@ -32,25 +29,32 @@ interface ICommonProps {
32
29
  completedGCRuns: number;
33
30
  isTombstoned: boolean;
34
31
  lastSummaryTime?: number;
35
- fromId?: string;
36
32
  viaHandle?: boolean;
37
33
  }
38
34
 
39
35
  /** The event that is logged when unreferenced node is used after a certain time. */
40
36
  interface IUnreferencedEventProps extends ICreateContainerMetadata, ICommonProps {
41
37
  state: UnreferencedState;
42
- id: string;
38
+ id: {
39
+ value: string;
40
+ tag: string;
41
+ };
43
42
  type: GCNodeType;
44
43
  unrefTime: number;
45
44
  age: number;
46
45
  timeout?: number;
46
+ fromId?: {
47
+ value: string;
48
+ tag: string;
49
+ };
47
50
  }
48
51
 
49
52
  /** Properties passed to nodeUsed function when a node is used. */
50
53
  interface INodeUsageProps extends ICommonProps {
51
- nodeId: string;
54
+ id: string;
52
55
  currentReferenceTimestampMs: number | undefined;
53
56
  packagePath: readonly string[] | undefined;
57
+ fromId?: string;
54
58
  }
55
59
 
56
60
  /**
@@ -125,18 +129,18 @@ export class GCTelemetryTracker {
125
129
  // If there is no reference timestamp to work with, no ops have been processed after creation. If so, skip
126
130
  // logging as nothing interesting would have happened worth logging.
127
131
  // If the node is not unreferenced, skip logging.
128
- const nodeStateTracker = this.getNodeStateTracker(nodeUsageProps.nodeId);
132
+ const nodeStateTracker = this.getNodeStateTracker(nodeUsageProps.id);
129
133
  if (!nodeStateTracker || nodeUsageProps.currentReferenceTimestampMs === undefined) {
130
134
  return;
131
135
  }
132
136
 
133
137
  // We log these events once per event per node. A unique id is generated by joining node state (inactive / sweep ready),
134
138
  // node's id and usage (loaded / changed / revived).
135
- const uniqueEventId = `${nodeStateTracker.state}-${nodeUsageProps.nodeId}-${nodeUsageProps.usageType}`;
136
- const nodeType = this.getNodeType(nodeUsageProps.nodeId);
139
+ const uniqueEventId = `${nodeStateTracker.state}-${nodeUsageProps.id}-${nodeUsageProps.usageType}`;
140
+ const nodeType = this.getNodeType(nodeUsageProps.id);
137
141
  if (
138
142
  !this.shouldLogNonActiveEvent(
139
- nodeUsageProps.nodeId,
143
+ nodeUsageProps.id,
140
144
  nodeType,
141
145
  nodeUsageProps.usageType,
142
146
  nodeStateTracker,
@@ -150,10 +154,10 @@ export class GCTelemetryTracker {
150
154
  this.loggedUnreferencedEvents.add(uniqueEventId);
151
155
 
152
156
  const state = nodeStateTracker.state;
153
- const { usageType, currentReferenceTimestampMs, packagePath, ...propsToLog } =
157
+ const { usageType, currentReferenceTimestampMs, packagePath, id, fromId, ...propsToLog } =
154
158
  nodeUsageProps;
155
159
  const eventProps: Omit<IUnreferencedEventProps, "state" | "usageType"> = {
156
- id: nodeUsageProps.nodeId,
160
+ id: tagAsCodeArtifact(id),
157
161
  type: nodeType,
158
162
  unrefTime: nodeStateTracker.unreferencedTimestampMs,
159
163
  age:
@@ -163,6 +167,7 @@ export class GCTelemetryTracker {
163
167
  state === UnreferencedState.Inactive
164
168
  ? this.configs.inactiveTimeoutMs
165
169
  : this.configs.sweepTimeoutMs,
170
+ fromId: fromId ? tagAsCodeArtifact(fromId) : undefined,
166
171
  ...propsToLog,
167
172
  ...this.createContainerMetadata,
168
173
  };
@@ -175,7 +180,7 @@ export class GCTelemetryTracker {
175
180
  {
176
181
  eventName: `GC_Tombstone_${nodeType}_Revived`,
177
182
  category: "generic",
178
- url: nodeUsageProps.nodeId,
183
+ url: tagAsCodeArtifact(id),
179
184
  gcTombstoneEnforcementAllowed: this.gcTombstoneEnforcementAllowed,
180
185
  },
181
186
  undefined /* packagePath */,
@@ -199,11 +204,16 @@ export class GCTelemetryTracker {
199
204
  // Events generated:
200
205
  // InactiveObject_Loaded, SweepReadyObject_Loaded
201
206
  if (nodeUsageProps.usageType === "Loaded") {
207
+ const { id: taggedId, fromId: taggedFromId, ...otherProps } = eventProps;
202
208
  const event = {
203
209
  eventName: `${state}Object_${nodeUsageProps.usageType}`,
204
210
  pkg: packagePathToTelemetryProperty(nodeUsageProps.packagePath),
205
211
  stack: generateStack(),
206
- ...eventProps,
212
+ id: taggedId,
213
+ fromId: taggedFromId,
214
+ details: JSON.stringify({
215
+ ...otherProps,
216
+ }),
207
217
  };
208
218
 
209
219
  // Do not log the inactive object x events as error events as they are not the best signal for
@@ -263,8 +273,8 @@ export class GCTelemetryTracker {
263
273
  if (missingExplicitRoutes.length > 0) {
264
274
  logger.sendErrorEvent({
265
275
  eventName: "gcUnknownOutboundReferences",
266
- gcNodeId: nodeId,
267
- gcRoutes: JSON.stringify(missingExplicitRoutes),
276
+ id: tagAsCodeArtifact(nodeId),
277
+ routes: tagAsCodeArtifact(JSON.stringify(missingExplicitRoutes)),
268
278
  });
269
279
  }
270
280
  }
@@ -281,31 +291,31 @@ export class GCTelemetryTracker {
281
291
  // InactiveObject_Loaded, InactiveObject_Changed, InactiveObject_Revived
282
292
  // SweepReadyObject_Loaded, SweepReadyObject_Changed, SweepReadyObject_Revived
283
293
  for (const eventProps of this.pendingEventsQueue) {
284
- const { usageType, state, ...propsToLog } = eventProps;
294
+ const { usageType, state, id, fromId, ...propsToLog } = eventProps;
285
295
  /**
286
296
  * Revived event is logged only if the node is active. If the node is not active, the reference to it was
287
297
  * from another unreferenced node and this scenario is not interesting to log.
288
298
  * Loaded and Changed events are logged only if the node is not active. If the node is active, it was
289
299
  * revived and a Revived event will be logged for it.
290
300
  */
291
- const nodeStateTracker = this.getNodeStateTracker(eventProps.id);
301
+ const nodeStateTracker = this.getNodeStateTracker(eventProps.id.value);
292
302
  const active =
293
303
  nodeStateTracker === undefined ||
294
304
  nodeStateTracker.state === UnreferencedState.Active;
295
305
  if ((usageType === "Revived") === active) {
296
- const pkg = await this.getNodePackagePath(eventProps.id);
306
+ const pkg = await this.getNodePackagePath(eventProps.id.value);
297
307
  const fromPkg = eventProps.fromId
298
- ? await this.getNodePackagePath(eventProps.fromId)
308
+ ? await this.getNodePackagePath(eventProps.fromId.value)
299
309
  : undefined;
300
310
  const event = {
301
- ...propsToLog,
302
311
  eventName: `${state}Object_${usageType}`,
303
- pkg: pkg
304
- ? { value: pkg.join("/"), tag: TelemetryDataTag.CodeArtifact }
305
- : undefined,
306
- fromPkg: fromPkg
307
- ? { value: fromPkg.join("/"), tag: TelemetryDataTag.CodeArtifact }
308
- : undefined,
312
+ details: JSON.stringify({
313
+ ...propsToLog,
314
+ }),
315
+ id,
316
+ fromId,
317
+ pkg: pkg ? tagAsCodeArtifact(pkg.join("/")) : undefined,
318
+ fromPkg: fromPkg ? tagAsCodeArtifact(fromPkg.join("/")) : undefined,
309
319
  };
310
320
 
311
321
  if (state === UnreferencedState.Inactive) {
@@ -336,6 +346,7 @@ export class GCTelemetryTracker {
336
346
  return;
337
347
  }
338
348
 
349
+ const deletedNodeIds: string[] = [];
339
350
  for (const [nodeId, nodeStateTracker] of unreferencedNodesState) {
340
351
  if (nodeStateTracker.state !== UnreferencedState.SweepReady) {
341
352
  return;
@@ -352,15 +363,19 @@ export class GCTelemetryTracker {
352
363
  return;
353
364
  }
354
365
  this.loggedUnreferencedEvents.add(uniqueEventId);
366
+ deletedNodeIds.push(nodeId);
367
+ }
368
+
369
+ if (deletedNodeIds.length > 0) {
355
370
  logger.sendTelemetryEvent({
356
- eventName: "GCObjectDeleted",
357
- id: nodeId,
358
- type: nodeType,
359
- age: currentReferenceTimestampMs - nodeStateTracker.unreferencedTimestampMs,
360
- timeout: this.configs.sweepTimeoutMs,
361
- completedGCRuns,
362
- lastSummaryTime,
363
- ...this.createContainerMetadata,
371
+ eventName: "GC_SweepReadyObjects_Delete",
372
+ details: JSON.stringify({
373
+ timeout: this.configs.sweepTimeoutMs,
374
+ completedGCRuns,
375
+ lastSummaryTime,
376
+ ...this.createContainerMetadata,
377
+ }),
378
+ id: tagAsCodeArtifact(JSON.stringify(deletedNodeIds)),
364
379
  });
365
380
  }
366
381
  }
package/src/gc/index.ts CHANGED
@@ -43,6 +43,7 @@ export {
43
43
  shouldAllowGcSweep,
44
44
  trimLeadingAndTrailingSlashes,
45
45
  unpackChildNodesGCDetails,
46
+ tagAsCodeArtifact,
46
47
  } from "./gcHelpers";
47
48
  export { runGarbageCollection } from "./gcReferenceGraphAlgorithm";
48
49
  export {
@@ -56,10 +57,5 @@ export {
56
57
  GCSummaryStateTracker,
57
58
  IGCSummaryTrackingData,
58
59
  } from "./gcSummaryStateTracker";
59
- export {
60
- skipClosureForXDaysKey,
61
- closuresMapLocalStorageKey,
62
- SweepReadyUsageDetectionHandler,
63
- } from "./gcSweepReadyUsageDetection";
64
60
  export { GCTelemetryTracker, sendGCUnexpectedUsageEvent } from "./gcTelemetry";
65
61
  export { UnreferencedStateTracker } from "./gcUnreferencedStateTracker";
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.0.0-internal.4.3.0";
9
+ export const pkgVersion = "2.0.0-internal.4.4.0";
@@ -147,14 +147,14 @@ class ScheduleManagerCore {
147
147
  // We are intentionally directly listening to the "op" to inspect system ops as well.
148
148
  // If we do not observe system ops, we are likely to hit 0x296 assert when system ops
149
149
  // precedes start of incomplete batch.
150
- this.deltaManager.on("op", (message) => this.afterOpProcessing(message.sequenceNumber));
150
+ this.deltaManager.on("op", (message) => this.afterOpProcessing(message));
151
151
  }
152
152
 
153
153
  /**
154
154
  * The only public function in this class - called when we processed an op,
155
155
  * to make decision if op processing should be paused or not after that.
156
156
  */
157
- public afterOpProcessing(sequenceNumber: number) {
157
+ public afterOpProcessing(message: ISequencedDocumentMessage) {
158
158
  assert(
159
159
  !this.localPaused,
160
160
  0x294 /* "can't have op processing paused if we are processing an op" */,
@@ -177,12 +177,24 @@ class ScheduleManagerCore {
177
177
 
178
178
  // do we have incomplete batch to worry about?
179
179
  if (this.pauseSequenceNumber !== undefined) {
180
- assert(
181
- sequenceNumber < this.pauseSequenceNumber,
182
- 0x296 /* "we should never start processing incomplete batch!" */,
183
- );
180
+ if (message.sequenceNumber >= this.pauseSequenceNumber) {
181
+ throw DataProcessingError.create(
182
+ // Former assert 0x296
183
+ "Incomplete batch",
184
+ "ScheduleManager",
185
+ message,
186
+ {
187
+ type: message.type,
188
+ contentType: typeof message.contents,
189
+ batch: message.metadata?.batch,
190
+ compression: message.compression,
191
+ pauseSeqNum: this.pauseSequenceNumber,
192
+ },
193
+ );
194
+ }
195
+
184
196
  // If the next op is the start of incomplete batch, then we can't process it until it's fully in - pause!
185
- if (sequenceNumber + 1 === this.pauseSequenceNumber) {
197
+ if (message.sequenceNumber + 1 === this.pauseSequenceNumber) {
186
198
  this.pauseQueue();
187
199
  }
188
200
  }
@@ -1,53 +0,0 @@
1
- /*!
2
- * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
- * Licensed under the MIT License.
4
- */
5
- import { ITelemetryProperties } from "@fluidframework/common-definitions";
6
- import { ICriticalContainerError } from "@fluidframework/container-definitions";
7
- import { IFluidErrorBase, LoggingError, MonitoringContext } from "@fluidframework/telemetry-utils";
8
- /**
9
- * Feature Gate Key -
10
- * How many days between closing the container from this error (avoids locking user out of their file altogether)
11
- */
12
- export declare const skipClosureForXDaysKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.SkipClosureForXDays";
13
- /**
14
- * LocalStorage key (NOT via feature gate / monitoring context)
15
- * A map from docId to info about the last time we closed due to this error
16
- */
17
- export declare const closuresMapLocalStorageKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.Closures";
18
- /**
19
- * Error class raised when a SweepReady object is used, indicating a bug in how
20
- * references are managed in the container by the application, or a bug in how
21
- * GC tracks those references.
22
- *
23
- * There's a chance for false positives when this error is raised by an Interactive Container,
24
- * since only the Summarizer has the latest truth about unreferenced node tracking
25
- */
26
- export declare class SweepReadyUsageError extends LoggingError implements IFluidErrorBase {
27
- /** This errorType will be in temporary use (until Sweep is fully implemented) so don't add to any errorType type */
28
- errorType: string;
29
- }
30
- /**
31
- * This class encapsulates the logic around what to do when a SweepReady object is used.
32
- * There are several tactics we plan to use in Dogfood environments to aid diagnosis of these cases:
33
- * - Closing the interactive container when either the interactive or summarizer client detects this kind of violation
34
- * (via sweepReadyUsageDetectionSetting above)
35
- * - Throttling the frequency of these crashes via a "Skip Closure Period" per container per device
36
- * (via skipClosureForXDaysKey above. Uses localStorage and closuresMapLocalStorageKey to implement this behavior)
37
- */
38
- export declare class SweepReadyUsageDetectionHandler {
39
- private readonly uniqueContainerKey;
40
- private readonly mc;
41
- private readonly closeFn;
42
- private readonly localStorage;
43
- constructor(uniqueContainerKey: string, mc: MonitoringContext, closeFn: (error?: ICriticalContainerError) => void, localStorageOverride?: Pick<Storage, "getItem" | "setItem">);
44
- /**
45
- * If SweepReady Usage Detection is enabled, close the interactive container.
46
- * If the SkipClosureForXDays setting is set, don't close the container more than once in that period.
47
- *
48
- * Once Sweep is fully implemented, this will be removed since the objects will be gone
49
- * and errors will arise elsewhere in the runtime
50
- */
51
- usageDetectedInInteractiveClient(errorProps: ITelemetryProperties): void;
52
- }
53
- //# sourceMappingURL=gcSweepReadyUsageDetection.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gcSweepReadyUsageDetection.d.ts","sourceRoot":"","sources":["../../src/gc/gcSweepReadyUsageDetection.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAEN,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,MAAM,iCAAiC,CAAC;AAGzC;;;GAGG;AACH,eAAO,MAAM,sBAAsB,iFAC4C,CAAC;AAEhF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,sEAC6B,CAAC;AAqBrE;;;;;;;GAOG;AACH,qBAAa,oBAAqB,SAAQ,YAAa,YAAW,eAAe;IAChF,oHAAoH;IAC7G,SAAS,EAAE,MAAM,CAAiD;CACzE;AAED;;;;;;;GAOG;AACH,qBAAa,+BAA+B;IAI1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IALzB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAuC;gBAGlD,kBAAkB,EAAE,MAAM,EAC1B,EAAE,EAAE,iBAAiB,EACrB,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,uBAAuB,KAAK,IAAI,EACnE,oBAAoB,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAc5D;;;;;;OAMG;IACI,gCAAgC,CAAC,UAAU,EAAE,oBAAoB;CA+CxE"}
@@ -1,130 +0,0 @@
1
- "use strict";
2
- /*!
3
- * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
4
- * Licensed under the MIT License.
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.SweepReadyUsageDetectionHandler = exports.SweepReadyUsageError = exports.closuresMapLocalStorageKey = exports.skipClosureForXDaysKey = void 0;
8
- const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
9
- const gcDefinitions_1 = require("./gcDefinitions");
10
- /**
11
- * Feature Gate Key -
12
- * How many days between closing the container from this error (avoids locking user out of their file altogether)
13
- */
14
- exports.skipClosureForXDaysKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.SkipClosureForXDays";
15
- /**
16
- * LocalStorage key (NOT via feature gate / monitoring context)
17
- * A map from docId to info about the last time we closed due to this error
18
- */
19
- exports.closuresMapLocalStorageKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.Closures";
20
- /**
21
- * Feature gate key to enable closing the container if SweepReady objects are used.
22
- * Value should contain keywords "interactiveClient" and/or "summarizer" to enable detection in each container type
23
- */
24
- const sweepReadyUsageDetectionSetting = {
25
- read(config) {
26
- const sweepReadyUsageDetectionKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection";
27
- const value = config.getString(sweepReadyUsageDetectionKey);
28
- if (value === undefined) {
29
- return { interactiveClient: false, summarizer: false };
30
- }
31
- return {
32
- interactiveClient: value.includes("interactiveClient"),
33
- summarizer: value.includes("summarizer"),
34
- };
35
- },
36
- };
37
- /**
38
- * Error class raised when a SweepReady object is used, indicating a bug in how
39
- * references are managed in the container by the application, or a bug in how
40
- * GC tracks those references.
41
- *
42
- * There's a chance for false positives when this error is raised by an Interactive Container,
43
- * since only the Summarizer has the latest truth about unreferenced node tracking
44
- */
45
- class SweepReadyUsageError extends telemetry_utils_1.LoggingError {
46
- constructor() {
47
- super(...arguments);
48
- /** This errorType will be in temporary use (until Sweep is fully implemented) so don't add to any errorType type */
49
- this.errorType = "unreferencedObjectUsedAfterGarbageCollected";
50
- }
51
- }
52
- exports.SweepReadyUsageError = SweepReadyUsageError;
53
- /**
54
- * This class encapsulates the logic around what to do when a SweepReady object is used.
55
- * There are several tactics we plan to use in Dogfood environments to aid diagnosis of these cases:
56
- * - Closing the interactive container when either the interactive or summarizer client detects this kind of violation
57
- * (via sweepReadyUsageDetectionSetting above)
58
- * - Throttling the frequency of these crashes via a "Skip Closure Period" per container per device
59
- * (via skipClosureForXDaysKey above. Uses localStorage and closuresMapLocalStorageKey to implement this behavior)
60
- */
61
- class SweepReadyUsageDetectionHandler {
62
- constructor(uniqueContainerKey, mc, closeFn, localStorageOverride) {
63
- var _a;
64
- this.uniqueContainerKey = uniqueContainerKey;
65
- this.mc = mc;
66
- this.closeFn = closeFn;
67
- const noopStorage = { getItem: () => null, setItem: () => { } };
68
- // localStorage is not defined in Node environment, so fall back to noopStorage if needed.
69
- this.localStorage = (_a = localStorageOverride !== null && localStorageOverride !== void 0 ? localStorageOverride : globalThis.localStorage) !== null && _a !== void 0 ? _a : noopStorage;
70
- if (this.localStorage === noopStorage) {
71
- // This means the Skip Closure Period logic will not work.
72
- this.mc.logger.sendTelemetryEvent({
73
- eventName: "SweepReadyUsageDetectionHandlerNoopStorage",
74
- });
75
- }
76
- }
77
- /**
78
- * If SweepReady Usage Detection is enabled, close the interactive container.
79
- * If the SkipClosureForXDays setting is set, don't close the container more than once in that period.
80
- *
81
- * Once Sweep is fully implemented, this will be removed since the objects will be gone
82
- * and errors will arise elsewhere in the runtime
83
- */
84
- usageDetectedInInteractiveClient(errorProps) {
85
- var _a;
86
- if (!sweepReadyUsageDetectionSetting.read(this.mc.config).interactiveClient) {
87
- return;
88
- }
89
- // Default stance is we close every time - this reflects the severity of SweepReady Object Usage.
90
- // However, we may choose to "throttle" the closures by setting the SkipClosureForXDays setting,
91
- // which will only allow the container to close once during that period, to avoid locking users out.
92
- let shouldClose = true;
93
- let pastClosuresMap = {};
94
- let lastCloseTime;
95
- const skipClosureForXDays = this.mc.config.getNumber(exports.skipClosureForXDaysKey);
96
- if (skipClosureForXDays !== undefined) {
97
- // Read pastClosuresMap from localStorage then extract the lastCloseTime from the map
98
- try {
99
- const rawValue = this.localStorage.getItem(exports.closuresMapLocalStorageKey);
100
- const parsedValue = rawValue === null ? {} : JSON.parse(rawValue);
101
- if (typeof parsedValue === "object") {
102
- pastClosuresMap = parsedValue;
103
- }
104
- }
105
- catch (e) { }
106
- lastCloseTime = (_a = pastClosuresMap[this.uniqueContainerKey]) === null || _a === void 0 ? void 0 : _a.lastCloseTime;
107
- // Don't close if we did already within the Skip Closure Period
108
- if (lastCloseTime !== undefined &&
109
- Date.now() < lastCloseTime + skipClosureForXDays * gcDefinitions_1.oneDayMs) {
110
- shouldClose = false;
111
- }
112
- }
113
- const error = new SweepReadyUsageError("SweepReady object used in Non-Summarizer Client", {
114
- errorDetails: JSON.stringify(Object.assign(Object.assign({}, errorProps), { lastCloseTime, skipClosureForXDays })),
115
- });
116
- if (shouldClose) {
117
- // Update closures map in localStorage before closing
118
- // Note there is a race condition between different tabs updating localStorage and overwriting
119
- // each others' updates. If so, some tab will crash again. Just reload one at a time to get unstuck
120
- pastClosuresMap[this.uniqueContainerKey] = { lastCloseTime: Date.now() };
121
- this.localStorage.setItem(exports.closuresMapLocalStorageKey, JSON.stringify(pastClosuresMap));
122
- this.closeFn(error);
123
- }
124
- else {
125
- this.mc.logger.sendErrorEvent({ eventName: "SweepReadyObject_UsageAllowed" }, error);
126
- }
127
- }
128
- }
129
- exports.SweepReadyUsageDetectionHandler = SweepReadyUsageDetectionHandler;
130
- //# sourceMappingURL=gcSweepReadyUsageDetection.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gcSweepReadyUsageDetection.js","sourceRoot":"","sources":["../../src/gc/gcSweepReadyUsageDetection.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { ITelemetryProperties } from \"@fluidframework/common-definitions\";\nimport { ICriticalContainerError } from \"@fluidframework/container-definitions\";\nimport {\n\tIConfigProvider,\n\tIFluidErrorBase,\n\tLoggingError,\n\tMonitoringContext,\n} from \"@fluidframework/telemetry-utils\";\nimport { oneDayMs } from \"./gcDefinitions\";\n\n/**\n * Feature Gate Key -\n * How many days between closing the container from this error (avoids locking user out of their file altogether)\n */\nexport const skipClosureForXDaysKey =\n\t\"Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.SkipClosureForXDays\";\n\n/**\n * LocalStorage key (NOT via feature gate / monitoring context)\n * A map from docId to info about the last time we closed due to this error\n */\nexport const closuresMapLocalStorageKey =\n\t\"Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.Closures\";\n\n/**\n * Feature gate key to enable closing the container if SweepReady objects are used.\n * Value should contain keywords \"interactiveClient\" and/or \"summarizer\" to enable detection in each container type\n */\nconst sweepReadyUsageDetectionSetting = {\n\tread(config: IConfigProvider) {\n\t\tconst sweepReadyUsageDetectionKey =\n\t\t\t\"Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection\";\n\t\tconst value = config.getString(sweepReadyUsageDetectionKey);\n\t\tif (value === undefined) {\n\t\t\treturn { interactiveClient: false, summarizer: false };\n\t\t}\n\t\treturn {\n\t\t\tinteractiveClient: value.includes(\"interactiveClient\"),\n\t\t\tsummarizer: value.includes(\"summarizer\"),\n\t\t};\n\t},\n};\n\n/**\n * Error class raised when a SweepReady object is used, indicating a bug in how\n * references are managed in the container by the application, or a bug in how\n * GC tracks those references.\n *\n * There's a chance for false positives when this error is raised by an Interactive Container,\n * since only the Summarizer has the latest truth about unreferenced node tracking\n */\nexport class SweepReadyUsageError extends LoggingError implements IFluidErrorBase {\n\t/** This errorType will be in temporary use (until Sweep is fully implemented) so don't add to any errorType type */\n\tpublic errorType: string = \"unreferencedObjectUsedAfterGarbageCollected\";\n}\n\n/**\n * This class encapsulates the logic around what to do when a SweepReady object is used.\n * There are several tactics we plan to use in Dogfood environments to aid diagnosis of these cases:\n * - Closing the interactive container when either the interactive or summarizer client detects this kind of violation\n * (via sweepReadyUsageDetectionSetting above)\n * - Throttling the frequency of these crashes via a \"Skip Closure Period\" per container per device\n * (via skipClosureForXDaysKey above. Uses localStorage and closuresMapLocalStorageKey to implement this behavior)\n */\nexport class SweepReadyUsageDetectionHandler {\n\tprivate readonly localStorage: Pick<Storage, \"getItem\" | \"setItem\">;\n\n\tconstructor(\n\t\tprivate readonly uniqueContainerKey: string,\n\t\tprivate readonly mc: MonitoringContext,\n\t\tprivate readonly closeFn: (error?: ICriticalContainerError) => void,\n\t\tlocalStorageOverride?: Pick<Storage, \"getItem\" | \"setItem\">,\n\t) {\n\t\tconst noopStorage = { getItem: () => null, setItem: () => {} };\n\t\t// localStorage is not defined in Node environment, so fall back to noopStorage if needed.\n\t\tthis.localStorage = localStorageOverride ?? globalThis.localStorage ?? noopStorage;\n\n\t\tif (this.localStorage === noopStorage) {\n\t\t\t// This means the Skip Closure Period logic will not work.\n\t\t\tthis.mc.logger.sendTelemetryEvent({\n\t\t\t\teventName: \"SweepReadyUsageDetectionHandlerNoopStorage\",\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * If SweepReady Usage Detection is enabled, close the interactive container.\n\t * If the SkipClosureForXDays setting is set, don't close the container more than once in that period.\n\t *\n\t * Once Sweep is fully implemented, this will be removed since the objects will be gone\n\t * and errors will arise elsewhere in the runtime\n\t */\n\tpublic usageDetectedInInteractiveClient(errorProps: ITelemetryProperties) {\n\t\tif (!sweepReadyUsageDetectionSetting.read(this.mc.config).interactiveClient) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Default stance is we close every time - this reflects the severity of SweepReady Object Usage.\n\t\t// However, we may choose to \"throttle\" the closures by setting the SkipClosureForXDays setting,\n\t\t// which will only allow the container to close once during that period, to avoid locking users out.\n\t\tlet shouldClose: boolean = true;\n\t\tlet pastClosuresMap: Record<string, { lastCloseTime: number } | undefined> = {};\n\t\tlet lastCloseTime: number | undefined;\n\t\tconst skipClosureForXDays = this.mc.config.getNumber(skipClosureForXDaysKey);\n\t\tif (skipClosureForXDays !== undefined) {\n\t\t\t// Read pastClosuresMap from localStorage then extract the lastCloseTime from the map\n\t\t\ttry {\n\t\t\t\tconst rawValue = this.localStorage.getItem(closuresMapLocalStorageKey);\n\t\t\t\tconst parsedValue = rawValue === null ? {} : JSON.parse(rawValue);\n\t\t\t\tif (typeof parsedValue === \"object\") {\n\t\t\t\t\tpastClosuresMap = parsedValue;\n\t\t\t\t}\n\t\t\t} catch (e) {}\n\t\t\tlastCloseTime = pastClosuresMap[this.uniqueContainerKey]?.lastCloseTime;\n\n\t\t\t// Don't close if we did already within the Skip Closure Period\n\t\t\tif (\n\t\t\t\tlastCloseTime !== undefined &&\n\t\t\t\tDate.now() < lastCloseTime + skipClosureForXDays * oneDayMs\n\t\t\t) {\n\t\t\t\tshouldClose = false;\n\t\t\t}\n\t\t}\n\n\t\tconst error = new SweepReadyUsageError(\"SweepReady object used in Non-Summarizer Client\", {\n\t\t\terrorDetails: JSON.stringify({ ...errorProps, lastCloseTime, skipClosureForXDays }),\n\t\t});\n\t\tif (shouldClose) {\n\t\t\t// Update closures map in localStorage before closing\n\t\t\t// Note there is a race condition between different tabs updating localStorage and overwriting\n\t\t\t// each others' updates. If so, some tab will crash again. Just reload one at a time to get unstuck\n\t\t\tpastClosuresMap[this.uniqueContainerKey] = { lastCloseTime: Date.now() };\n\t\t\tthis.localStorage.setItem(closuresMapLocalStorageKey, JSON.stringify(pastClosuresMap));\n\n\t\t\tthis.closeFn(error);\n\t\t} else {\n\t\t\tthis.mc.logger.sendErrorEvent({ eventName: \"SweepReadyObject_UsageAllowed\" }, error);\n\t\t}\n\t}\n}\n"]}