@fluidframework/container-runtime 0.51.0-43124 → 0.51.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/containerRuntime.d.ts +31 -31
  2. package/dist/containerRuntime.d.ts.map +1 -1
  3. package/dist/containerRuntime.js +61 -144
  4. package/dist/containerRuntime.js.map +1 -1
  5. package/dist/dataStoreContext.js +1 -1
  6. package/dist/dataStoreContext.js.map +1 -1
  7. package/dist/dataStores.d.ts +3 -5
  8. package/dist/dataStores.d.ts.map +1 -1
  9. package/dist/dataStores.js +3 -4
  10. package/dist/dataStores.js.map +1 -1
  11. package/dist/garbageCollection.d.ts +116 -0
  12. package/dist/garbageCollection.d.ts.map +1 -0
  13. package/dist/garbageCollection.js +148 -0
  14. package/dist/garbageCollection.js.map +1 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/packageVersion.d.ts +1 -1
  19. package/dist/packageVersion.d.ts.map +1 -1
  20. package/dist/packageVersion.js +1 -1
  21. package/dist/packageVersion.js.map +1 -1
  22. package/dist/pendingStateManager.d.ts +0 -1
  23. package/dist/pendingStateManager.d.ts.map +1 -1
  24. package/dist/pendingStateManager.js +0 -36
  25. package/dist/pendingStateManager.js.map +1 -1
  26. package/lib/containerRuntime.d.ts +31 -31
  27. package/lib/containerRuntime.d.ts.map +1 -1
  28. package/lib/containerRuntime.js +62 -145
  29. package/lib/containerRuntime.js.map +1 -1
  30. package/lib/dataStoreContext.js +1 -1
  31. package/lib/dataStoreContext.js.map +1 -1
  32. package/lib/dataStores.d.ts +3 -5
  33. package/lib/dataStores.d.ts.map +1 -1
  34. package/lib/dataStores.js +3 -4
  35. package/lib/dataStores.js.map +1 -1
  36. package/lib/garbageCollection.d.ts +116 -0
  37. package/lib/garbageCollection.d.ts.map +1 -0
  38. package/lib/garbageCollection.js +144 -0
  39. package/lib/garbageCollection.js.map +1 -0
  40. package/lib/index.d.ts +1 -0
  41. package/lib/index.d.ts.map +1 -1
  42. package/lib/index.js.map +1 -1
  43. package/lib/packageVersion.d.ts +1 -1
  44. package/lib/packageVersion.d.ts.map +1 -1
  45. package/lib/packageVersion.js +1 -1
  46. package/lib/packageVersion.js.map +1 -1
  47. package/lib/pendingStateManager.d.ts +0 -1
  48. package/lib/pendingStateManager.d.ts.map +1 -1
  49. package/lib/pendingStateManager.js +0 -36
  50. package/lib/pendingStateManager.js.map +1 -1
  51. package/package.json +11 -11
  52. package/src/containerRuntime.ts +89 -188
  53. package/src/dataStoreContext.ts +1 -1
  54. package/src/dataStores.ts +5 -5
  55. package/src/garbageCollection.ts +269 -0
  56. package/src/index.ts +1 -0
  57. package/src/packageVersion.ts +1 -1
  58. package/src/pendingStateManager.ts +0 -43
@@ -0,0 +1,269 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
+ import { runGarbageCollection } from "@fluidframework/garbage-collector";
8
+ import { ISnapshotTree } from "@fluidframework/protocol-definitions";
9
+ import { IGarbageCollectionData } from "@fluidframework/runtime-definitions";
10
+ import { ReadAndParseBlob, RefreshSummaryResult } from "@fluidframework/runtime-utils";
11
+ import { ChildLogger, PerformanceEvent } from "@fluidframework/telemetry-utils";
12
+
13
+ import { IGCRuntimeOptions } from "./containerRuntime";
14
+ import { getLocalStorageFeatureGate } from "./localStorageFeatureGates";
15
+ import {
16
+ getGCVersion,
17
+ GCVersion,
18
+ IContainerRuntimeMetadata,
19
+ metadataBlobName,
20
+ } from "./summaryFormat";
21
+
22
+ /** This is the current version of garbage collection. */
23
+ const GCVersion = 1;
24
+
25
+ // Local storage key to turn GC on / off.
26
+ const runGCKey = "FluidRunGC";
27
+ // Local storage key to turn GC test mode on / off.
28
+ const gcTestModeKey = "FluidGCTestMode";
29
+ // Local storage key to turn GC sweep on / off.
30
+ const runSweepKey = "FluidRunSweep";
31
+
32
+ /** The used state statistics of a node. */
33
+ export interface IUsedStateStats {
34
+ totalNodeCount: number;
35
+ unusedNodeCount: number;
36
+ }
37
+
38
+ /** The statistics of the system state after a garbage collection run. */
39
+ export interface IGCStats {
40
+ totalNodes: number;
41
+ deletedNodes: number;
42
+ totalDataStores: number;
43
+ deletedDataStores: number;
44
+ }
45
+
46
+ /** Defines the APIs for the runtime object to be passed to the garbage collector. */
47
+ export interface IGarbageCollectionRuntime {
48
+ /** Returns the garbage collection data of the runtime. */
49
+ getGCData(fullGC?: boolean): Promise<IGarbageCollectionData>;
50
+ /** After GC has run, called to notify the runtime of routes that are used in it. */
51
+ updateUsedRoutes(usedRoutes: string[]): IUsedStateStats;
52
+ }
53
+
54
+ /** Defines the contract for the garbage collector. */
55
+ export interface IGarbageCollector {
56
+ /** Tells whether GC should run or not. */
57
+ readonly shouldRunGC: boolean;
58
+ /**
59
+ * This tracks two things:
60
+ * 1. Whether GC is enabled - If this is 0, GC is disabled. If this is > 0, GC is enabled.
61
+ * 2. If GC is enabled, the version of GC used to generate the GC data written in a summary.
62
+ */
63
+ readonly gcSummaryFeatureVersion: number;
64
+ /** Tells whether the GC version has changed compared to the version in the latest summary. */
65
+ readonly hasGCVersionChanged: boolean;
66
+ /** Run garbage collection and update the reference / used state of the system. */
67
+ collectGarbage(
68
+ options: { logger?: ITelemetryLogger, runGC?: boolean, runSweep?: boolean, fullGC?: boolean },
69
+ ): Promise<IGCStats>;
70
+ /** Called when the latest summary of the system has been refreshed. */
71
+ latestSummaryStateRefreshed(result: RefreshSummaryResult, readAndParseBlob: ReadAndParseBlob): Promise<void>;
72
+ }
73
+
74
+ /**
75
+ * The garbage collector for the container runtime. It consolidates the garbage collection functionality and maintains
76
+ * its state across summaries.
77
+ */
78
+ export class GarbageCollector implements IGarbageCollector {
79
+ public static create(
80
+ provider: IGarbageCollectionRuntime,
81
+ gcOptions: IGCRuntimeOptions,
82
+ deleteUnusedRoutes: (unusedRoutes: string[]) => void,
83
+ baseLogger: ITelemetryLogger,
84
+ existing: boolean,
85
+ metadata?: IContainerRuntimeMetadata,
86
+ ): IGarbageCollector {
87
+ return new GarbageCollector(provider, gcOptions, deleteUnusedRoutes, baseLogger, existing, metadata);
88
+ }
89
+
90
+ /**
91
+ * Tells whether GC should be run based on the GC options and local storage flags.
92
+ */
93
+ public readonly shouldRunGC: boolean;
94
+
95
+ /**
96
+ * This tracks two things:
97
+ * 1. Whether GC is enabled - If this is 0, GC is disabled. If this is > 0, GC is enabled.
98
+ * 2. If GC is enabled, the version of GC used to generate the GC data written in a summary.
99
+ */
100
+ public get gcSummaryFeatureVersion(): number {
101
+ return this.gcEnabled ? this.currentGCVersion : 0;
102
+ }
103
+
104
+ /**
105
+ * Tells whether the GC version has changed compared to the version in the latest summary.
106
+ */
107
+ public get hasGCVersionChanged(): boolean {
108
+ // The current version can differ from the latest summary version in two cases:
109
+ // 1. The summary this client loaded with has data from a different GC version.
110
+ // 2. This client's latest summary was updated from a snapshot that has a different GC version.
111
+ return this.shouldRunGC && this.latestSummaryGCVersion !== this.currentGCVersion;
112
+ }
113
+
114
+ /**
115
+ * Tracks if GC is enabled for this document. This is specified during document creation and doesn't change
116
+ * throughout its lifetime.
117
+ */
118
+ private readonly gcEnabled: boolean;
119
+ private readonly shouldRunSweep: boolean;
120
+ private readonly testMode: boolean;
121
+ private readonly logger: ITelemetryLogger;
122
+
123
+ // The current GC version that this container is running.
124
+ private readonly currentGCVersion = GCVersion;
125
+ // This is the version of GC data in the latest summary being tracked.
126
+ private latestSummaryGCVersion: GCVersion;
127
+
128
+ protected constructor(
129
+ private readonly provider: IGarbageCollectionRuntime,
130
+ private readonly gcOptions: IGCRuntimeOptions,
131
+ /**
132
+ * After GC has run, called to delete objects in the runtime whose routes are unused. This is not part of the
133
+ * provider because its specific to this garbage collector implementation and is not part of the contract.
134
+ */
135
+ private readonly deleteUnusedRoutes: (unusedRoutes: string[]) => void,
136
+ baseLogger: ITelemetryLogger,
137
+ existing: boolean,
138
+ metadata?: IContainerRuntimeMetadata,
139
+ ) {
140
+ this.logger = ChildLogger.create(baseLogger, "GarbageCollector");
141
+
142
+ let prevSummaryGCVersion: number | undefined;
143
+ // GC can only be enabled during creation. After that, it can never be enabled again. So, for existing
144
+ // documents, we get this information from the metadata blob.
145
+ if (existing) {
146
+ prevSummaryGCVersion = getGCVersion(metadata);
147
+ // Existing documents which did not have metadata blob or had GC disabled have version as 0. For all
148
+ // other exsiting documents, GC is enabled.
149
+ this.gcEnabled = prevSummaryGCVersion > 0;
150
+ } else {
151
+ // For new documents, GC has to be exlicitly enabled via the gcAllowed flag in GC options.
152
+ this.gcEnabled = gcOptions.gcAllowed === true;
153
+ }
154
+ // For existing document, the latest summary is the one that we loaded from. So, use its GC version as the
155
+ // latest tracked GC version. For new documents, we will be writing the first summary with the current version.
156
+ this.latestSummaryGCVersion = prevSummaryGCVersion ?? this.currentGCVersion;
157
+
158
+ // Whether GC should run or not. Can override with localStorage flag.
159
+ this.shouldRunGC = getLocalStorageFeatureGate(runGCKey) ?? (
160
+ // GC must be enabled for the document.
161
+ this.gcEnabled
162
+ // GC must not be disabled via GC options.
163
+ && !gcOptions.disableGC
164
+ );
165
+
166
+ // Whether GC sweep phase should run or not. If this is false, only GC mark phase is run. Can override with
167
+ // localStorage flag.
168
+ this.shouldRunSweep = this.shouldRunGC &&
169
+ (getLocalStorageFeatureGate(runSweepKey) ?? gcOptions.runSweep === true);
170
+
171
+ // Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
172
+ this.testMode = getLocalStorageFeatureGate(gcTestModeKey) ?? gcOptions.runGCInTestMode === true;
173
+ }
174
+
175
+ /**
176
+ * Runs garbage collection and udpates the reference / used state of the nodes in the container.
177
+ * @returns the number of data stores that have been marked as unreferenced.
178
+ */
179
+ public async collectGarbage(
180
+ options: {
181
+ /** Logger to use for logging GC events */
182
+ logger?: ITelemetryLogger,
183
+ /** True to run GC sweep phase after the mark phase */
184
+ runSweep?: boolean,
185
+ /** True to generate full GC data */
186
+ fullGC?: boolean,
187
+ },
188
+ ): Promise<IGCStats> {
189
+ const {
190
+ logger = this.logger,
191
+ runSweep = this.shouldRunSweep,
192
+ fullGC = this.gcOptions.runFullGC === true || this.hasGCVersionChanged,
193
+ } = options;
194
+
195
+ return PerformanceEvent.timedExecAsync(logger, { eventName: "GarbageCollection" }, async (event) => {
196
+ const gcStats: {
197
+ deletedNodes?: number,
198
+ totalNodes?: number,
199
+ deletedDataStores?: number,
200
+ totalDataStores?: number,
201
+ } = {};
202
+
203
+ // Get the runtime's GC data and run GC on the reference graph in it.
204
+ const gcData = await this.provider.getGCData(fullGC);
205
+ const { referencedNodeIds, deletedNodeIds } = runGarbageCollection(
206
+ gcData.gcNodes,
207
+ [ "/" ],
208
+ logger,
209
+ );
210
+
211
+ // Remove this node's route ("/") and notify data stores of routes that are used in it.
212
+ const usedRoutes = referencedNodeIds.filter((id: string) => { return id !== "/"; });
213
+ const dataStoreUsedStateStats = this.provider.updateUsedRoutes(usedRoutes);
214
+
215
+ if (runSweep) {
216
+ // Placeholder for running sweep logic.
217
+ }
218
+
219
+ // Update stats to be reported in the peformance event.
220
+ gcStats.deletedNodes = deletedNodeIds.length;
221
+ gcStats.totalNodes = referencedNodeIds.length + deletedNodeIds.length;
222
+ gcStats.deletedDataStores = dataStoreUsedStateStats.unusedNodeCount;
223
+ gcStats.totalDataStores = dataStoreUsedStateStats.totalNodeCount;
224
+
225
+ // If we are running in GC test mode, delete objects for unused routes. This enables testing scenarios
226
+ // involving access to deleted data.
227
+ if (this.testMode) {
228
+ this.deleteUnusedRoutes(deletedNodeIds);
229
+ }
230
+ event.end(gcStats);
231
+ return gcStats as IGCStats;
232
+ },
233
+ { end: true, cancel: "error" });
234
+ }
235
+
236
+ /**
237
+ * Called when the latest summary of the system has been refreshed. This will be used to update the state of the
238
+ * latest summary tracked.
239
+ */
240
+ public async latestSummaryStateRefreshed(
241
+ result: RefreshSummaryResult,
242
+ readAndParseBlob: ReadAndParseBlob,
243
+ ): Promise<void> {
244
+ if (!this.shouldRunGC || !result.latestSummaryUpdated) {
245
+ return;
246
+ }
247
+
248
+ // If the summary was tracked by this client, it was the one that generated the summary in the first place.
249
+ // Basically, it was written in the current GC version.
250
+ if (result.wasSummaryTracked) {
251
+ this.latestSummaryGCVersion = this.currentGCVersion;
252
+ return;
253
+ }
254
+ // If the summary was not tracked by this client, update latest GC version from the snapshot in the result as
255
+ // that is now the latest summary.
256
+ await this.updateSummaryGCVersionFromSnapshot(result.snapshot, readAndParseBlob);
257
+ }
258
+
259
+ /**
260
+ * Update the latest summary GC version from the metadata blob in the given snapshot.
261
+ */
262
+ private async updateSummaryGCVersionFromSnapshot(snapshot: ISnapshotTree, readAndParseBlob: ReadAndParseBlob) {
263
+ const metadataBlobId = snapshot.blobs[metadataBlobName];
264
+ if (metadataBlobId) {
265
+ const metadata = await readAndParseBlob<IContainerRuntimeMetadata>(metadataBlobId);
266
+ this.latestSummaryGCVersion = getGCVersion(metadata);
267
+ }
268
+ }
269
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  export * from "./containerRuntime";
7
7
  export * from "./deltaScheduler";
8
8
  export * from "./dataStoreRegistry";
9
+ export { IGarbageCollectionRuntime, IGCStats, IUsedStateStats } from "./garbageCollection";
9
10
  export * from "./pendingStateManager";
10
11
  export * from "./summarizer";
11
12
  export * from "./summarizerTypes";
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "0.51.0-43124";
9
+ export const pkgVersion = "0.51.3";
@@ -71,7 +71,6 @@ export class PendingStateManager implements IDisposable {
71
71
  private readonly initialStates: Deque<IPendingState>;
72
72
  private readonly previousClientIds = new Set<string>();
73
73
  private readonly firstStashedCSN: number = -1;
74
- private stashedCount = 0;
75
74
  private readonly disposeOnce = new Lazy<void>(() => {
76
75
  this.initialStates.clear();
77
76
  this.pendingStates.clear();
@@ -127,7 +126,6 @@ export class PendingStateManager implements IDisposable {
127
126
  // get stashed op count and client sequence number of first op
128
127
  const messages = initialState.pendingStates
129
128
  .filter((state) => state.type === "message") as IPendingMessage[];
130
- this.stashedCount = messages.length;
131
129
  this.firstStashedCSN = messages[0].clientSequenceNumber;
132
130
  }
133
131
  }
@@ -290,7 +288,6 @@ export class PendingStateManager implements IDisposable {
290
288
  // if it's not a message just drop it and keep looking
291
289
  if (nextState.type === "message") {
292
290
  this.assertOpMatch(nextState, message, isOriginalClientId);
293
- --this.stashedCount;
294
291
  return { localAck: true, localOpMetadata: nextState.localOpMetadata };
295
292
  }
296
293
  }
@@ -454,7 +451,6 @@ export class PendingStateManager implements IDisposable {
454
451
  // This assert suggests we are about to send same ops twice, which will result in data loss.
455
452
  assert(this.clientId !== this.containerRuntime.clientId,
456
453
  0x173 /* "replayPendingStates called twice for same clientId!" */);
457
- const prevClientId = this.clientId;
458
454
  this.clientId = this.containerRuntime.clientId;
459
455
 
460
456
  assert(this.initialStates.isEmpty(), 0x174 /* "initial states should be empty before replaying pending" */);
@@ -464,45 +460,6 @@ export class PendingStateManager implements IDisposable {
464
460
  return;
465
461
  }
466
462
 
467
- if (!prevClientId && this.stashedCount > 0) {
468
- // this is first connect, verify we are about to "resubmit" only stashed ops
469
- assert(this.pendingStates.toArray().filter((s) => s.type === "message").length === this.stashedCount,
470
- 0x290 /* "unexpected message queued before first connect" */);
471
-
472
- Array.from(this.previousClientIds).map((id) =>
473
- assert(this.containerRuntime.getQuorum().getMember(id) === undefined,
474
- 0x291 /* "client with stashed ops already connected" */));
475
-
476
- // send rejoin op with stashed client ID if we have it
477
- if (this.previousClientIds.size > 0) {
478
- const clientId = Array.from(this.previousClientIds)[0];
479
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
480
- this.pendingStates.unshift({
481
- type: "message",
482
- messageType: ContainerMessageType.Rejoin,
483
- content: { clientId },
484
- } as IPendingMessage);
485
- ++pendingStatesCount;
486
- }
487
- }
488
-
489
- if (prevClientId) {
490
- // add a rejoin op so future clients provided with our stashed pending ops can recognize them
491
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
492
- const firstState = this.pendingStates.peekFront()!;
493
- if (firstState.type !== "message" || firstState.messageType !== ContainerMessageType.Rejoin) {
494
- // if there is already a rejoin op in the queue, just resubmit same op under new client ID
495
- // otherwise, add one to the queue
496
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
497
- this.pendingStates.unshift({
498
- type: "message",
499
- messageType: ContainerMessageType.Rejoin,
500
- content: { clientId: prevClientId },
501
- } as IPendingMessage);
502
- ++pendingStatesCount;
503
- }
504
- }
505
-
506
463
  // Reset the pending message count because all these messages will be removed from the queue.
507
464
  this.pendingMessagesCount = 0;
508
465