@fluidframework/container-runtime 2.0.0-internal.2.0.4 → 2.0.0-internal.2.1.1

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 (78) hide show
  1. package/dist/containerRuntime.d.ts +2 -1
  2. package/dist/containerRuntime.d.ts.map +1 -1
  3. package/dist/containerRuntime.js +63 -23
  4. package/dist/containerRuntime.js.map +1 -1
  5. package/dist/dataStore.d.ts.map +1 -1
  6. package/dist/dataStore.js +6 -0
  7. package/dist/dataStore.js.map +1 -1
  8. package/dist/dataStoreContext.d.ts +7 -0
  9. package/dist/dataStoreContext.d.ts.map +1 -1
  10. package/dist/dataStoreContext.js +34 -8
  11. package/dist/dataStoreContext.js.map +1 -1
  12. package/dist/dataStoreContexts.js +1 -1
  13. package/dist/dataStoreContexts.js.map +1 -1
  14. package/dist/dataStores.d.ts +3 -2
  15. package/dist/dataStores.d.ts.map +1 -1
  16. package/dist/dataStores.js +29 -3
  17. package/dist/dataStores.js.map +1 -1
  18. package/dist/garbageCollection.d.ts +19 -5
  19. package/dist/garbageCollection.d.ts.map +1 -1
  20. package/dist/garbageCollection.js +120 -37
  21. package/dist/garbageCollection.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/packageVersion.d.ts +1 -1
  27. package/dist/packageVersion.js +1 -1
  28. package/dist/packageVersion.js.map +1 -1
  29. package/dist/summarizerClientElection.js +1 -1
  30. package/dist/summarizerClientElection.js.map +1 -1
  31. package/dist/summaryGenerator.d.ts.map +1 -1
  32. package/dist/summaryGenerator.js +3 -2
  33. package/dist/summaryGenerator.js.map +1 -1
  34. package/garbageCollection.md +27 -22
  35. package/lib/containerRuntime.d.ts +2 -1
  36. package/lib/containerRuntime.d.ts.map +1 -1
  37. package/lib/containerRuntime.js +64 -24
  38. package/lib/containerRuntime.js.map +1 -1
  39. package/lib/dataStore.d.ts.map +1 -1
  40. package/lib/dataStore.js +6 -0
  41. package/lib/dataStore.js.map +1 -1
  42. package/lib/dataStoreContext.d.ts +7 -0
  43. package/lib/dataStoreContext.d.ts.map +1 -1
  44. package/lib/dataStoreContext.js +35 -9
  45. package/lib/dataStoreContext.js.map +1 -1
  46. package/lib/dataStoreContexts.js +1 -1
  47. package/lib/dataStoreContexts.js.map +1 -1
  48. package/lib/dataStores.d.ts +3 -2
  49. package/lib/dataStores.d.ts.map +1 -1
  50. package/lib/dataStores.js +30 -4
  51. package/lib/dataStores.js.map +1 -1
  52. package/lib/garbageCollection.d.ts +19 -5
  53. package/lib/garbageCollection.d.ts.map +1 -1
  54. package/lib/garbageCollection.js +119 -36
  55. package/lib/garbageCollection.js.map +1 -1
  56. package/lib/index.d.ts +1 -1
  57. package/lib/index.d.ts.map +1 -1
  58. package/lib/index.js +1 -1
  59. package/lib/index.js.map +1 -1
  60. package/lib/packageVersion.d.ts +1 -1
  61. package/lib/packageVersion.js +1 -1
  62. package/lib/packageVersion.js.map +1 -1
  63. package/lib/summarizerClientElection.js +1 -1
  64. package/lib/summarizerClientElection.js.map +1 -1
  65. package/lib/summaryGenerator.d.ts.map +1 -1
  66. package/lib/summaryGenerator.js +3 -2
  67. package/lib/summaryGenerator.js.map +1 -1
  68. package/package.json +26 -23
  69. package/src/containerRuntime.ts +77 -26
  70. package/src/dataStore.ts +13 -1
  71. package/src/dataStoreContext.ts +48 -10
  72. package/src/dataStoreContexts.ts +1 -1
  73. package/src/dataStores.ts +34 -3
  74. package/src/garbageCollection.ts +144 -44
  75. package/src/index.ts +1 -1
  76. package/src/packageVersion.ts +1 -1
  77. package/src/summarizerClientElection.ts +1 -1
  78. package/src/summaryGenerator.ts +3 -2
@@ -3,7 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { IDisposable, ITelemetryLogger } from "@fluidframework/common-definitions";
6
+ import { IDisposable, ITelemetryLogger, ITelemetryProperties } from "@fluidframework/common-definitions";
7
7
  import {
8
8
  FluidObject,
9
9
  IRequest,
@@ -65,7 +65,11 @@ import {
65
65
  TelemetryDataTag,
66
66
  ThresholdCounter,
67
67
  } from "@fluidframework/telemetry-utils";
68
- import { DataProcessingError } from "@fluidframework/container-utils";
68
+ import {
69
+ DataCorruptionError,
70
+ DataProcessingError,
71
+ extractSafePropertiesFromMessage,
72
+ } from "@fluidframework/container-utils";
69
73
 
70
74
  import { ContainerRuntime } from "./containerRuntime";
71
75
  import {
@@ -190,6 +194,13 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
190
194
  private _disposed = false;
191
195
  public get disposed() { return this._disposed; }
192
196
 
197
+ /**
198
+ * Tombstone is a temporary feature that prevents a data store from sending / receiving ops, signals and from
199
+ * loading.
200
+ */
201
+ private _tombstoned = false;
202
+ public get tombstoned() { return this._tombstoned; }
203
+
193
204
  public get attachState(): AttachState {
194
205
  return this._attachState;
195
206
  }
@@ -305,6 +316,14 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
305
316
  }
306
317
  }
307
318
 
319
+ public setTombstone(tombstone: boolean) {
320
+ if (this.tombstoned === tombstone) {
321
+ return;
322
+ }
323
+
324
+ this._tombstoned = tombstone;
325
+ }
326
+
308
327
  private rejectDeferredRealize(reason: string, packageName?: string): never {
309
328
  throw new LoggingError(reason, { packageName: { value: packageName, tag: TelemetryDataTag.CodeArtifact } });
310
329
  }
@@ -381,7 +400,8 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
381
400
  * its new client ID when we are connecting or connected.
382
401
  */
383
402
  public setConnectionState(connected: boolean, clientId?: string) {
384
- this.verifyNotClosed();
403
+ // ConnectionState should not fail in tombstone mode as this is internally run
404
+ this.verifyNotClosed("setConnectionState", false /* checkTombstone */);
385
405
 
386
406
  // Connection events are ignored if the store is not yet loaded
387
407
  if (!this.loaded) {
@@ -395,7 +415,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
395
415
  }
396
416
 
397
417
  public process(messageArg: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown): void {
398
- this.verifyNotClosed();
418
+ this.verifyNotClosed("process", true, extractSafePropertiesFromMessage(messageArg));
399
419
 
400
420
  const innerContents = messageArg.contents as FluidDataStoreMessage;
401
421
  const message = {
@@ -417,7 +437,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
417
437
  }
418
438
 
419
439
  public processSignal(message: IInboundSignalMessage, local: boolean): void {
420
- this.verifyNotClosed();
440
+ this.verifyNotClosed("processSignal");
421
441
 
422
442
  // Signals are ignored if the store is not yet loaded
423
443
  if (!this.loaded) {
@@ -582,7 +602,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
582
602
  }
583
603
 
584
604
  public submitMessage(type: string, content: any, localOpMetadata: unknown): void {
585
- this.verifyNotClosed();
605
+ this.verifyNotClosed("submitMessage");
586
606
  assert(!!this.channel, 0x146 /* "Channel must exist when submitting message" */);
587
607
  const fluidDataStoreContent: FluidDataStoreMessage = {
588
608
  content,
@@ -604,7 +624,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
604
624
  *
605
625
  */
606
626
  public setChannelDirty(address: string): void {
607
- this.verifyNotClosed();
627
+ this.verifyNotClosed("setChannelDirty");
608
628
 
609
629
  // Get the latest sequence number.
610
630
  const latestSequenceNumber = this.deltaManager.lastSequenceNumber;
@@ -619,7 +639,8 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
619
639
  }
620
640
 
621
641
  public submitSignal(type: string, content: any) {
622
- this.verifyNotClosed();
642
+ this.verifyNotClosed("submitSignal");
643
+
623
644
  assert(!!this.channel, 0x147 /* "Channel must exist on submitting signal" */);
624
645
  return this._containerRuntime.submitDataStoreSignal(this.id, type, content);
625
646
  }
@@ -732,9 +753,17 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
732
753
  return this.channel.applyStashedOp(innerContents.content);
733
754
  }
734
755
 
735
- private verifyNotClosed() {
756
+ private verifyNotClosed(callSite: string, checkTombstone = true, safeTelemetryProps: ITelemetryProperties = {}) {
736
757
  if (this._disposed) {
737
- throw new Error("Context is closed");
758
+ throw new Error(`Context is closed! Call site [${callSite}]`);
759
+ }
760
+
761
+ if (checkTombstone && this.tombstoned) {
762
+ const messageString = `Context is tombstoned! Call site [${callSite}]`;
763
+ throw new DataCorruptionError(messageString, {
764
+ errorMessage: messageString,
765
+ ...safeTelemetryProps,
766
+ });
738
767
  }
739
768
  }
740
769
 
@@ -991,6 +1020,15 @@ export class LocalDetachedFluidDataStoreContext
991
1020
 
992
1021
  super.bindRuntime(dataStoreChannel);
993
1022
 
1023
+ // Load the handle to the data store's entryPoint to make sure that for a detached data store, the entryPoint
1024
+ // initialization function is called before the data store gets attached and potentially connected to the
1025
+ // delta stream, so it gets a chance to do things while the data store is still "purely local".
1026
+ // This preserves the behavior from before we introduced entryPoints, where the instantiateDataStore method
1027
+ // of data store factories tends to construct the data object (at least kick off an async method that returns
1028
+ // it); that code moved to the entryPoint initialization function, so we want to ensure it still executes
1029
+ // before the data store is attached.
1030
+ await dataStoreChannel.entryPoint?.get();
1031
+
994
1032
  if (await this.isRoot()) {
995
1033
  dataStoreChannel.makeVisibleAndAttachGraph();
996
1034
  }
@@ -87,7 +87,7 @@ import { FluidDataStoreContext, LocalFluidDataStoreContext } from "./dataStoreCo
87
87
  return undefined;
88
88
  }
89
89
 
90
- return this._contexts.get(id) as LocalFluidDataStoreContext;
90
+ return context as LocalFluidDataStoreContext;
91
91
  }
92
92
 
93
93
  /**
package/src/dataStores.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  convertSnapshotTreeToSummaryTree,
33
33
  convertToSummaryTree,
34
34
  create404Response,
35
+ createResponseError,
35
36
  responseToException,
36
37
  SummaryTreeBuilder,
37
38
  } from "@fluidframework/runtime-utils";
@@ -419,14 +420,25 @@ export class DataStores implements IDisposable {
419
420
  );
420
421
  }
421
422
 
422
- public async getDataStore(id: string, wait: boolean): Promise<FluidDataStoreContext> {
423
+ public async getDataStore(id: string, wait: boolean, viaHandle: boolean): Promise<FluidDataStoreContext> {
423
424
  const context = await this.contexts.getBoundOrRemoted(id, wait);
425
+ const request = { url: id };
424
426
  if (context === undefined) {
425
427
  // The requested data store does not exits. Throw a 404 response exception.
426
- const request = { url: id };
427
428
  throw responseToException(create404Response(request), request);
428
429
  }
429
430
 
431
+ if (context.tombstoned) {
432
+ // Note: if a user writes a request to look like it's viaHandle, we will also send this telemetry event
433
+ this.logger.sendErrorEvent({
434
+ eventName: "TombstonedDataStoreRequested",
435
+ url: request.url,
436
+ viaHandle,
437
+ });
438
+ // The requested data store is removed by gc. Throw a 404 gc response exception.
439
+ throw responseToException(createResponseError(404, "Datastore removed by gc", request), request);
440
+ }
441
+
430
442
  return context;
431
443
  }
432
444
 
@@ -597,6 +609,11 @@ export class DataStores implements IDisposable {
597
609
  // Verify that the used routes are correct.
598
610
  for (const [id] of usedDataStoreRoutes) {
599
611
  assert(this.contexts.has(id), 0x167 /* "Used route does not belong to any known data store" */);
612
+
613
+ // Revive datastores regardless of whether or not tombstone the tombstone flag is flipped
614
+ const dataStore = this.contexts.get(id);
615
+ assert(dataStore !== undefined, 0x46e /* No data store retrieved with specified id */);
616
+ dataStore.setTombstone(false /* tombstone */);
600
617
  }
601
618
 
602
619
  // Update the used routes in each data store. Used routes is empty for unused data stores.
@@ -609,8 +626,9 @@ export class DataStores implements IDisposable {
609
626
  * When running GC in test mode, this is called to delete objects whose routes are unused. This enables testing
610
627
  * scenarios with accessing deleted content.
611
628
  * @param unusedRoutes - The routes that are unused in all data stores in this Container.
629
+ * @param tombstone - set the objects corresponding to routes as tombstones.
612
630
  */
613
- public deleteUnusedRoutes(unusedRoutes: string[]) {
631
+ public deleteUnusedRoutes(unusedRoutes: string[], tombstone: boolean = false) {
614
632
  for (const route of unusedRoutes) {
615
633
  const pathParts = route.split("/");
616
634
  // Delete data store only if its route (/datastoreId) is in unusedRoutes. We don't want to delete a data
@@ -620,6 +638,19 @@ export class DataStores implements IDisposable {
620
638
  }
621
639
  const dataStoreId = pathParts[1];
622
640
  assert(this.contexts.has(dataStoreId), 0x2d7 /* No data store with specified id */);
641
+
642
+ /**
643
+ * When running GC in tombstone mode, datastore contexts are tombstoned. Tombstoned datastore contexts
644
+ * enable testing scenarios with accessing deleted content without actually deleting content from
645
+ * summaries.
646
+ */
647
+ if (tombstone) {
648
+ const dataStore = this.contexts.get(dataStoreId);
649
+ assert(dataStore !== undefined, 0x442 /* No data store retrieved with specified id */);
650
+ dataStore.setTombstone(true /* tombstone */);
651
+ continue;
652
+ }
653
+
623
654
  // Delete the contexts of unused data stores.
624
655
  this.contexts.delete(dataStoreId);
625
656
  // Delete the summarizer node of the unused data stores.
@@ -25,6 +25,7 @@ import {
25
25
  ISummarizeResult,
26
26
  ITelemetryContext,
27
27
  IGarbageCollectionNodeData,
28
+ ISummaryTreeWithStats,
28
29
  } from "@fluidframework/runtime-definitions";
29
30
  import {
30
31
  mergeStats,
@@ -61,6 +62,8 @@ const GCVersion = 1;
61
62
  export const gcTreeKey = "gc";
62
63
  // They prefix for GC blobs in the GC tree in summary.
63
64
  export const gcBlobPrefix = "__gc";
65
+ // The key for tombstone blob in the GC tree in summary.
66
+ export const gcTombstoneBlobKey = "__tombstones";
64
67
 
65
68
  // Feature gate key to turn GC on / off.
66
69
  export const runGCKey = "Fluid.GarbageCollection.RunGC";
@@ -74,6 +77,8 @@ export const runSessionExpiryKey = "Fluid.GarbageCollection.RunSessionExpiry";
74
77
  export const trackGCStateKey = "Fluid.GarbageCollection.TrackGCState";
75
78
  // Feature gate key to turn GC sweep log off.
76
79
  export const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
80
+ // Feature gate key to tombstone datastores.
81
+ export const testTombstoneKey = "Fluid.GarbageCollection.Test.Tombstone";
77
82
 
78
83
  // One day in milliseconds.
79
84
  export const oneDayMs = 1 * 24 * 60 * 60 * 1000;
@@ -214,6 +219,14 @@ interface IUnreferencedEventProps {
214
219
  viaHandle?: boolean;
215
220
  }
216
221
 
222
+ /**
223
+ * The GC data that is tracked for a summary that is submitted.
224
+ */
225
+ interface IGCSummaryTrackingData {
226
+ serializedGCState: string | undefined;
227
+ serializedTombstones: string | undefined;
228
+ }
229
+
217
230
  /**
218
231
  * Helper class that tracks the state of an unreferenced node such as the time it was unreferenced and if it can
219
232
  * be deleted by the sweep phase.
@@ -370,6 +383,7 @@ export class GarbageCollector implements IGarbageCollector {
370
383
  public readonly trackGCState: boolean;
371
384
 
372
385
  private readonly testMode: boolean;
386
+ private readonly tombstoneMode: boolean;
373
387
  private readonly mc: MonitoringContext;
374
388
 
375
389
  /**
@@ -393,17 +407,19 @@ export class GarbageCollector implements IGarbageCollector {
393
407
 
394
408
  // Keeps track of the GC state from the last run.
395
409
  private previousGCDataFromLastRun: IGarbageCollectionData | undefined;
410
+ // Keeps a list of references (edges in the GC graph) between GC runs. Each entry has a node id and a list of
411
+ // outbound routes from that node.
412
+ private readonly newReferencesSinceLastRun: Map<string, string[]> = new Map();
413
+ private tombstones: string[] = [];
414
+
396
415
  /**
397
- * Keeps track of the serialized GC blob from the latest summary successfully submitted to the server.
416
+ * Keeps track of the GC data from the latest summary successfully submitted to and acked from the server.
398
417
  */
399
- private latestSerializedSummaryState: string | undefined;
418
+ private latestSummaryData: IGCSummaryTrackingData | undefined;
400
419
  /**
401
- * Keeps track of the serialized GC blob from the last GC run of the client.
420
+ * Keeps track of the GC data from the last summary submitted to the server but not yet acked.
402
421
  */
403
- private pendingSerializedSummaryState: string | undefined;
404
- // Keeps a list of references (edges in the GC graph) between GC runs. Each entry has a node id and a list of
405
- // outbound routes from that node.
406
- private readonly newReferencesSinceLastRun: Map<string, string[]> = new Map();
422
+ private pendingSummaryData: IGCSummaryTrackingData | undefined;
407
423
 
408
424
  // Promise when resolved initializes the base state of the nodes from the base summary state.
409
425
  private readonly initializeBaseStateP: Promise<void>;
@@ -449,6 +465,7 @@ export class GarbageCollector implements IGarbageCollector {
449
465
  runGC: this.shouldRunGC,
450
466
  runSweep: this.shouldRunSweep,
451
467
  testMode: this.testMode,
468
+ tombstoneMode: this.tombstoneMode,
452
469
  sessionExpiry: this.sessionExpiryTimeoutMs,
453
470
  sweepTimeout: this.sweepTimeoutMs,
454
471
  inactiveTimeout: this.inactiveTimeoutMs,
@@ -606,6 +623,7 @@ export class GarbageCollector implements IGarbageCollector {
606
623
 
607
624
  // Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
608
625
  this.testMode = this.mc.config.getBoolean(gcTestModeKey) ?? this.gcOptions.runGCInTestMode === true;
626
+ this.tombstoneMode = this.mc.config.getBoolean(testTombstoneKey) ?? false;
609
627
 
610
628
  // The GC state needs to be reset if the base snapshot contains GC tree and GC is disabled or it doesn't
611
629
  // contain GC tree and GC is enabled.
@@ -623,14 +641,20 @@ export class GarbageCollector implements IGarbageCollector {
623
641
  // For newer documents, GC data should be present in the GC tree in the root of the snapshot.
624
642
  const gcSnapshotTree = baseSnapshot.trees[gcTreeKey];
625
643
  if (gcSnapshotTree !== undefined) {
626
- const baseGCState = await getGCStateFromSnapshot(
644
+ const baseGCData = await getGCDataFromSnapshot(
627
645
  gcSnapshotTree,
628
646
  readAndParseBlob,
629
647
  );
648
+ if (baseGCData.tombstones !== undefined && this.tombstoneMode) {
649
+ this.tombstones = baseGCData.tombstones;
650
+ }
630
651
  if (this.trackGCState) {
631
- this.latestSerializedSummaryState = JSON.stringify(generateSortedGCState(baseGCState));
652
+ this.latestSummaryData = {
653
+ serializedGCState: JSON.stringify(generateSortedGCState(baseGCData.gcState)),
654
+ serializedTombstones: JSON.stringify(baseGCData.tombstones),
655
+ };
632
656
  }
633
- return baseGCState;
657
+ return baseGCData.gcState;
634
658
  }
635
659
 
636
660
  // back-compat - Older documents will have the GC blobs in each data store's summary tree. Get them and
@@ -787,15 +811,19 @@ export class GarbageCollector implements IGarbageCollector {
787
811
  */
788
812
  public setConnectionState(connected: boolean, clientId?: string | undefined): void {
789
813
  /**
790
- * For non-summarizer clients, initialize the base state when the container becomes active, i.e., it transitions
814
+ * For all clients, initialize the base state when the container becomes active, i.e., it transitions
791
815
  * to "write" mode. This will ensure that the container's own join op is processed and there is a recent
792
816
  * reference timestamp that will be used to update the state of unreferenced nodes. Also, all trailing ops which
793
817
  * could affect the GC state will have been processed.
794
818
  *
819
+ * If GC is up-to-date for the client and the summarizing client, there will be an doubling of both
820
+ * InactiveObject_Loaded and SweepReady_Loaded errors, as there will be one from the sending client and one from
821
+ * the receiving summarizer client.
822
+ *
795
823
  * Ideally, this initialization should only be done for summarizer client. However, we are currently rolling out
796
824
  * sweep in phases and we want to track when inactive and sweep ready objects are used in any client.
797
825
  */
798
- if (this.activeConnection() && !this.isSummarizerClient && this.shouldRunGC) {
826
+ if (this.activeConnection() && this.shouldRunGC) {
799
827
  this.initializeBaseStateP.catch((error) => {});
800
828
  }
801
829
  }
@@ -884,6 +912,23 @@ export class GarbageCollector implements IGarbageCollector {
884
912
  // involving access to deleted data.
885
913
  if (this.testMode) {
886
914
  this.runtime.deleteUnusedRoutes(gcResult.deletedNodeIds);
915
+ } else {
916
+ // If we are running in GC tombstone mode, tombstone objects for unused routes. This enables testing
917
+ // scenarios involving access to "deleted" data without actually deleting the data from summaries.
918
+ // Note: we will not tombstone in test mode
919
+ if (this.tombstoneMode) {
920
+ const tombstoneRoutes: string[] = [];
921
+ // Currently only tombstone datastores
922
+ for (const [key, value] of this.unreferencedNodesState.entries()) {
923
+ if (
924
+ value.state === UnreferencedState.SweepReady &&
925
+ this.runtime.getNodeType(key) === GCNodeType.DataStore
926
+ ) {
927
+ tombstoneRoutes.push(key);
928
+ }
929
+ }
930
+ this.runtime.deleteUnusedRoutes(tombstoneRoutes);
931
+ }
887
932
  }
888
933
 
889
934
  // Log pending unreferenced events such as a node being used after inactive. This is done after GC runs and
@@ -916,35 +961,74 @@ export class GarbageCollector implements IGarbageCollector {
916
961
  };
917
962
  }
918
963
 
919
- const newSerializedSummaryState = JSON.stringify(generateSortedGCState(gcState));
964
+ const serializedGCState = JSON.stringify(generateSortedGCState(gcState));
965
+ const serializedTombstones = this.tombstones.length > 0 ? JSON.stringify(this.tombstones.sort()) : undefined;
920
966
 
921
967
  /**
922
- * As an optimization if the GC tree hasn't changed and we're tracking the gc state, return a tree handle
923
- * instead of returning the whole GC tree. If there are changes, then we want to return the whole tree.
968
+ * Incremental summary of GC data - If any of the GC state or tombstone state hasn't changed since the last
969
+ * summary, send summary handles for them. Otherwise, send the data in summary blobs.
924
970
  */
925
971
  if (this.trackGCState) {
926
- this.pendingSerializedSummaryState = newSerializedSummaryState;
927
- if (
928
- this.latestSerializedSummaryState !== undefined &&
929
- this.latestSerializedSummaryState === newSerializedSummaryState &&
930
- !fullTree &&
931
- trackState
932
- ) {
933
- const stats = mergeStats();
934
- stats.handleNodeCount++;
935
- return {
936
- summary: {
937
- type: SummaryType.Handle,
938
- handle: `/${gcTreeKey}`,
939
- handleType: SummaryType.Tree,
940
- },
941
- stats,
942
- };
972
+ this.pendingSummaryData = { serializedGCState, serializedTombstones };
973
+ if (trackState && !fullTree && this.latestSummaryData !== undefined) {
974
+ // If neither GC state or tombstone state changed, send a summary handle for the entire GC data.
975
+ if (this.latestSummaryData.serializedGCState === serializedGCState
976
+ && this.latestSummaryData.serializedTombstones === serializedTombstones) {
977
+ const stats = mergeStats();
978
+ stats.handleNodeCount++;
979
+ return {
980
+ summary: {
981
+ type: SummaryType.Handle,
982
+ handle: `/${gcTreeKey}`,
983
+ handleType: SummaryType.Tree,
984
+ },
985
+ stats,
986
+ };
987
+ }
988
+
989
+ // If either or both of GC state or tombstone state changed, build a GC summary tree.
990
+ return this.buildGCSummaryTree(serializedGCState, serializedTombstones, true /* trackState */);
943
991
  }
944
992
  }
993
+ // If not tracking GC state, build a GC summary tree without any summary handles.
994
+ return this.buildGCSummaryTree(serializedGCState, serializedTombstones, false /* trackState */);
995
+ }
945
996
 
997
+ /**
998
+ * Builds the GC summary tree which contains GC state and tombstone state.
999
+ * If trackState is false, both GC state and tombstone state are written as summary blobs.
1000
+ * If trackState is true, summary blob is written for GC state or tombstone state if they changed.
1001
+ * @param serializedGCState - The GC state serialized as string.
1002
+ * @param serializedTombstones - THe tombstone state serialized as string.
1003
+ * @param trackState - Whether we are tracking GC state across summaries.
1004
+ * @returns the GC summary tree.
1005
+ */
1006
+ private buildGCSummaryTree(
1007
+ serializedGCState: string,
1008
+ serializedTombstones: string | undefined,
1009
+ trackState: boolean,
1010
+ ): ISummaryTreeWithStats {
1011
+ const gcStateBlobKey = `${gcBlobPrefix}_root`;
946
1012
  const builder = new SummaryTreeBuilder();
947
- builder.addBlob(`${gcBlobPrefix}_root`, newSerializedSummaryState);
1013
+
1014
+ // If the GC state hasn't changed, write a summary handle, else write a summary blob for it.
1015
+ if (this.latestSummaryData?.serializedGCState === serializedGCState && trackState) {
1016
+ builder.addHandle(gcStateBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcStateBlobKey}`);
1017
+ } else {
1018
+ builder.addBlob(gcStateBlobKey, serializedGCState);
1019
+ }
1020
+
1021
+ // If there is no tombstone data, return only the GC state.
1022
+ if (serializedTombstones === undefined) {
1023
+ return builder.getSummaryTree();
1024
+ }
1025
+
1026
+ // If the tombstone state hasn't changed, write a summary handle, else write a summary blob for it.
1027
+ if (this.latestSummaryData?.serializedTombstones === serializedTombstones && trackState) {
1028
+ builder.addHandle(gcTombstoneBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcTombstoneBlobKey}`);
1029
+ } else {
1030
+ builder.addBlob(gcTombstoneBlobKey, serializedTombstones);
1031
+ }
948
1032
  return builder.getSummaryTree();
949
1033
  }
950
1034
 
@@ -987,8 +1071,8 @@ export class GarbageCollector implements IGarbageCollector {
987
1071
  this.latestSummaryGCVersion = this.currentGCVersion;
988
1072
  this.initialStateNeedsReset = false;
989
1073
  if (this.trackGCState) {
990
- this.latestSerializedSummaryState = this.pendingSerializedSummaryState;
991
- this.pendingSerializedSummaryState = undefined;
1074
+ this.latestSummaryData = this.pendingSummaryData;
1075
+ this.pendingSummaryData = undefined;
992
1076
  }
993
1077
  return;
994
1078
  }
@@ -1003,15 +1087,18 @@ export class GarbageCollector implements IGarbageCollector {
1003
1087
 
1004
1088
  const gcSnapshotTree = snapshot.trees[gcTreeKey];
1005
1089
  if (gcSnapshotTree !== undefined && this.trackGCState) {
1006
- const latestGCState = await getGCStateFromSnapshot(
1090
+ const latestGCData = await getGCDataFromSnapshot(
1007
1091
  gcSnapshotTree,
1008
1092
  readAndParseBlob,
1009
1093
  );
1010
- this.latestSerializedSummaryState = JSON.stringify(generateSortedGCState(latestGCState));
1094
+ this.latestSummaryData = {
1095
+ serializedGCState: JSON.stringify(generateSortedGCState(latestGCData.gcState)),
1096
+ serializedTombstones: JSON.stringify(latestGCData.tombstones),
1097
+ };
1011
1098
  } else {
1012
- this.latestSerializedSummaryState = undefined;
1099
+ this.latestSummaryData = undefined;
1013
1100
  }
1014
- this.pendingSerializedSummaryState = undefined;
1101
+ this.pendingSummaryData = undefined;
1015
1102
  }
1016
1103
 
1017
1104
  /**
@@ -1089,6 +1176,7 @@ export class GarbageCollector implements IGarbageCollector {
1089
1176
  currentReferenceTimestampMs: number,
1090
1177
  ) {
1091
1178
  this.previousGCDataFromLastRun = cloneGCData(gcData);
1179
+ this.tombstones = [];
1092
1180
  this.newReferencesSinceLastRun.clear();
1093
1181
 
1094
1182
  // Iterate through the referenced nodes and stop tracking if they were unreferenced before.
@@ -1121,6 +1209,12 @@ export class GarbageCollector implements IGarbageCollector {
1121
1209
  );
1122
1210
  } else {
1123
1211
  nodeStateTracker.updateTracking(currentReferenceTimestampMs);
1212
+ if (this.tombstoneMode && nodeStateTracker.state === UnreferencedState.SweepReady) {
1213
+ const nodeType = this.runtime.getNodeType(nodeId);
1214
+ if (nodeType === GCNodeType.DataStore || nodeType === GCNodeType.Blob) {
1215
+ this.tombstones.push(nodeId);
1216
+ }
1217
+ }
1124
1218
  }
1125
1219
  }
1126
1220
  }
@@ -1461,15 +1555,21 @@ export class GarbageCollector implements IGarbageCollector {
1461
1555
  }
1462
1556
 
1463
1557
  /**
1464
- * Gets the garbage collection state from the given snapshot tree. The GC state may be written into multiple blobs.
1465
- * Merge the GC state from all such blobs and return the merged GC state.
1558
+ * Gets the garbage collection data from the given snapshot tree. It contains GC state and tombstone state.
1559
+ * The GC state may be written into multiple blobs. Merge the GC state from all such blobs into one.
1466
1560
  */
1467
- async function getGCStateFromSnapshot(
1561
+ async function getGCDataFromSnapshot(
1468
1562
  gcSnapshotTree: ISnapshotTree,
1469
1563
  readAndParseBlob: ReadAndParseBlob,
1470
- ): Promise<IGarbageCollectionState> {
1564
+ ) {
1471
1565
  let rootGCState: IGarbageCollectionState = { gcNodes: {} };
1566
+ let tombstones: string[] | undefined;
1472
1567
  for (const key of Object.keys(gcSnapshotTree.blobs)) {
1568
+ if (key === gcTombstoneBlobKey) {
1569
+ tombstones = await readAndParseBlob<string[]>(gcSnapshotTree.blobs[key]);
1570
+ continue;
1571
+ }
1572
+
1473
1573
  // Skip blobs that do not start with the GC prefix.
1474
1574
  if (!key.startsWith(gcBlobPrefix)) {
1475
1575
  continue;
@@ -1484,7 +1584,7 @@ async function getGCStateFromSnapshot(
1484
1584
  // Merge the GC state of this blob into the root GC state.
1485
1585
  rootGCState = concatGarbageCollectionStates(rootGCState, gcState);
1486
1586
  }
1487
- return rootGCState;
1587
+ return { gcState: rootGCState, tombstones };
1488
1588
  }
1489
1589
 
1490
1590
  function generateSortedGCState(gcState: IGarbageCollectionState): IGarbageCollectionState {
package/src/index.ts CHANGED
@@ -28,8 +28,8 @@ export {
28
28
  export { FluidDataStoreRegistry } from "./dataStoreRegistry";
29
29
  export {
30
30
  gcBlobPrefix,
31
+ gcTombstoneBlobKey,
31
32
  gcTreeKey,
32
- IGarbageCollectionRuntime,
33
33
  IGCStats,
34
34
  } from "./garbageCollection";
35
35
  export {
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.0.0-internal.2.0.4";
9
+ export const pkgVersion = "2.0.0-internal.2.1.1";
@@ -77,7 +77,7 @@ export class SummarizerClientElection
77
77
  // Log and elect a new summarizer client.
78
78
  const opsSinceLastReport = sequenceNumber - this.lastReportedSeq;
79
79
  if (opsSinceLastReport > this.maxOpsSinceLastSummary) {
80
- this.logger.sendErrorEvent({
80
+ this.logger.sendTelemetryEvent({
81
81
  eventName: "ElectedClientNotSummarizing",
82
82
  electedClientId,
83
83
  lastSummaryAckSeqForClient: this.lastSummaryAckSeqForClient,
@@ -231,13 +231,14 @@ export class SummaryGenerator {
231
231
  const category = cancellationToken.cancelled || error?.errorType === DriverErrorType.offlineError ?
232
232
  "generic" : "error";
233
233
 
234
+ const message = getFailMessage(errorCode);
234
235
  summarizeEvent.cancel({
235
236
  ...properties,
236
237
  reason: errorCode,
237
238
  category,
238
239
  retryAfterSeconds,
239
- }, error);
240
- resultsBuilder.fail(getFailMessage(errorCode), error, nackSummaryResult, retryAfterSeconds);
240
+ }, error ?? message); // disconnect & summaryAckTimeout do not have proper error.
241
+ resultsBuilder.fail(message, error, nackSummaryResult, retryAfterSeconds);
241
242
  };
242
243
 
243
244
  // Wait to generate and send summary