@fluidframework/container-runtime 2.0.0-internal.2.2.1 → 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.
Files changed (195) hide show
  1. package/.eslintrc.js +19 -8
  2. package/dist/batchTracker.d.ts +1 -2
  3. package/dist/batchTracker.d.ts.map +1 -1
  4. package/dist/batchTracker.js.map +1 -1
  5. package/dist/blobManager.d.ts +44 -33
  6. package/dist/blobManager.d.ts.map +1 -1
  7. package/dist/blobManager.js +130 -97
  8. package/dist/blobManager.js.map +1 -1
  9. package/dist/containerRuntime.d.ts +39 -8
  10. package/dist/containerRuntime.d.ts.map +1 -1
  11. package/dist/containerRuntime.js +117 -61
  12. package/dist/containerRuntime.js.map +1 -1
  13. package/dist/dataStoreContext.d.ts +1 -1
  14. package/dist/dataStoreContext.d.ts.map +1 -1
  15. package/dist/dataStoreContext.js +4 -3
  16. package/dist/dataStoreContext.js.map +1 -1
  17. package/dist/dataStores.d.ts +9 -6
  18. package/dist/dataStores.d.ts.map +1 -1
  19. package/dist/dataStores.js +30 -24
  20. package/dist/dataStores.js.map +1 -1
  21. package/dist/garbageCollection.d.ts +41 -20
  22. package/dist/garbageCollection.d.ts.map +1 -1
  23. package/dist/garbageCollection.js +205 -151
  24. package/dist/garbageCollection.js.map +1 -1
  25. package/dist/garbageCollectionConstants.d.ts +6 -3
  26. package/dist/garbageCollectionConstants.d.ts.map +1 -1
  27. package/dist/garbageCollectionConstants.js +7 -7
  28. package/dist/garbageCollectionConstants.js.map +1 -1
  29. package/dist/garbageCollectionTombstoneUtils.d.ts +13 -0
  30. package/dist/garbageCollectionTombstoneUtils.d.ts.map +1 -0
  31. package/dist/garbageCollectionTombstoneUtils.js +28 -0
  32. package/dist/garbageCollectionTombstoneUtils.js.map +1 -0
  33. package/dist/index.d.ts +0 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -5
  36. package/dist/index.js.map +1 -1
  37. package/dist/opLifecycle/batchManager.d.ts +13 -1
  38. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  39. package/dist/opLifecycle/batchManager.js +35 -1
  40. package/dist/opLifecycle/batchManager.js.map +1 -1
  41. package/dist/opLifecycle/definitions.d.ts +25 -1
  42. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  43. package/dist/opLifecycle/definitions.js.map +1 -1
  44. package/dist/opLifecycle/index.d.ts +2 -2
  45. package/dist/opLifecycle/index.d.ts.map +1 -1
  46. package/dist/opLifecycle/index.js +2 -1
  47. package/dist/opLifecycle/index.js.map +1 -1
  48. package/dist/opLifecycle/opCompressor.d.ts +1 -1
  49. package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
  50. package/dist/opLifecycle/opCompressor.js +24 -10
  51. package/dist/opLifecycle/opCompressor.js.map +1 -1
  52. package/dist/opLifecycle/opDecompressor.d.ts +2 -1
  53. package/dist/opLifecycle/opDecompressor.d.ts.map +1 -1
  54. package/dist/opLifecycle/opDecompressor.js +30 -17
  55. package/dist/opLifecycle/opDecompressor.js.map +1 -1
  56. package/dist/opLifecycle/opSplitter.d.ts +34 -2
  57. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  58. package/dist/opLifecycle/opSplitter.js +114 -5
  59. package/dist/opLifecycle/opSplitter.js.map +1 -1
  60. package/dist/opLifecycle/outbox.d.ts +5 -0
  61. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  62. package/dist/opLifecycle/outbox.js +24 -14
  63. package/dist/opLifecycle/outbox.js.map +1 -1
  64. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  65. package/dist/opLifecycle/remoteMessageProcessor.js +17 -2
  66. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  67. package/dist/packageVersion.d.ts +1 -1
  68. package/dist/packageVersion.js +1 -1
  69. package/dist/packageVersion.js.map +1 -1
  70. package/dist/runningSummarizer.d.ts.map +1 -1
  71. package/dist/runningSummarizer.js +0 -1
  72. package/dist/runningSummarizer.js.map +1 -1
  73. package/dist/scheduleManager.d.ts +0 -1
  74. package/dist/scheduleManager.d.ts.map +1 -1
  75. package/dist/scheduleManager.js +9 -20
  76. package/dist/scheduleManager.js.map +1 -1
  77. package/dist/summarizer.d.ts +0 -1
  78. package/dist/summarizer.d.ts.map +1 -1
  79. package/dist/summarizer.js +2 -1
  80. package/dist/summarizer.js.map +1 -1
  81. package/dist/summarizerTypes.d.ts +1 -0
  82. package/dist/summarizerTypes.d.ts.map +1 -1
  83. package/dist/summarizerTypes.js.map +1 -1
  84. package/dist/summaryFormat.d.ts.map +1 -1
  85. package/dist/summaryFormat.js +1 -2
  86. package/dist/summaryFormat.js.map +1 -1
  87. package/lib/batchTracker.d.ts +1 -2
  88. package/lib/batchTracker.d.ts.map +1 -1
  89. package/lib/batchTracker.js.map +1 -1
  90. package/lib/blobManager.d.ts +44 -33
  91. package/lib/blobManager.d.ts.map +1 -1
  92. package/lib/blobManager.js +131 -98
  93. package/lib/blobManager.js.map +1 -1
  94. package/lib/containerRuntime.d.ts +39 -8
  95. package/lib/containerRuntime.d.ts.map +1 -1
  96. package/lib/containerRuntime.js +115 -59
  97. package/lib/containerRuntime.js.map +1 -1
  98. package/lib/dataStoreContext.d.ts +1 -1
  99. package/lib/dataStoreContext.d.ts.map +1 -1
  100. package/lib/dataStoreContext.js +5 -4
  101. package/lib/dataStoreContext.js.map +1 -1
  102. package/lib/dataStores.d.ts +9 -6
  103. package/lib/dataStores.d.ts.map +1 -1
  104. package/lib/dataStores.js +32 -26
  105. package/lib/dataStores.js.map +1 -1
  106. package/lib/garbageCollection.d.ts +41 -20
  107. package/lib/garbageCollection.d.ts.map +1 -1
  108. package/lib/garbageCollection.js +201 -147
  109. package/lib/garbageCollection.js.map +1 -1
  110. package/lib/garbageCollectionConstants.d.ts +6 -3
  111. package/lib/garbageCollectionConstants.d.ts.map +1 -1
  112. package/lib/garbageCollectionConstants.js +6 -6
  113. package/lib/garbageCollectionConstants.js.map +1 -1
  114. package/lib/garbageCollectionTombstoneUtils.d.ts +13 -0
  115. package/lib/garbageCollectionTombstoneUtils.d.ts.map +1 -0
  116. package/lib/garbageCollectionTombstoneUtils.js +24 -0
  117. package/lib/garbageCollectionTombstoneUtils.js.map +1 -0
  118. package/lib/index.d.ts +0 -1
  119. package/lib/index.d.ts.map +1 -1
  120. package/lib/index.js +0 -1
  121. package/lib/index.js.map +1 -1
  122. package/lib/opLifecycle/batchManager.d.ts +13 -1
  123. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  124. package/lib/opLifecycle/batchManager.js +35 -1
  125. package/lib/opLifecycle/batchManager.js.map +1 -1
  126. package/lib/opLifecycle/definitions.d.ts +25 -1
  127. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  128. package/lib/opLifecycle/definitions.js.map +1 -1
  129. package/lib/opLifecycle/index.d.ts +2 -2
  130. package/lib/opLifecycle/index.d.ts.map +1 -1
  131. package/lib/opLifecycle/index.js +1 -1
  132. package/lib/opLifecycle/index.js.map +1 -1
  133. package/lib/opLifecycle/opCompressor.d.ts +1 -1
  134. package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
  135. package/lib/opLifecycle/opCompressor.js +24 -10
  136. package/lib/opLifecycle/opCompressor.js.map +1 -1
  137. package/lib/opLifecycle/opDecompressor.d.ts +2 -1
  138. package/lib/opLifecycle/opDecompressor.d.ts.map +1 -1
  139. package/lib/opLifecycle/opDecompressor.js +30 -17
  140. package/lib/opLifecycle/opDecompressor.js.map +1 -1
  141. package/lib/opLifecycle/opSplitter.d.ts +34 -2
  142. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  143. package/lib/opLifecycle/opSplitter.js +112 -4
  144. package/lib/opLifecycle/opSplitter.js.map +1 -1
  145. package/lib/opLifecycle/outbox.d.ts +5 -0
  146. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  147. package/lib/opLifecycle/outbox.js +24 -14
  148. package/lib/opLifecycle/outbox.js.map +1 -1
  149. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  150. package/lib/opLifecycle/remoteMessageProcessor.js +17 -2
  151. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  152. package/lib/packageVersion.d.ts +1 -1
  153. package/lib/packageVersion.js +1 -1
  154. package/lib/packageVersion.js.map +1 -1
  155. package/lib/runningSummarizer.d.ts.map +1 -1
  156. package/lib/runningSummarizer.js +0 -1
  157. package/lib/runningSummarizer.js.map +1 -1
  158. package/lib/scheduleManager.d.ts +0 -1
  159. package/lib/scheduleManager.d.ts.map +1 -1
  160. package/lib/scheduleManager.js +9 -20
  161. package/lib/scheduleManager.js.map +1 -1
  162. package/lib/summarizer.d.ts +0 -1
  163. package/lib/summarizer.d.ts.map +1 -1
  164. package/lib/summarizer.js +2 -1
  165. package/lib/summarizer.js.map +1 -1
  166. package/lib/summarizerTypes.d.ts +1 -0
  167. package/lib/summarizerTypes.d.ts.map +1 -1
  168. package/lib/summarizerTypes.js.map +1 -1
  169. package/lib/summaryFormat.d.ts.map +1 -1
  170. package/lib/summaryFormat.js +1 -2
  171. package/lib/summaryFormat.js.map +1 -1
  172. package/package.json +37 -19
  173. package/src/batchTracker.ts +1 -1
  174. package/src/blobManager.ts +146 -103
  175. package/src/containerRuntime.ts +166 -65
  176. package/src/dataStoreContext.ts +5 -5
  177. package/src/dataStores.ts +40 -30
  178. package/src/garbageCollection.ts +254 -183
  179. package/src/garbageCollectionConstants.ts +7 -6
  180. package/src/garbageCollectionTombstoneUtils.ts +31 -0
  181. package/src/index.ts +0 -5
  182. package/src/opLifecycle/batchManager.ts +59 -1
  183. package/src/opLifecycle/definitions.ts +27 -1
  184. package/src/opLifecycle/index.ts +2 -1
  185. package/src/opLifecycle/opCompressor.ts +29 -12
  186. package/src/opLifecycle/opDecompressor.ts +39 -18
  187. package/src/opLifecycle/opSplitter.ts +141 -7
  188. package/src/opLifecycle/outbox.ts +32 -16
  189. package/src/opLifecycle/remoteMessageProcessor.ts +19 -3
  190. package/src/packageVersion.ts +1 -1
  191. package/src/runningSummarizer.ts +0 -1
  192. package/src/scheduleManager.ts +19 -30
  193. package/src/summarizer.ts +1 -1
  194. package/src/summarizerTypes.ts +1 -0
  195. package/src/summaryFormat.ts +1 -2
@@ -69,6 +69,7 @@ import {
69
69
  } from "@fluidframework/protocol-definitions";
70
70
  import {
71
71
  FlushMode,
72
+ gcTreeKey,
72
73
  InboundAttachMessage,
73
74
  IFluidDataStoreContextDetached,
74
75
  IFluidDataStoreRegistry,
@@ -155,9 +156,6 @@ import {
155
156
  IGarbageCollector,
156
157
  IGCStats,
157
158
  } from "./garbageCollection";
158
- import {
159
- gcTreeKey,
160
- } from "./garbageCollectionConstants";
161
159
  import {
162
160
  channelToDataStore,
163
161
  IDataStoreAliasMessage,
@@ -482,6 +480,25 @@ export interface IContainerRuntimeOptions {
482
480
  * @experimental This config should be driven by the connection with the service and will be moved in the future.
483
481
  */
484
482
  readonly maxBatchSizeInBytes?: number;
483
+ /**
484
+ * If the op payload needs to be chunked in order to work around the maximum size of the batch, this value represents
485
+ * how large the individual chunks will be. This is only supported when compression is enabled.
486
+ *
487
+ * If unspecified, if a batch exceeds `maxBatchSizeInBytes` after compression, the container will close with an instance
488
+ * of `GenericError` with the `BatchTooLarge` message.
489
+ *
490
+ * @experimental Not ready for use.
491
+ */
492
+ readonly chunkSizeInBytes?: number;
493
+ /**
494
+ * If enabled, the runtime will block all attempts to send an op with a different reference sequence number
495
+ * from the previous ops submitted in the same JS turn. This happens when ops are reentrant (an op is created as a
496
+ * response to another op, likely from an event handler).
497
+ *
498
+ * By default, the feature is disabled. If enabled from options, the `Fluid.ContainerRuntime.DisableOpReentryCheck`
499
+ * can be used to disable it at runtime.
500
+ */
501
+ readonly enableOpReentryCheck?: boolean;
485
502
  }
486
503
 
487
504
  /**
@@ -662,6 +679,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
662
679
  compressionAlgorithm: CompressionAlgorithms.lz4
663
680
  },
664
681
  maxBatchSizeInBytes = defaultMaxBatchSizeInBytes,
682
+ chunkSizeInBytes = Number.POSITIVE_INFINITY,
683
+ enableOpReentryCheck = false,
665
684
  } = runtimeOptions;
666
685
 
667
686
  const pendingRuntimeState = context.pendingLocalState as IPendingRuntimeState | undefined;
@@ -719,7 +738,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
719
738
  if (loadSequenceNumberVerification === "log") {
720
739
  logger.sendErrorEvent({ eventName: "SequenceNumberMismatch" }, error);
721
740
  } else {
741
+ // Call both close and dispose as close implementation will no longer dispose runtime in future (2.0.0-internal.3.0.0)
722
742
  context.closeFn(error);
743
+ context.disposeFn?.(error);
723
744
  }
724
745
  }
725
746
  }
@@ -739,6 +760,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
739
760
  enableOfflineLoad,
740
761
  compressionOptions,
741
762
  maxBatchSizeInBytes,
763
+ chunkSizeInBytes,
764
+ enableOpReentryCheck,
742
765
  },
743
766
  containerScope,
744
767
  logger,
@@ -790,8 +813,17 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
790
813
  return this.reSubmit;
791
814
  }
792
815
 
816
+ public get disposeFn(): (error?: ICriticalContainerError) => void {
817
+ // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
818
+ return this.context.disposeFn ?? this.context.closeFn;
819
+ }
820
+
793
821
  public get closeFn(): (error?: ICriticalContainerError) => void {
794
- return this.context.closeFn;
822
+ // Also call disposeFn to retain functionality of runtime being disposed on close
823
+ return (error?: ICriticalContainerError) => {
824
+ this.context.closeFn(error);
825
+ this.context.disposeFn?.(error);
826
+ };
795
827
  }
796
828
 
797
829
  public get flushMode(): FlushMode {
@@ -863,6 +895,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
863
895
 
864
896
  private dirtyContainer: boolean;
865
897
  private emitDirtyDocumentEvent = true;
898
+ private readonly enableOpReentryCheck: boolean;
866
899
 
867
900
  private readonly defaultTelemetrySignalSampleCount = 100;
868
901
  private _perfSignalData: IPerfSignalReport = {
@@ -1010,17 +1043,28 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1010
1043
  this.messageAtLastSummary = metadata?.message;
1011
1044
 
1012
1045
  this._connected = this.context.connected;
1013
- this.remoteMessageProcessor = new RemoteMessageProcessor(new OpSplitter(chunks), new OpDecompressor());
1014
1046
 
1015
- this.handleContext = new ContainerFluidHandleContext("", this);
1047
+ this.mc = loggerToMonitoringContext(ChildLogger.create(this.logger, "ContainerRuntime"));
1048
+
1049
+ const opSplitter = new OpSplitter(
1050
+ chunks,
1051
+ this.context.submitBatchFn,
1052
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableCompressionChunking") === true ?
1053
+ Number.POSITIVE_INFINITY : runtimeOptions.chunkSizeInBytes,
1054
+ runtimeOptions.maxBatchSizeInBytes,
1055
+ this.mc.logger);
1056
+ this.remoteMessageProcessor = new RemoteMessageProcessor(opSplitter, new OpDecompressor());
1016
1057
 
1017
- this.mc = loggerToMonitoringContext(
1018
- ChildLogger.create(this.logger, "ContainerRuntime"));
1058
+ this.handleContext = new ContainerFluidHandleContext("", this);
1019
1059
 
1020
1060
  if (this.summaryConfiguration.state === "enabled") {
1021
1061
  this.validateSummaryHeuristicConfiguration(this.summaryConfiguration);
1022
1062
  }
1023
1063
 
1064
+ this.enableOpReentryCheck = runtimeOptions.enableOpReentryCheck === true
1065
+ // Allow for a break-glass config to override the options
1066
+ && this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableOpReentryCheck") !== true;
1067
+
1024
1068
  this.summariesDisabled = this.isSummariesDisabled();
1025
1069
  this.heuristicsDisabled = this.isHeuristicsDisabled();
1026
1070
  this.summarizerClientElectionEnabled = this.isSummarizerClientElectionEnabled();
@@ -1079,6 +1123,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1079
1123
  // If GC should not run, let the summarizer node know so that it does not track GC state.
1080
1124
  gcDisabled: !this.garbageCollector.shouldRunGC,
1081
1125
  },
1126
+ // Function to get GC data if needed. This will always be called by the root summarizer node to get GC data.
1127
+ async (fullGC?: boolean) => this.getGCDataInternal(fullGC),
1128
+ // Function to get the GC details from the base snapshot we loaded from.
1129
+ async () => this.garbageCollector.getBaseGCDetails(),
1082
1130
  );
1083
1131
 
1084
1132
  if (baseSnapshot) {
@@ -1092,7 +1140,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1092
1140
  (id: string, createParam: CreateChildSummarizerNodeParam) => (
1093
1141
  summarizeInternal: SummarizeInternalFn,
1094
1142
  getGCDataFn: (fullGC?: boolean) => Promise<IGarbageCollectionData>,
1095
- getBaseGCDetailsFn: () => Promise<IGarbageCollectionDetailsBase>,
1143
+ getBaseGCDetailsFn?: () => Promise<IGarbageCollectionDetailsBase>,
1096
1144
  ) => this.summarizerNode.createChild(
1097
1145
  summarizeInternal,
1098
1146
  id,
@@ -1117,12 +1165,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1117
1165
  this.handleContext,
1118
1166
  blobManagerSnapshot,
1119
1167
  () => this.storage,
1120
- (blobId, localId) => {
1168
+ (localId: string, blobId?: string) => {
1121
1169
  if (!this.disposed) {
1122
- this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId, localId });
1170
+ this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { localId, blobId });
1123
1171
  }
1124
1172
  },
1125
1173
  (blobPath: string) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"),
1174
+ (fromPath: string, toPath: string) => this.garbageCollector.addedOutboundReference(fromPath, toPath),
1126
1175
  this,
1127
1176
  pendingRuntimeState?.pendingAttachmentBlobs,
1128
1177
  );
@@ -1147,15 +1196,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1147
1196
  },
1148
1197
  pendingRuntimeState?.pending);
1149
1198
 
1199
+ const compressionOptions = this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableCompression") === true ?
1200
+ {
1201
+ minimumBatchSizeInBytes: Number.POSITIVE_INFINITY,
1202
+ compressionAlgorithm: CompressionAlgorithms.lz4
1203
+ } : runtimeOptions.compressionOptions;
1204
+
1150
1205
  this.outbox = new Outbox({
1151
1206
  shouldSend: () => this.canSendOps(),
1152
1207
  pendingStateManager: this.pendingStateManager,
1153
1208
  containerContext: this.context,
1154
1209
  compressor: new OpCompressor(this.mc.logger),
1210
+ splitter: opSplitter,
1155
1211
  config: {
1156
- compressionOptions: runtimeOptions.compressionOptions,
1212
+ compressionOptions,
1157
1213
  maxBatchSizeInBytes: runtimeOptions.maxBatchSizeInBytes,
1214
+ enableOpReentryCheck: this.enableOpReentryCheck,
1158
1215
  },
1216
+ logger: this.mc.logger,
1159
1217
  });
1160
1218
 
1161
1219
  this.context.quorum.on("removeMember", (clientId: string) => {
@@ -1638,7 +1696,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1638
1696
  if (!this.shouldContinueReconnecting()) {
1639
1697
  this.closeFn(
1640
1698
  DataProcessingError.create(
1641
- // eslint-disable-next-line max-len
1642
1699
  "Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)",
1643
1700
  "setConnectionState",
1644
1701
  undefined,
@@ -1685,7 +1742,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1685
1742
 
1686
1743
  try {
1687
1744
  let localOpMetadata: unknown;
1688
- if (local && runtimeMessage) {
1745
+ if (local && runtimeMessage && message.type !== ContainerMessageType.ChunkedOp) {
1689
1746
  localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1690
1747
  }
1691
1748
 
@@ -1820,9 +1877,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1820
1877
  assert(this.outbox.isEmpty, 0x3cf /* reentrancy */);
1821
1878
  }
1822
1879
 
1823
- public orderSequentially(callback: () => void): void {
1880
+ public orderSequentially<T>(callback: () => T): T {
1824
1881
  let checkpoint: IBatchCheckpoint | undefined;
1825
-
1882
+ let result: T;
1826
1883
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
1827
1884
  // Note: we are not touching this.pendingAttachBatch here, for two reasons:
1828
1885
  // 1. It would not help, as we flush attach ops as they become available.
@@ -1831,7 +1888,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1831
1888
  }
1832
1889
  try {
1833
1890
  this._orderSequentiallyCalls++;
1834
- callback();
1891
+ result = callback();
1835
1892
  } catch (error) {
1836
1893
  if (checkpoint) {
1837
1894
  // This will throw and close the container if rollback fails
@@ -1863,6 +1920,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1863
1920
  if (this.flushMode === FlushMode.Immediate && this._orderSequentiallyCalls === 0) {
1864
1921
  this.flush();
1865
1922
  }
1923
+ return result;
1866
1924
  }
1867
1925
 
1868
1926
  public async createDataStore(pkg: string | string[]): Promise<IDataStore> {
@@ -2112,6 +2170,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2112
2170
  return this.dataStores.updateStateBeforeGC();
2113
2171
  }
2114
2172
 
2173
+ private async getGCDataInternal(fullGC?: boolean): Promise<IGarbageCollectionData> {
2174
+ return this.dataStores.getGCData(fullGC);
2175
+ }
2176
+
2115
2177
  /**
2116
2178
  * Implementation of IGarbageCollectionRuntime::getGCData.
2117
2179
  * Generates and returns the GC data for this container.
@@ -2119,7 +2181,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2119
2181
  */
2120
2182
  public async getGCData(fullGC?: boolean): Promise<IGarbageCollectionData> {
2121
2183
  const builder = new GCDataBuilder();
2122
- const dsGCData = await this.dataStores.getGCData(fullGC);
2184
+ const dsGCData = await this.summarizerNode.getGCData(fullGC);
2123
2185
  builder.addNodes(dsGCData.gcNodes);
2124
2186
 
2125
2187
  const blobsGCData = this.blobManager.getGCData(fullGC);
@@ -2138,40 +2200,28 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2138
2200
  // always referenced, so the used routes is only self-route (empty string).
2139
2201
  this.summarizerNode.updateUsedRoutes([""]);
2140
2202
 
2141
- const blobManagerUsedRoutes: string[] = [];
2142
- const dataStoreUsedRoutes: string[] = [];
2143
- for (const route of usedRoutes) {
2144
- if (this.isBlobPath(route)) {
2145
- blobManagerUsedRoutes.push(route);
2146
- } else {
2147
- dataStoreUsedRoutes.push(route);
2148
- }
2149
- }
2150
-
2151
- this.blobManager.updateUsedRoutes(blobManagerUsedRoutes);
2152
- this.dataStores.updateUsedRoutes(dataStoreUsedRoutes);
2203
+ const { dataStoreRoutes } = this.getDataStoreAndBlobManagerRoutes(usedRoutes);
2204
+ this.dataStores.updateUsedRoutes(dataStoreRoutes);
2153
2205
  }
2154
2206
 
2155
2207
  /**
2156
- * This is called to update objects whose routes are unused. The unused objects are either deleted or marked as
2157
- * tombstones.
2158
- * @param unusedRoutes - The routes that are unused in all data stores and attachment blobs in this Container.
2159
- * @param tombstone - if true, the objects corresponding to unused routes are marked tombstones. Otherwise, they
2160
- * are deleted.
2208
+ * This is called to update objects whose routes are unused.
2209
+ * @param unusedRoutes - Data store and attachment blob routes that are unused in this Container.
2161
2210
  */
2162
- public updateUnusedRoutes(unusedRoutes: string[], tombstone: boolean) {
2163
- const blobManagerUnusedRoutes: string[] = [];
2164
- const dataStoreUnusedRoutes: string[] = [];
2165
- for (const route of unusedRoutes) {
2166
- if (this.isBlobPath(route)) {
2167
- blobManagerUnusedRoutes.push(route);
2168
- } else {
2169
- dataStoreUnusedRoutes.push(route);
2170
- }
2171
- }
2211
+ public updateUnusedRoutes(unusedRoutes: string[]) {
2212
+ const { blobManagerRoutes, dataStoreRoutes } = this.getDataStoreAndBlobManagerRoutes(unusedRoutes);
2213
+ this.blobManager.updateUnusedRoutes(blobManagerRoutes);
2214
+ this.dataStores.updateUnusedRoutes(dataStoreRoutes);
2215
+ }
2172
2216
 
2173
- this.blobManager.updateUnusedRoutes(blobManagerUnusedRoutes, tombstone);
2174
- this.dataStores.updateUnusedRoutes(dataStoreUnusedRoutes, tombstone);
2217
+ /**
2218
+ * This is called to update objects that are tombstones.
2219
+ * @param tombstonedRoutes - Data store and attachment blob routes that are tombstones in this Container.
2220
+ */
2221
+ public updateTombstonedRoutes(tombstonedRoutes: string[]) {
2222
+ const { blobManagerRoutes, dataStoreRoutes } = this.getDataStoreAndBlobManagerRoutes(tombstonedRoutes);
2223
+ this.blobManager.updateTombstonedRoutes(blobManagerRoutes);
2224
+ this.dataStores.updateTombstonedRoutes(dataStoreRoutes);
2175
2225
  }
2176
2226
 
2177
2227
  /**
@@ -2201,7 +2251,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2201
2251
  public async getGCNodePackagePath(nodePath: string): Promise<readonly string[] | undefined> {
2202
2252
  switch (this.getNodeType(nodePath)) {
2203
2253
  case GCNodeType.Blob:
2204
- return ["_blobs"];
2254
+ return [BlobManager.basePath];
2205
2255
  case GCNodeType.DataStore:
2206
2256
  case GCNodeType.SubDataStore:
2207
2257
  return this.dataStores.getDataStorePackagePath(nodePath);
@@ -2221,6 +2271,25 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2221
2271
  return true;
2222
2272
  }
2223
2273
 
2274
+ /**
2275
+ * From a given list of routes, separate and return routes that belong to blob manager and data stores.
2276
+ * @param routes - A list of routes that can belong to data stores or blob manager.
2277
+ * @returns - Two route lists - One that contains routes for blob manager and another one that contains routes
2278
+ * for data stores.
2279
+ */
2280
+ private getDataStoreAndBlobManagerRoutes(routes: string[]) {
2281
+ const blobManagerRoutes: string[] = [];
2282
+ const dataStoreRoutes: string[] = [];
2283
+ for (const route of routes) {
2284
+ if (this.isBlobPath(route)) {
2285
+ blobManagerRoutes.push(route);
2286
+ } else {
2287
+ dataStoreRoutes.push(route);
2288
+ }
2289
+ }
2290
+ return { blobManagerRoutes, dataStoreRoutes };
2291
+ }
2292
+
2224
2293
  /**
2225
2294
  * Runs garbage collection and updates the reference / used state of the nodes in the container.
2226
2295
  * @returns the statistics of the garbage collection run; undefined if GC did not run.
@@ -2315,7 +2384,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2315
2384
  if (this.deltaManager.lastSequenceNumber !== summaryRefSeqNum) {
2316
2385
  return {
2317
2386
  continue: false,
2318
- // eslint-disable-next-line max-len
2319
2387
  error: `lastSequenceNumber changed before uploading to storage. ${this.deltaManager.lastSequenceNumber} !== ${summaryRefSeqNum}`,
2320
2388
  };
2321
2389
  }
@@ -2325,7 +2393,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2325
2393
  if (lastAck !== this.summaryCollection.latestAck) {
2326
2394
  return {
2327
2395
  continue: false,
2328
- // eslint-disable-next-line max-len
2329
2396
  error: `Last summary changed while summarizing. ${this.summaryCollection.latestAck} !== ${lastAck}`,
2330
2397
  };
2331
2398
  }
@@ -2587,7 +2654,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2587
2654
  } else if (!this.flushMicroTaskExists) {
2588
2655
  this.flushMicroTaskExists = true;
2589
2656
  // Queue a microtask to detect the end of the turn and force a flush.
2590
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
2591
2657
  Promise.resolve().then(() => {
2592
2658
  this.flushMicroTaskExists = false;
2593
2659
  this.flush();
@@ -2708,17 +2774,42 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2708
2774
  // It should only be done by the summarizerNode, if required.
2709
2775
  // When fetching from storage we will always get the latest version and do not use the ackHandle.
2710
2776
  const snapshotTreeFetcher = async () => {
2711
- const fetchResult = await this.fetchSnapshotFromStorage(
2712
- null,
2777
+ const fetchResult = await this.fetchLatestSnapshotFromStorage(
2713
2778
  summaryLogger,
2714
2779
  {
2715
2780
  eventName: "RefreshLatestSummaryGetSnapshot",
2716
2781
  ackHandle,
2717
2782
  summaryRefSeq,
2718
2783
  fetchLatest: true,
2719
- });
2784
+ },
2785
+ );
2720
2786
 
2721
2787
  const latestSnapshotRefSeq = await seqFromTree(fetchResult.snapshotTree, readAndParseBlob);
2788
+ /**
2789
+ * If the fetched snapshot is older than the one for which the ack was received, close the container.
2790
+ * This should never happen because an ack should be sent after the latest summary is updated in the server.
2791
+ * However, there are couple of scenarios where it's possible:
2792
+ * 1. A file was modified externally resulting in modifying the snapshot's sequence number. This can lead to
2793
+ * the document being unusable and we should not proceed.
2794
+ * 2. The server DB failed after the ack was sent which may delete the corresponding snapshot. Ideally, in
2795
+ * such cases, the file will be rolled back along with the ack and we will eventually reach a consistent
2796
+ * state.
2797
+ */
2798
+ if (latestSnapshotRefSeq < summaryRefSeq) {
2799
+ const error = DataProcessingError.create(
2800
+ "Fetched snapshot is older than the received ack",
2801
+ "RefreshLatestSummaryAck",
2802
+ undefined /* sequencedMessage */,
2803
+ {
2804
+ ackHandle,
2805
+ summaryRefSeq,
2806
+ latestSnapshotRefSeq,
2807
+ },
2808
+ );
2809
+ this.closeFn(error);
2810
+ throw error;
2811
+ }
2812
+
2722
2813
  summaryLogger.sendTelemetryEvent(
2723
2814
  {
2724
2815
  eventName: "LatestSummaryRetrieved",
@@ -2744,7 +2835,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2744
2835
  );
2745
2836
 
2746
2837
  // Notify the garbage collector so it can update its latest summary state.
2747
- await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
2838
+ await this.garbageCollector.refreshLatestSummary(
2839
+ result,
2840
+ proposalHandle,
2841
+ summaryRefSeq,
2842
+ readAndParseBlob,
2843
+ );
2748
2844
  }
2749
2845
 
2750
2846
  /**
@@ -2756,11 +2852,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2756
2852
  private async refreshLatestSummaryAckFromServer(
2757
2853
  summaryLogger: ITelemetryLogger,
2758
2854
  ): Promise<{ latestSnapshotRefSeq: number; latestSnapshotVersionId: string | undefined; }> {
2759
- const { snapshotTree, versionId } = await this.fetchSnapshotFromStorage(null, summaryLogger, {
2760
- eventName: "RefreshLatestSummaryGetSnapshot",
2761
- fetchLatest: true,
2762
- },
2763
- FetchSource.noCache,
2855
+ const { snapshotTree, versionId } = await this.fetchLatestSnapshotFromStorage(
2856
+ summaryLogger,
2857
+ {
2858
+ eventName: "RefreshLatestSummaryGetSnapshot",
2859
+ fetchLatest: true,
2860
+ },
2764
2861
  );
2765
2862
 
2766
2863
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
@@ -2775,16 +2872,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2775
2872
  );
2776
2873
 
2777
2874
  // Notify the garbage collector so it can update its latest summary state.
2778
- await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
2875
+ await this.garbageCollector.refreshLatestSummary(
2876
+ result,
2877
+ undefined,
2878
+ latestSnapshotRefSeq,
2879
+ readAndParseBlob,
2880
+ )
2779
2881
 
2780
2882
  return { latestSnapshotRefSeq, latestSnapshotVersionId: versionId };
2781
2883
  }
2782
2884
 
2783
- private async fetchSnapshotFromStorage(
2784
- versionId: string | null,
2885
+ private async fetchLatestSnapshotFromStorage(
2785
2886
  logger: ITelemetryLogger,
2786
2887
  event: ITelemetryGenericEvent,
2787
- fetchSource?: FetchSource,
2788
2888
  ): Promise<{ snapshotTree: ISnapshotTree; versionId: string; }> {
2789
2889
  return PerformanceEvent.timedExecAsync(
2790
2890
  logger, event, async (perfEvent: {
@@ -2797,7 +2897,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2797
2897
  const trace = Trace.start();
2798
2898
 
2799
2899
  const versions = await this.storage.getVersions(
2800
- versionId, 1, "refreshLatestSummaryAckFromServer", fetchSource);
2900
+ null, 1, "refreshLatestSummaryAckFromServer", FetchSource.noCache);
2801
2901
  assert(!!versions && !!versions[0], 0x137 /* "Failed to get version from storage" */);
2802
2902
  stats.getVersionDuration = trace.trace().duration;
2803
2903
 
@@ -2960,6 +3060,7 @@ const waitForSeq = async (
2960
3060
  ): Promise<void> => new Promise<void>((resolve, reject) => {
2961
3061
  // TODO: remove cast to any when actual event is determined
2962
3062
  deltaManager.on("closed" as any, reject);
3063
+ deltaManager.on("disposed" as any, reject);
2963
3064
 
2964
3065
  // If we already reached target sequence number, simply resolve the promise.
2965
3066
  if (deltaManager.lastSequenceNumber >= targetSeq) {
@@ -61,7 +61,6 @@ import {
61
61
  import {
62
62
  addBlobToSummary,
63
63
  convertSummaryTreeToITree,
64
- packagePathToTelemetryProperty,
65
64
  } from "@fluidframework/runtime-utils";
66
65
  import {
67
66
  ChildLogger,
@@ -88,6 +87,7 @@ import {
88
87
  getFluidDataStoreAttributes,
89
88
  } from "./summaryFormat";
90
89
  import { throwOnTombstoneUsageKey } from "./garbageCollectionConstants";
90
+ import { sendGCTombstoneEvent } from "./garbageCollectionTombstoneUtils";
91
91
  import { summarizerClientType } from "./summarizerClientElection";
92
92
 
93
93
  function createAttributes(
@@ -782,11 +782,11 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
782
782
 
783
783
  // Always log an error when tombstoned data store is used. However, throw an error only if
784
784
  // throwOnTombstoneUsage is set.
785
- this.mc.logger.sendErrorEvent({
785
+ const event = {
786
786
  eventName: "GC_Tombstone_DataStore_Changed",
787
787
  callSite,
788
- pkg: packagePathToTelemetryProperty(this.pkg),
789
- }, error);
788
+ };
789
+ sendGCTombstoneEvent(this.mc, event, this.clientDetails.type === summarizerClientType, this.pkg, error);
790
790
  // Always log an error when tombstoned data store is used. However, throw an error only if
791
791
  // throwOnTombstoneUsage is set and the client is not a summarizer.
792
792
  if (this.throwOnTombstoneUsage) {
@@ -799,7 +799,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
799
799
  return (
800
800
  summarizeInternal: SummarizeInternalFn,
801
801
  getGCDataFn: (fullGC?: boolean) => Promise<IGarbageCollectionData>,
802
- getBaseGCDetailsFn: () => Promise<IGarbageCollectionDetailsBase>,
802
+ getBaseGCDetailsFn?: () => Promise<IGarbageCollectionDetailsBase>,
803
803
  ) => this.summarizerNode.createChild(
804
804
  summarizeInternal,
805
805
  id,
package/src/dataStores.ts CHANGED
@@ -34,7 +34,6 @@ import {
34
34
  create404Response,
35
35
  createResponseError,
36
36
  responseToException,
37
- packagePathToTelemetryProperty,
38
37
  SummaryTreeBuilder,
39
38
  } from "@fluidframework/runtime-utils";
40
39
  import { ChildLogger, loggerToMonitoringContext, LoggingError, MonitoringContext, TelemetryDataTag } from "@fluidframework/telemetry-utils";
@@ -42,7 +41,7 @@ import { AttachState } from "@fluidframework/container-definitions";
42
41
  import { BlobCacheStorageService, buildSnapshotTree } from "@fluidframework/driver-utils";
43
42
  import { assert, Lazy, LazyPromise } from "@fluidframework/common-utils";
44
43
  import { v4 as uuid } from "uuid";
45
- import { GCDataBuilder, unpackChildNodesUsedRoutes } from "@fluidframework/garbage-collector";
44
+ import { GCDataBuilder, unpackChildNodesGCDetails, unpackChildNodesUsedRoutes } from "@fluidframework/garbage-collector";
46
45
  import { DataStoreContexts } from "./dataStoreContexts";
47
46
  import { ContainerRuntime } from "./containerRuntime";
48
47
  import {
@@ -57,6 +56,7 @@ import { IDataStoreAliasMessage, isDataStoreAliasMessage } from "./dataStore";
57
56
  import { GCNodeType } from "./garbageCollection";
58
57
  import { throwOnTombstoneUsageKey } from "./garbageCollectionConstants";
59
58
  import { summarizerClientType } from "./summarizerClientElection";
59
+ import { sendGCTombstoneEvent } from "./garbageCollectionTombstoneUtils";
60
60
 
61
61
  type PendingAliasResolve = (success: boolean) => void;
62
62
 
@@ -99,7 +99,7 @@ export class DataStores implements IDisposable {
99
99
  (id: string, createParam: CreateChildSummarizerNodeParam) => CreateChildSummarizerNodeFn,
100
100
  private readonly deleteChildSummarizerNodeFn: (id: string) => void,
101
101
  baseLogger: ITelemetryBaseLogger,
102
- getBaseGCDetails: () => Promise<Map<string, IGarbageCollectionDetailsBase>>,
102
+ getBaseGCDetails: () => Promise<IGarbageCollectionDetailsBase>,
103
103
  private readonly gcNodeUpdated: (
104
104
  nodePath: string, timestampMs: number, packagePath?: readonly string[]) => void,
105
105
  private readonly aliasMap: Map<string, string>,
@@ -109,7 +109,8 @@ export class DataStores implements IDisposable {
109
109
  this.containerRuntimeHandle = new FluidObjectHandle(this.runtime, "/", this.runtime.IFluidHandleContext);
110
110
 
111
111
  const baseGCDetailsP = new LazyPromise(async () => {
112
- return getBaseGCDetails();
112
+ const baseGCDetails = await getBaseGCDetails();
113
+ return unpackChildNodesGCDetails(baseGCDetails);
113
114
  });
114
115
  // Returns the base GC details for the data store with the given id.
115
116
  const dataStoreBaseGCDetails = async (dataStoreId: string) => {
@@ -441,12 +442,18 @@ export class DataStores implements IDisposable {
441
442
  // The requested data store is removed by gc. Create a 404 gc response exception.
442
443
  const error = responseToException(createResponseError(404, "Datastore removed by gc", request), request);
443
444
  // Note: if a user writes a request to look like it's viaHandle, we will also send this telemetry event
444
- this.mc.logger.sendErrorEvent({
445
+ const event = {
445
446
  eventName: "GC_Tombstone_DataStore_Requested",
446
447
  url: request.url,
447
- pkg: packagePathToTelemetryProperty(context.isLoaded ? context.packagePath : undefined),
448
448
  viaHandle,
449
- }, error);
449
+ };
450
+ sendGCTombstoneEvent(
451
+ this.mc,
452
+ event,
453
+ this.runtime.clientDetails.type === summarizerClientType,
454
+ context.isLoaded ? context.packagePath : undefined,
455
+ error,
456
+ );
450
457
  // Always log an error when tombstoned data store is used. However, throw an error only if
451
458
  // throwOnTombstoneUsage is set.
452
459
  if (this.throwOnTombstoneUsage) {
@@ -624,11 +631,6 @@ export class DataStores implements IDisposable {
624
631
  // Verify that the used routes are correct.
625
632
  for (const [id] of usedDataStoreRoutes) {
626
633
  assert(this.contexts.has(id), 0x167 /* "Used route does not belong to any known data store" */);
627
-
628
- // Revive datastores regardless of whether or not tombstone the tombstone flag is flipped
629
- const dataStore = this.contexts.get(id);
630
- assert(dataStore !== undefined, 0x46e /* No data store retrieved with specified id */);
631
- dataStore.setTombstone(false /* tombstone */);
632
634
  }
633
635
 
634
636
  // Update the used routes in each data store. Used routes is empty for unused data stores.
@@ -638,13 +640,10 @@ export class DataStores implements IDisposable {
638
640
  }
639
641
 
640
642
  /**
641
- * This is called to update objects whose routes are unused. The unused objects are either deleted or marked as
642
- * tombstones.
643
+ * This is called to update objects whose routes are unused. The unused objects are deleted.
643
644
  * @param unusedRoutes - The routes that are unused in all data stores in this Container.
644
- * @param tombstone - if true, the objects corresponding to unused routes are marked tombstones. Otherwise, they
645
- * are deleted.
646
645
  */
647
- public updateUnusedRoutes(unusedRoutes: string[], tombstone: boolean) {
646
+ public updateUnusedRoutes(unusedRoutes: string[]) {
648
647
  for (const route of unusedRoutes) {
649
648
  const pathParts = route.split("/");
650
649
  // Delete data store only if its route (/datastoreId) is in unusedRoutes. We don't want to delete a data
@@ -654,19 +653,6 @@ export class DataStores implements IDisposable {
654
653
  }
655
654
  const dataStoreId = pathParts[1];
656
655
  assert(this.contexts.has(dataStoreId), 0x2d7 /* No data store with specified id */);
657
-
658
- /**
659
- * When running GC in tombstone mode, datastore contexts are tombstoned. Tombstoned datastore contexts
660
- * enable testing scenarios with accessing deleted content without actually deleting content from
661
- * summaries.
662
- */
663
- if (tombstone) {
664
- const dataStore = this.contexts.get(dataStoreId);
665
- assert(dataStore !== undefined, 0x442 /* No data store retrieved with specified id */);
666
- dataStore.setTombstone(true /* tombstone */);
667
- continue;
668
- }
669
-
670
656
  // Delete the contexts of unused data stores.
671
657
  this.contexts.delete(dataStoreId);
672
658
  // Delete the summarizer node of the unused data stores.
@@ -674,6 +660,30 @@ export class DataStores implements IDisposable {
674
660
  }
675
661
  }
676
662
 
663
+ /**
664
+ * This is called to update objects whose routes are tombstones. Tombstoned datastore contexts enable testing
665
+ * scenarios with accessing deleted content without actually deleting content from summaries.
666
+ * @param tombstonedRoutes - The routes that are tombstones in all data stores in this Container.
667
+ */
668
+ public updateTombstonedRoutes(tombstonedRoutes: string[]) {
669
+ const tombstonedDataStoresSet: Set<string> = new Set();
670
+ for (const route of tombstonedRoutes) {
671
+ const pathParts = route.split("/");
672
+ // Tombstone data store only if its route (/datastoreId) is directly in tombstoneRoutes.
673
+ if (pathParts.length > 2) {
674
+ continue;
675
+ }
676
+ const dataStoreId = pathParts[1];
677
+ assert(this.contexts.has(dataStoreId), 0x510 /* No data store with specified id */);
678
+ tombstonedDataStoresSet.add(dataStoreId);
679
+ }
680
+
681
+ // Update the used routes in each data store. Used routes is empty for unused data stores.
682
+ for (const [contextId, context] of this.contexts) {
683
+ context.setTombstone(tombstonedDataStoresSet.has(contextId));
684
+ }
685
+ }
686
+
677
687
  /**
678
688
  * Returns the outbound routes of this channel. Only root data stores are considered referenced and their paths are
679
689
  * part of outbound routes.