@fluidframework/container-runtime 2.0.0-internal.2.3.1 → 2.0.0-internal.2.4.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.
@@ -30,6 +30,7 @@ import {
30
30
  IGarbageCollectionNodeData,
31
31
  IGarbageCollectionSummaryDetailsLegacy,
32
32
  ISummaryTreeWithStats,
33
+ gcDeletedBlobKey,
33
34
  } from "@fluidframework/runtime-definitions";
34
35
  import {
35
36
  mergeStats,
@@ -157,9 +158,8 @@ export interface IGarbageCollector {
157
158
  getBaseGCDetails(): Promise<IGarbageCollectionDetailsBase>;
158
159
  /** Called when the latest summary of the system has been refreshed. */
159
160
  refreshLatestSummary(
160
- result: RefreshSummaryResult,
161
161
  proposalHandle: string | undefined,
162
- summaryRefSeq: number,
162
+ result: RefreshSummaryResult,
163
163
  readAndParseBlob: ReadAndParseBlob,
164
164
  ): Promise<void>;
165
165
  /** Called when a node is updated. Used to detect and log when an inactive node is changed or loaded. */
@@ -172,6 +172,8 @@ export interface IGarbageCollector {
172
172
  ): void;
173
173
  /** Called when a reference is added to a node. Used to identify nodes that were referenced between summaries. */
174
174
  addedOutboundReference(fromNodePath: string, toNodePath: string): void;
175
+ /** Returns true if this node has been deleted by GC during sweep phase. */
176
+ isNodeDeleted(nodePath: string): boolean;
175
177
  setConnectionState(connected: boolean, clientId?: string): void;
176
178
  dispose(): void;
177
179
  }
@@ -226,6 +228,7 @@ interface IUnreferencedEventProps {
226
228
  interface IGCSummaryTrackingData {
227
229
  serializedGCState: string | undefined;
228
230
  serializedTombstones: string | undefined;
231
+ serializedDeletedNodes: string | undefined;
229
232
  }
230
233
 
231
234
  /**
@@ -419,7 +422,10 @@ export class GarbageCollector implements IGarbageCollector {
419
422
  // Keeps a list of references (edges in the GC graph) between GC runs. Each entry has a node id and a list of
420
423
  // outbound routes from that node.
421
424
  private readonly newReferencesSinceLastRun: Map<string, string[]> = new Map();
425
+ // A list of nodes that have been tombstoned.
422
426
  private tombstones: string[] = [];
427
+ // A list of nodes that have been deleted during sweep phase.
428
+ private deletedNodes: Set<string> = new Set();
423
429
 
424
430
  /**
425
431
  * Keeps track of the GC data from the latest summary successfully submitted to and acked from the server.
@@ -565,8 +571,7 @@ export class GarbageCollector implements IGarbageCollector {
565
571
  // flag in GC options to false.
566
572
  this.gcEnabled = this.gcOptions.gcAllowed !== false;
567
573
  // The sweep phase has to be explicitly enabled by setting the sweepAllowed flag in GC options to true.
568
- // ...unless we're using the TestOverride
569
- this.sweepEnabled = this.gcOptions.sweepAllowed === true || testOverrideSweepTimeoutMs !== undefined;
574
+ this.sweepEnabled = this.gcOptions.sweepAllowed === true;
570
575
 
571
576
  // Set the Session Expiry only if the flag is enabled and GC is enabled.
572
577
  if (this.mc.config.getBoolean(runSessionExpiryKey) && this.gcEnabled) {
@@ -640,8 +645,9 @@ export class GarbageCollector implements IGarbageCollector {
640
645
 
641
646
  // Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
642
647
  this.testMode = this.mc.config.getBoolean(gcTestModeKey) ?? this.gcOptions.runGCInTestMode === true;
643
- // Whether we are running in tombstone mode. This is true by default unless disabled via feature flags.
644
- this.tombstoneMode = this.mc.config.getBoolean(disableTombstoneKey) !== true;
648
+ // Whether we are running in tombstone mode. This is enabled by default if sweep won't run. It can be disabled
649
+ // via feature flags.
650
+ this.tombstoneMode = !this.shouldRunSweep && this.mc.config.getBoolean(disableTombstoneKey) !== true;
645
651
 
646
652
  // If GC ran in the container that generated the base snapshot, it will have a GC tree.
647
653
  this.wasGCRunInLatestSummary = baseSnapshot?.trees[gcTreeKey] !== undefined;
@@ -704,7 +710,9 @@ export class GarbageCollector implements IGarbageCollector {
704
710
  }
705
711
  // If there is only one node (root node just added above), either GC is disabled or we are loading from
706
712
  // the first summary generated by detached container. In both cases, GC was not run - return undefined.
707
- return Object.keys(gcState.gcNodes).length === 1 ? undefined : { gcState, tombstones: undefined };
713
+ return Object.keys(gcState.gcNodes).length === 1
714
+ ? undefined
715
+ : { gcState, tombstones: undefined, deletedNodes: undefined };
708
716
  } catch (error) {
709
717
  const dpe = DataProcessingError.wrapIfUnrecognized(
710
718
  error,
@@ -782,24 +790,33 @@ export class GarbageCollector implements IGarbageCollector {
782
790
  }
783
791
 
784
792
  /**
785
- * Called during container initialization. Initialize the tombstone state so that object are marked as tombstones
786
- * before they are loaded or used. This is important to get accurate information of whether tombstoned object are
787
- * in use or not.
793
+ * Called during container initialization. Initialize from the tombstone state in the base snapshot. This is done
794
+ * during initialization so that deleted or tombstoned objects are marked as such before they are loaded or used.
788
795
  */
789
796
  public async initializeBaseState(): Promise<void> {
790
797
  const baseSnapshotData = await this.baseSnapshotDataP;
791
798
  /**
792
- * The base snapshot data or tombstone state will not be present if the container is loaded from:
799
+ * The base snapshot data will not be present if the container is loaded from:
793
800
  * 1. The first summary created by the detached container.
794
801
  * 2. A summary that was generated with GC disabled.
795
802
  * 3. A summary that was generated before GC even existed.
796
- * 4. A summary that was generated with tombstone feature disabled.
797
803
  */
798
- if (!this.tombstoneMode || baseSnapshotData?.tombstones === undefined) {
804
+ if (baseSnapshotData === undefined) {
799
805
  return;
800
806
  }
801
- this.tombstones = Array.from(baseSnapshotData.tombstones);
802
- this.runtime.updateTombstonedRoutes(this.tombstones);
807
+
808
+ // Initialize the deleted nodes from the snapshot. This is done irrespective of whether sweep is enabled or not
809
+ // to identify deleted nodes' usage.
810
+ if (baseSnapshotData.deletedNodes !== undefined) {
811
+ this.deletedNodes = new Set(baseSnapshotData.deletedNodes);
812
+ }
813
+
814
+ // If running in tombstone mode, initialize the tombstone state from the snapshot. Also, notify the runtime of
815
+ // tombstone routes.
816
+ if (this.tombstoneMode && baseSnapshotData.tombstones !== undefined) {
817
+ this.tombstones = Array.from(baseSnapshotData.tombstones);
818
+ this.runtime.updateTombstonedRoutes(this.tombstones);
819
+ }
803
820
  }
804
821
 
805
822
  /**
@@ -831,9 +848,29 @@ export class GarbageCollector implements IGarbageCollector {
831
848
  };
832
849
  this.unreferencedNodesState.clear();
833
850
 
834
- // If tombstone mode is enabled, update tombstone information and also update all tombstoned nodes in the
835
- // container as per the state in the snapshot data.
836
- if (this.tombstoneMode) {
851
+ // If running sweep, the tombstone state represents the list of nodes that have been deleted during sweep.
852
+ // If running in tombstone mode, the tombstone state represents the list of nodes that have been marked as
853
+ // tombstones.
854
+ // If this call is because we are refreshing from a snapshot due to an ack, it is likely that the GC state
855
+ // in the snapshot is newer than this client's. And so, the deleted / tombstone nodes need to be updated.
856
+ if (this.shouldRunSweep) {
857
+ const snapshotDeletedNodes = snapshotData?.tombstones ? new Set(snapshotData.tombstones) : undefined;
858
+ // If the snapshot contains deleted nodes that are not yet deleted by this client, ask the runtime to
859
+ // delete them.
860
+ if (snapshotDeletedNodes !== undefined) {
861
+ const newDeletedNodes: string[] = [];
862
+ for (const nodeId of snapshotDeletedNodes) {
863
+ if (!this.deletedNodes.has(nodeId)) {
864
+ newDeletedNodes.push(nodeId);
865
+ }
866
+ }
867
+ if (newDeletedNodes.length > 0) {
868
+ // Call container runtime to delete these nodes and add deleted nodes to this.deletedNodes.
869
+ }
870
+ }
871
+ } else if (this.tombstoneMode) {
872
+ // The snapshot may contain more or fewer tombstone nodes than this client. Update tombstone state and
873
+ // notify the runtime to update its state as well.
837
874
  this.tombstones = snapshotData?.tombstones ? Array.from(snapshotData.tombstones) : [];
838
875
  this.runtime.updateTombstonedRoutes(this.tombstones);
839
876
  }
@@ -869,6 +906,7 @@ export class GarbageCollector implements IGarbageCollector {
869
906
  this.latestSummaryData = {
870
907
  serializedGCState: JSON.stringify(generateSortedGCState(snapshotData.gcState)),
871
908
  serializedTombstones: JSON.stringify(snapshotData.tombstones),
909
+ serializedDeletedNodes: JSON.stringify(snapshotData.deletedNodes),
872
910
  };
873
911
  }
874
912
  }
@@ -1020,18 +1058,26 @@ export class GarbageCollector implements IGarbageCollector {
1020
1058
  }
1021
1059
 
1022
1060
  const serializedGCState = JSON.stringify(generateSortedGCState(gcState));
1061
+ // Serialize and write deleted nodes, if any. This is done irrespective of whether sweep is enabled or not so
1062
+ // to identify deleted nodes' usage.
1063
+ const serializedDeletedNodes = this.deletedNodes.size > 0
1064
+ ? JSON.stringify(Array.from(this.deletedNodes).sort())
1065
+ : undefined;
1066
+ // If running in tombstone mode, serialize and write tombstones, if any.
1023
1067
  const serializedTombstones = this.tombstoneMode
1024
1068
  ? (this.tombstones.length > 0 ? JSON.stringify(this.tombstones.sort()) : undefined)
1025
1069
  : undefined;
1026
1070
 
1027
1071
  /**
1028
- * Incremental summary of GC data - If any of the GC state or tombstone state hasn't changed since the last
1029
- * summary, send summary handles for them. Otherwise, send the data in summary blobs.
1072
+ * Incremental summary of GC data - If none of GC state, deleted nodes or tombstones changed since last summary,
1073
+ * write summary handle instead of summary tree for GC.
1074
+ * Otherwise, write the GC summary tree. In the tree, for each of these that changed, write a summary blob and
1075
+ * for each of these that did not change, write a summary handle.
1030
1076
  */
1031
1077
  if (this.trackGCState) {
1032
- this.pendingSummaryData = { serializedGCState, serializedTombstones };
1078
+ this.pendingSummaryData = { serializedGCState, serializedTombstones, serializedDeletedNodes };
1033
1079
  if (trackState && !fullTree && this.latestSummaryData !== undefined) {
1034
- // If neither GC state or tombstone state changed, send a summary handle for the entire GC data.
1080
+ // If nothing changed since last summary, send a summary handle for the entire GC data.
1035
1081
  if (this.latestSummaryData.serializedGCState === serializedGCState
1036
1082
  && this.latestSummaryData.serializedTombstones === serializedTombstones) {
1037
1083
  const stats = mergeStats();
@@ -1046,26 +1092,30 @@ export class GarbageCollector implements IGarbageCollector {
1046
1092
  };
1047
1093
  }
1048
1094
 
1049
- // If either or both of GC state or tombstone state changed, build a GC summary tree.
1050
- return this.buildGCSummaryTree(serializedGCState, serializedTombstones, true /* trackState */);
1095
+ // If some state changed, build a GC summary tree.
1096
+ return this.buildGCSummaryTree(
1097
+ serializedGCState, serializedTombstones, serializedDeletedNodes, true /* trackState */);
1051
1098
  }
1052
1099
  }
1053
1100
  // If not tracking GC state, build a GC summary tree without any summary handles.
1054
- return this.buildGCSummaryTree(serializedGCState, serializedTombstones, false /* trackState */);
1101
+ return this.buildGCSummaryTree(
1102
+ serializedGCState, serializedTombstones, serializedDeletedNodes, false /* trackState */);
1055
1103
  }
1056
1104
 
1057
1105
  /**
1058
- * Builds the GC summary tree which contains GC state and tombstone state.
1059
- * If trackState is false, both GC state and tombstone state are written as summary blobs.
1060
- * If trackState is true, summary blob is written for GC state or tombstone state if they changed.
1106
+ * Builds the GC summary tree which contains GC state, deleted nodes and tombstones.
1107
+ * If trackState is false, all of GC state, deleted nodes and tombstones are written as summary blobs.
1108
+ * If trackState is true, only states that changed are written. Rest are written as handles.
1061
1109
  * @param serializedGCState - The GC state serialized as string.
1062
- * @param serializedTombstones - THe tombstone state serialized as string.
1110
+ * @param serializedTombstones - The tombstone state serialized as string.
1111
+ * @param serializedDeletedNodes - Deleted nodes serialized as string.
1063
1112
  * @param trackState - Whether we are tracking GC state across summaries.
1064
1113
  * @returns the GC summary tree.
1065
1114
  */
1066
1115
  private buildGCSummaryTree(
1067
1116
  serializedGCState: string,
1068
1117
  serializedTombstones: string | undefined,
1118
+ serializedDeletedNodes: string | undefined,
1069
1119
  trackState: boolean,
1070
1120
  ): ISummaryTreeWithStats {
1071
1121
  const gcStateBlobKey = `${gcBlobPrefix}_root`;
@@ -1078,16 +1128,26 @@ export class GarbageCollector implements IGarbageCollector {
1078
1128
  builder.addBlob(gcStateBlobKey, serializedGCState);
1079
1129
  }
1080
1130
 
1081
- // If there is no tombstone data, return only the GC state.
1082
- if (serializedTombstones === undefined) {
1131
+ // If tombstones exist, write a summary handle if it hasn't changed. If it has changed, write a
1132
+ // summary blob.
1133
+ if (serializedTombstones !== undefined) {
1134
+ if (this.latestSummaryData?.serializedTombstones === serializedTombstones && trackState) {
1135
+ builder.addHandle(gcTombstoneBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcTombstoneBlobKey}`);
1136
+ } else {
1137
+ builder.addBlob(gcTombstoneBlobKey, serializedTombstones);
1138
+ }
1139
+ }
1140
+
1141
+ // If there are no deleted nodes, return the summary tree.
1142
+ if (serializedDeletedNodes === undefined) {
1083
1143
  return builder.getSummaryTree();
1084
1144
  }
1085
1145
 
1086
- // If the tombstone state hasn't changed, write a summary handle, else write a summary blob for it.
1087
- if (this.latestSummaryData?.serializedTombstones === serializedTombstones && trackState) {
1088
- builder.addHandle(gcTombstoneBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcTombstoneBlobKey}`);
1146
+ // If the deleted nodes hasn't changed, write a summary handle, else write a summary blob for it.
1147
+ if (this.latestSummaryData?.serializedDeletedNodes === serializedDeletedNodes && trackState) {
1148
+ builder.addHandle(gcDeletedBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcDeletedBlobKey}`);
1089
1149
  } else {
1090
- builder.addBlob(gcTombstoneBlobKey, serializedTombstones);
1150
+ builder.addBlob(gcDeletedBlobKey, serializedDeletedNodes);
1091
1151
  }
1092
1152
  return builder.getSummaryTree();
1093
1153
  }
@@ -1118,9 +1178,8 @@ export class GarbageCollector implements IGarbageCollector {
1118
1178
  * is downloaded and should be used to update the state.
1119
1179
  */
1120
1180
  public async refreshLatestSummary(
1121
- result: RefreshSummaryResult,
1122
1181
  proposalHandle: string | undefined,
1123
- summaryRefSeq: number,
1182
+ result: RefreshSummaryResult,
1124
1183
  readAndParseBlob: ReadAndParseBlob,
1125
1184
  ): Promise<void> {
1126
1185
  // If the latest summary was updated and the summary was tracked, this client is the one that generated this
@@ -1147,8 +1206,8 @@ export class GarbageCollector implements IGarbageCollector {
1147
1206
  }
1148
1207
 
1149
1208
  // If the summary was not tracked by this client, the state should be updated from the downloaded snapshot.
1150
- const snapshot = result.snapshot;
1151
- const metadataBlobId = snapshot.blobs[metadataBlobName];
1209
+ const snapshotTree = result.snapshotTree;
1210
+ const metadataBlobId = snapshotTree.blobs[metadataBlobName];
1152
1211
  if (metadataBlobId) {
1153
1212
  const metadata = await readAndParseBlob<IContainerRuntimeMetadata>(metadataBlobId);
1154
1213
  this.latestSummaryGCVersion = getGCVersion(metadata);
@@ -1162,10 +1221,10 @@ export class GarbageCollector implements IGarbageCollector {
1162
1221
  "No reference timestamp when updating GC state from snapshot",
1163
1222
  "refreshLatestSummary",
1164
1223
  undefined,
1165
- { proposalHandle, summaryRefSeq, details: JSON.stringify(this.configs) },
1224
+ { proposalHandle, summaryRefSeq: result.summaryRefSeq, details: JSON.stringify(this.configs) },
1166
1225
  );
1167
1226
  }
1168
- const gcSnapshotTree = snapshot.trees[gcTreeKey];
1227
+ const gcSnapshotTree = snapshotTree.trees[gcTreeKey];
1169
1228
  // If GC ran in the container that generated this snapshot, it will have a GC tree.
1170
1229
  this.wasGCRunInLatestSummary = gcSnapshotTree !== undefined;
1171
1230
  let latestGCData: IGarbageCollectionSnapshotData | undefined;
@@ -1257,6 +1316,14 @@ export class GarbageCollector implements IGarbageCollector {
1257
1316
  }
1258
1317
  }
1259
1318
 
1319
+ /**
1320
+ * Returns whether a node with the given path has been deleted or not. This can be used by the runtime to identify
1321
+ * cases where objects are used after they are deleted and throw / log errors accordingly.
1322
+ */
1323
+ public isNodeDeleted(nodePath: string): boolean {
1324
+ return this.deletedNodes.has(nodePath);
1325
+ }
1326
+
1260
1327
  public dispose(): void {
1261
1328
  this.sessionExpiryTimer?.clear();
1262
1329
  this.sessionExpiryTimer = undefined;
@@ -1640,7 +1707,7 @@ export class GarbageCollector implements IGarbageCollector {
1640
1707
  }
1641
1708
  }
1642
1709
 
1643
- // If SweepReady Usage Detection is enabed, the handler may close the interactive container.
1710
+ // If SweepReady Usage Detection is enabled, the handler may close the interactive container.
1644
1711
  // Once Sweep is fully implemented, this will be removed since the objects will be gone
1645
1712
  // and errors will arise elsewhere in the runtime
1646
1713
  if (state === UnreferencedState.SweepReady) {
@@ -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.3.1";
9
+ export const pkgVersion = "2.0.0-internal.2.4.1";
package/src/summarizer.ts CHANGED
@@ -160,7 +160,7 @@ export class Summarizer extends EventEmitter implements ISummarizer {
160
160
  // This will result in "summarizerClientDisconnected" stop reason recorded in telemetry,
161
161
  // unless stop() was called earlier
162
162
  this.dispose();
163
- (this.runtime.disposeFn ?? this.runtime.closeFn)()
163
+ (this.runtime.disposeFn ?? this.runtime.closeFn)();
164
164
  }
165
165
 
166
166
  private async runCore(onBehalfOf: string): Promise<SummarizerStopReason> {