@fluidframework/runtime-utils 2.0.0-internal.2.2.0 → 2.0.0-internal.2.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/runtime-utils",
3
- "version": "2.0.0-internal.2.2.0",
3
+ "version": "2.0.0-internal.2.3.0",
4
4
  "description": "Collection of utility functions for Fluid Runtime",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -64,23 +64,23 @@
64
64
  "dependencies": {
65
65
  "@fluidframework/common-definitions": "^0.20.1",
66
66
  "@fluidframework/common-utils": "^1.0.0",
67
- "@fluidframework/container-definitions": ">=2.0.0-internal.2.2.0 <2.0.0-internal.3.0.0",
68
- "@fluidframework/container-runtime-definitions": ">=2.0.0-internal.2.2.0 <2.0.0-internal.3.0.0",
69
- "@fluidframework/core-interfaces": ">=2.0.0-internal.2.2.0 <2.0.0-internal.3.0.0",
70
- "@fluidframework/datastore-definitions": ">=2.0.0-internal.2.2.0 <2.0.0-internal.3.0.0",
71
- "@fluidframework/garbage-collector": ">=2.0.0-internal.2.2.0 <2.0.0-internal.3.0.0",
67
+ "@fluidframework/container-definitions": ">=2.0.0-internal.2.3.0 <2.0.0-internal.3.0.0",
68
+ "@fluidframework/container-runtime-definitions": ">=2.0.0-internal.2.3.0 <2.0.0-internal.3.0.0",
69
+ "@fluidframework/core-interfaces": ">=2.0.0-internal.2.3.0 <2.0.0-internal.3.0.0",
70
+ "@fluidframework/datastore-definitions": ">=2.0.0-internal.2.3.0 <2.0.0-internal.3.0.0",
71
+ "@fluidframework/garbage-collector": ">=2.0.0-internal.2.3.0 <2.0.0-internal.3.0.0",
72
72
  "@fluidframework/protocol-base": "^0.1038.2000",
73
73
  "@fluidframework/protocol-definitions": "^1.1.0",
74
- "@fluidframework/runtime-definitions": ">=2.0.0-internal.2.2.0 <2.0.0-internal.3.0.0",
75
- "@fluidframework/telemetry-utils": ">=2.0.0-internal.2.2.0 <2.0.0-internal.3.0.0"
74
+ "@fluidframework/runtime-definitions": ">=2.0.0-internal.2.3.0 <2.0.0-internal.3.0.0",
75
+ "@fluidframework/telemetry-utils": ">=2.0.0-internal.2.3.0 <2.0.0-internal.3.0.0"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@fluid-tools/build-cli": "^0.7.0",
79
79
  "@fluidframework/build-common": "^1.1.0",
80
80
  "@fluidframework/build-tools": "^0.7.0",
81
- "@fluidframework/eslint-config-fluid": "^1.2.0",
82
- "@fluidframework/mocha-test-setup": ">=2.0.0-internal.2.2.0 <2.0.0-internal.3.0.0",
83
- "@fluidframework/runtime-utils-previous": "npm:@fluidframework/runtime-utils@2.0.0-internal.2.1.0",
81
+ "@fluidframework/eslint-config-fluid": "^2.0.0",
82
+ "@fluidframework/mocha-test-setup": ">=2.0.0-internal.2.3.0 <2.0.0-internal.3.0.0",
83
+ "@fluidframework/runtime-utils-previous": "npm:@fluidframework/runtime-utils@2.0.0-internal.2.2.0",
84
84
  "@microsoft/api-extractor": "^7.22.2",
85
85
  "@rushstack/eslint-config": "^2.5.1",
86
86
  "@types/mocha": "^9.1.1",
@@ -98,9 +98,9 @@
98
98
  "typescript": "~4.5.5"
99
99
  },
100
100
  "typeValidation": {
101
- "version": "2.0.0-internal.2.2.0",
102
- "baselineRange": ">=2.0.0-internal.2.1.0 <2.0.0-internal.2.2.0",
103
- "baselineVersion": "2.0.0-internal.2.1.0",
101
+ "version": "2.0.0-internal.2.3.0",
102
+ "baselineRange": ">=2.0.0-internal.2.2.0 <2.0.0-internal.2.3.0",
103
+ "baselineVersion": "2.0.0-internal.2.2.0",
104
104
  "broken": {}
105
105
  }
106
106
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/runtime-utils";
9
- export const pkgVersion = "2.0.0-internal.2.2.0";
9
+ export const pkgVersion = "2.0.0-internal.2.3.0";
@@ -384,7 +384,7 @@ export class SummarizerNode implements IRootSummarizerNode {
384
384
  return this._latestSummary;
385
385
  }
386
386
 
387
- private readonly canReuseHandle: boolean;
387
+ protected readonly canReuseHandle: boolean;
388
388
 
389
389
  /**
390
390
  * Do not call constructor directly.
@@ -5,11 +5,11 @@
5
5
 
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
7
  import { assert, LazyPromise } from "@fluidframework/common-utils";
8
- import { cloneGCData } from "@fluidframework/garbage-collector";
8
+ import { cloneGCData, getGCDataFromSnapshot, runGarbageCollection, unpackChildNodesGCDetails } from "@fluidframework/garbage-collector";
9
9
  import { ISnapshotTree } from "@fluidframework/protocol-definitions";
10
10
  import {
11
11
  CreateChildSummarizerNodeParam,
12
- gcBlobKey,
12
+ gcTreeKey,
13
13
  IGarbageCollectionData,
14
14
  IGarbageCollectionDetailsBase,
15
15
  ISummarizeInternalResult,
@@ -26,6 +26,7 @@ import {
26
26
  ICreateChildDetails,
27
27
  IInitialSummary,
28
28
  ISummarizerNodeRootContract,
29
+ parseSummaryForSubtrees,
29
30
  SummaryNode,
30
31
  } from "./summarizerNodeUtils";
31
32
 
@@ -127,22 +128,28 @@ export class SummarizerNodeWithGC extends SummarizerNode implements IRootSummari
127
128
  * - gcData: The garbage collection data of this node that is required for running GC.
128
129
  */
129
130
  private async loadBaseGCDetails() {
131
+ if (this.baseGCDetailsLoaded) {
132
+ return;
133
+ }
130
134
  const baseGCDetails = await this.baseGCDetailsP;
131
135
 
132
- // Possible race - If there were parallel calls to loadBaseGCDetails, we want to make sure that we only update
136
+ // Possible race - If there were parallel calls to loadBaseGCDetails, we want to make sure that we update
133
137
  // the state from the base details only once.
134
138
  if (this.baseGCDetailsLoaded) {
135
139
  return;
136
140
  }
137
141
  this.baseGCDetailsLoaded = true;
138
142
 
143
+ // Update GC data, used routes and reference used routes. The used routes are sorted because they are compared
144
+ // across GC runs to check if they changed. Sorting ensures that the elements are in the same order.
139
145
  // If the GC details has GC data, initialize our GC data from it.
140
146
  if (baseGCDetails.gcData !== undefined) {
141
147
  this.gcData = cloneGCData(baseGCDetails.gcData);
142
148
  }
143
- // Sort the used routes because we compare them with the current used routes to check if they changed between
144
- // summaries. Both are sorted so that the order of elements is the same.
145
- this.referenceUsedRoutes = baseGCDetails.usedRoutes?.sort();
149
+ if (baseGCDetails.usedRoutes !== undefined) {
150
+ this.usedRoutes = Array.from(baseGCDetails.usedRoutes).sort();
151
+ this.referenceUsedRoutes = Array.from(baseGCDetails.usedRoutes).sort()
152
+ }
146
153
  }
147
154
 
148
155
  public async summarize(
@@ -177,10 +184,11 @@ export class SummarizerNodeWithGC extends SummarizerNode implements IRootSummari
177
184
  // called and the node's data has not changed since last summary, the GC data in initial details is returned.
178
185
  await this.loadBaseGCDetails();
179
186
 
180
- // If there is no new data since last summary and we have GC data from the previous run, return it. We may not
181
- // have data from previous GC run for clients with older summary format before GC was added. They won't have
182
- // GC details in their initial summary.
183
- if (!fullGC && !this.hasDataChanged() && this.gcData !== undefined) {
187
+ // If there is no new data since last summary and we have GC data from the previous run, return it. The previous
188
+ // GC data may not be available if loaded from a snapshot with either GC disabled or before GC was added.
189
+ // Note - canReuseHandle is checked to be consistent with summarize - generate GC data for nodes for which
190
+ // summary must be generated.
191
+ if (this.canReuseHandle && !fullGC && !this.hasDataChanged() && this.gcData !== undefined) {
184
192
  return cloneGCData(this.gcData);
185
193
  }
186
194
 
@@ -259,8 +267,7 @@ export class SummarizerNodeWithGC extends SummarizerNode implements IRootSummari
259
267
  }
260
268
 
261
269
  /**
262
- * Called when we need to upload the reference state from the given summary. Read the GC blob and get the state
263
- * to upload from it.
270
+ * Called when we need to upload the reference state from the given summary.
264
271
  */
265
272
  protected async refreshLatestSummaryFromSnapshot(
266
273
  referenceSequenceNumber: number,
@@ -270,21 +277,7 @@ export class SummarizerNodeWithGC extends SummarizerNode implements IRootSummari
270
277
  correlatedSummaryLogger: ITelemetryLogger,
271
278
  readAndParseBlob: ReadAndParseBlob,
272
279
  ): Promise<void> {
273
- // If GC is disabled, skip setting referenced used routes since we are not tracking GC state.
274
- if (!this.gcDisabled) {
275
- const gcDetailsBlob = snapshotTree.blobs[gcBlobKey];
276
- if (gcDetailsBlob !== undefined) {
277
- const gcDetails = await readAndParseBlob<IGarbageCollectionDetailsBase>(gcDetailsBlob);
278
-
279
- // Possible re-entrancy. If we have already seen a summary later than this one, ignore it.
280
- if (this.referenceSequenceNumber >= referenceSequenceNumber) {
281
- return;
282
- }
283
-
284
- this.referenceUsedRoutes = gcDetails.usedRoutes;
285
- }
286
- }
287
-
280
+ await this.refreshGCStateFromSnapshot(referenceSequenceNumber, snapshotTree, readAndParseBlob);
288
281
  return super.refreshLatestSummaryFromSnapshot(
289
282
  referenceSequenceNumber,
290
283
  snapshotTree,
@@ -295,6 +288,85 @@ export class SummarizerNodeWithGC extends SummarizerNode implements IRootSummari
295
288
  );
296
289
  }
297
290
 
291
+ /**
292
+ * Updates GC state from the given snapshot if GC is enabled and the snapshot is newer than the one this node
293
+ * is tracking.
294
+ */
295
+ private async refreshGCStateFromSnapshot(
296
+ referenceSequenceNumber: number,
297
+ snapshotTree: ISnapshotTree,
298
+ readAndParseBlob: ReadAndParseBlob,
299
+ ): Promise<void> {
300
+ // If GC is disabled or we have seen a newer summary, skip updating GC state.
301
+ if (this.gcDisabled || this.referenceSequenceNumber >= referenceSequenceNumber) {
302
+ return;
303
+ }
304
+
305
+ // Load the base GC details before proceeding because if that happens later it can overwrite the GC details
306
+ // written by the following code.
307
+ await this.loadBaseGCDetails();
308
+
309
+ // Possible re-entrancy. We may already have processed this while loading base GC details.
310
+ if (this.referenceSequenceNumber >= referenceSequenceNumber) {
311
+ return;
312
+ }
313
+
314
+ /**
315
+ * GC data is written at root of the snapshot tree under "gc" sub-tree. This data needs to be propagated to
316
+ * all the nodes in the container.
317
+ * The root summarizer node reads the GC data from the "gc" sub-tree, runs GC on it to get used routes in
318
+ * the container and updates its GC data and referenced used routes. It then gets the GC data and used
319
+ * routes of all its children and adds it to their snapshot tree.
320
+ * All the other nodes gets the GC data and used routes from their snapshot tree and updates their state.
321
+ * They get the GC data and used routes of their children and add it to their snapshot tree and so on.
322
+ *
323
+ * Note that if the snapshot does not have GC tree, GC data will be set to undefined and used routes will be
324
+ * set to self-route (meaning referenced) for all nodes. This is important because the GC data needs to be
325
+ * regenerated in the next summary.
326
+ */
327
+ let gcDetails: IGarbageCollectionDetailsBase | undefined;
328
+ const gcSnapshotTree = snapshotTree.trees[gcTreeKey];
329
+ if (gcSnapshotTree !== undefined) {
330
+ // If there is a GC tree in the snapshot, this is the root summarizer node. Read GC data from the tree
331
+ // process it as explained above.
332
+ const gcSnapshotData = await getGCDataFromSnapshot(gcSnapshotTree, readAndParseBlob);
333
+
334
+ const gcNodes: { [id: string]: string[]; } = {};
335
+ for (const [nodeId, nodeData] of Object.entries(gcSnapshotData.gcState.gcNodes)) {
336
+ gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
337
+ }
338
+ // Run GC on the nodes in the snapshot to get the used routes for each node in the container.
339
+ const usedRoutes = runGarbageCollection(gcNodes, ["/"]).referencedNodeIds;
340
+ gcDetails = { gcData: { gcNodes }, usedRoutes };
341
+ } else {
342
+ // If there is a GC blob in the snapshot, it's a non-root summarizer nodes - The root summarizer node
343
+ // writes GC blob in the snapshot of child nodes. Get GC data and used routes from the blob.
344
+ const gcDetailsBlob = snapshotTree.blobs[gcTreeKey];
345
+ if (gcDetailsBlob !== undefined) {
346
+ gcDetails = JSON.parse(gcDetailsBlob) as IGarbageCollectionDetailsBase;
347
+ }
348
+ }
349
+
350
+ // Update this node to the same GC state it was when the ack corresponding to this summary was processed.
351
+ this.gcData = gcDetails?.gcData !== undefined ? cloneGCData(gcDetails.gcData) : undefined;
352
+ this.referenceUsedRoutes = gcDetails?.usedRoutes !== undefined ? Array.from(gcDetails.usedRoutes) : undefined;
353
+ // If there are no used routes in the GC details, set it to have self route which will make the node
354
+ // referenced. This scenario can only happen if the snapshot is from a client where GC was not run or
355
+ // disabled. In both the cases, the node should be referenced.
356
+ this.usedRoutes = gcDetails?.usedRoutes !== undefined ? Array.from(gcDetails.usedRoutes) : [""];
357
+
358
+ if (gcDetails === undefined) {
359
+ return;
360
+ }
361
+
362
+ // Generate the GC data and used routes of children GC nodes and add it to their snapshot tree.
363
+ const gcDetailsMap = unpackChildNodesGCDetails(gcDetails);
364
+ const { childrenTree } = parseSummaryForSubtrees(snapshotTree);
365
+ gcDetailsMap.forEach((childGCDetails: IGarbageCollectionDetailsBase, childId: string) => {
366
+ childrenTree.trees[childId].blobs[gcTreeKey] = JSON.stringify(childGCDetails);
367
+ });
368
+ }
369
+
298
370
  /**
299
371
  * Override the createChild method to return an instance of SummarizerNodeWithGC.
300
372
  */
@@ -315,6 +387,20 @@ export class SummarizerNodeWithGC extends SummarizerNode implements IRootSummari
315
387
  ): ISummarizerNodeWithGC {
316
388
  assert(!this.children.has(id), 0x1b6 /* "Create SummarizerNode child already exists" */);
317
389
 
390
+ /**
391
+ * Update the child node's base GC details from this node's current GC details instead of updating from the base
392
+ * GC details of this node. This will handle scenarios where the GC details was updated during refresh from
393
+ * snapshot and the child node wasn't created then. If a child is created after that, its GC details should be
394
+ * the one from the downloaded snapshot and not the base GC details.
395
+ */
396
+ const getChildBaseGCDetailsP = new LazyPromise<IGarbageCollectionDetailsBase>(async () => {
397
+ // Ensure that the base GC details is loaded because a child can be created before GC runs which is when
398
+ // base GC details is usually loaded.
399
+ await this.loadBaseGCDetails();
400
+ const childBaseGCDetails = unpackChildNodesGCDetails({ gcData: this.gcData, usedRoutes: this.usedRoutes });
401
+ return childBaseGCDetails.get(id) ?? {};
402
+ });
403
+
318
404
  const createDetails: ICreateChildDetails = this.getCreateDetailsForChild(id, createParam);
319
405
  const child = new SummarizerNodeWithGC(
320
406
  this.defaultLogger,
@@ -329,7 +415,7 @@ export class SummarizerNodeWithGC extends SummarizerNode implements IRootSummari
329
415
  createDetails.initialSummary,
330
416
  this.wipSummaryLogger,
331
417
  getGCDataFn,
332
- getBaseGCDetailsFn,
418
+ async () => getChildBaseGCDetailsP,
333
419
  );
334
420
 
335
421
  // There may be additional state that has to be updated in this child. For example, if a summary is being