@fluidframework/container-runtime 0.58.3000-61081 → 0.59.2000-61729

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 (93) hide show
  1. package/dist/blobManager.d.ts +13 -1
  2. package/dist/blobManager.d.ts.map +1 -1
  3. package/dist/blobManager.js +52 -0
  4. package/dist/blobManager.js.map +1 -1
  5. package/dist/connectionTelemetry.js +8 -8
  6. package/dist/connectionTelemetry.js.map +1 -1
  7. package/dist/containerRuntime.d.ts +27 -3
  8. package/dist/containerRuntime.d.ts.map +1 -1
  9. package/dist/containerRuntime.js +100 -14
  10. package/dist/containerRuntime.js.map +1 -1
  11. package/dist/dataStore.js +8 -1
  12. package/dist/dataStore.js.map +1 -1
  13. package/dist/dataStoreContext.d.ts +9 -3
  14. package/dist/dataStoreContext.d.ts.map +1 -1
  15. package/dist/dataStoreContext.js +22 -6
  16. package/dist/dataStoreContext.js.map +1 -1
  17. package/dist/dataStores.d.ts +13 -5
  18. package/dist/dataStores.d.ts.map +1 -1
  19. package/dist/dataStores.js +39 -18
  20. package/dist/dataStores.js.map +1 -1
  21. package/dist/deltaScheduler.d.ts +4 -5
  22. package/dist/deltaScheduler.d.ts.map +1 -1
  23. package/dist/deltaScheduler.js +54 -35
  24. package/dist/deltaScheduler.js.map +1 -1
  25. package/dist/garbageCollection.d.ts +31 -27
  26. package/dist/garbageCollection.d.ts.map +1 -1
  27. package/dist/garbageCollection.js +76 -75
  28. package/dist/garbageCollection.js.map +1 -1
  29. package/dist/orderedClientElection.d.ts.map +1 -1
  30. package/dist/orderedClientElection.js +2 -2
  31. package/dist/orderedClientElection.js.map +1 -1
  32. package/dist/packageVersion.d.ts +1 -1
  33. package/dist/packageVersion.js +1 -1
  34. package/dist/packageVersion.js.map +1 -1
  35. package/dist/summarizerClientElection.d.ts.map +1 -1
  36. package/dist/summarizerClientElection.js +8 -0
  37. package/dist/summarizerClientElection.js.map +1 -1
  38. package/dist/summaryGenerator.d.ts.map +1 -1
  39. package/dist/summaryGenerator.js +2 -3
  40. package/dist/summaryGenerator.js.map +1 -1
  41. package/lib/blobManager.d.ts +13 -1
  42. package/lib/blobManager.d.ts.map +1 -1
  43. package/lib/blobManager.js +52 -0
  44. package/lib/blobManager.js.map +1 -1
  45. package/lib/connectionTelemetry.js +8 -8
  46. package/lib/connectionTelemetry.js.map +1 -1
  47. package/lib/containerRuntime.d.ts +27 -3
  48. package/lib/containerRuntime.d.ts.map +1 -1
  49. package/lib/containerRuntime.js +101 -15
  50. package/lib/containerRuntime.js.map +1 -1
  51. package/lib/dataStore.js +8 -1
  52. package/lib/dataStore.js.map +1 -1
  53. package/lib/dataStoreContext.d.ts +9 -3
  54. package/lib/dataStoreContext.d.ts.map +1 -1
  55. package/lib/dataStoreContext.js +22 -6
  56. package/lib/dataStoreContext.js.map +1 -1
  57. package/lib/dataStores.d.ts +13 -5
  58. package/lib/dataStores.d.ts.map +1 -1
  59. package/lib/dataStores.js +39 -18
  60. package/lib/dataStores.js.map +1 -1
  61. package/lib/deltaScheduler.d.ts +4 -5
  62. package/lib/deltaScheduler.d.ts.map +1 -1
  63. package/lib/deltaScheduler.js +54 -35
  64. package/lib/deltaScheduler.js.map +1 -1
  65. package/lib/garbageCollection.d.ts +31 -27
  66. package/lib/garbageCollection.d.ts.map +1 -1
  67. package/lib/garbageCollection.js +75 -74
  68. package/lib/garbageCollection.js.map +1 -1
  69. package/lib/orderedClientElection.d.ts.map +1 -1
  70. package/lib/orderedClientElection.js +2 -2
  71. package/lib/orderedClientElection.js.map +1 -1
  72. package/lib/packageVersion.d.ts +1 -1
  73. package/lib/packageVersion.js +1 -1
  74. package/lib/packageVersion.js.map +1 -1
  75. package/lib/summarizerClientElection.d.ts.map +1 -1
  76. package/lib/summarizerClientElection.js +8 -0
  77. package/lib/summarizerClientElection.js.map +1 -1
  78. package/lib/summaryGenerator.d.ts.map +1 -1
  79. package/lib/summaryGenerator.js +2 -3
  80. package/lib/summaryGenerator.js.map +1 -1
  81. package/package.json +33 -21
  82. package/src/blobManager.ts +60 -1
  83. package/src/connectionTelemetry.ts +9 -9
  84. package/src/containerRuntime.ts +106 -17
  85. package/src/dataStore.ts +7 -1
  86. package/src/dataStoreContext.ts +22 -7
  87. package/src/dataStores.ts +40 -19
  88. package/src/deltaScheduler.ts +65 -39
  89. package/src/garbageCollection.ts +92 -78
  90. package/src/orderedClientElection.ts +2 -1
  91. package/src/packageVersion.ts +1 -1
  92. package/src/summarizerClientElection.ts +8 -0
  93. package/src/summaryGenerator.ts +1 -7
@@ -10,7 +10,6 @@ import {
10
10
  FluidObject,
11
11
  IFluidHandle,
12
12
  IFluidHandleContext,
13
- IFluidObject,
14
13
  IFluidRouter,
15
14
  IRequest,
16
15
  IResponse,
@@ -101,6 +100,7 @@ import {
101
100
  seqFromTree,
102
101
  calculateStats,
103
102
  } from "@fluidframework/runtime-utils";
103
+ import { GCDataBuilder } from "@fluidframework/garbage-collector";
104
104
  import { v4 as uuid } from "uuid";
105
105
  import { ContainerFluidHandleContext } from "./containerHandleContext";
106
106
  import { FluidDataStoreRegistry } from "./dataStoreRegistry";
@@ -141,6 +141,7 @@ import { formExponentialFn, Throttler } from "./throttler";
141
141
  import { RunWhileConnectedCoordinator } from "./runWhileConnectedCoordinator";
142
142
  import {
143
143
  GarbageCollector,
144
+ GCNodeType,
144
145
  gcTreeKey,
145
146
  IGarbageCollectionRuntime,
146
147
  IGarbageCollector,
@@ -637,7 +638,7 @@ export class ScheduleManager {
637
638
 
638
639
  // This could be the beginning of a new batch or an individual message.
639
640
  this.emitter.emit("batchBegin", message);
640
- this.deltaScheduler.batchBegin();
641
+ this.deltaScheduler.batchBegin(message);
641
642
 
642
643
  const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
643
644
  if (batch) {
@@ -658,7 +659,7 @@ export class ScheduleManager {
658
659
  this.hitError = true;
659
660
  this.batchClientId = undefined;
660
661
  this.emitter.emit("batchEnd", error, message);
661
- this.deltaScheduler.batchEnd();
662
+ this.deltaScheduler.batchEnd(message);
662
663
  return;
663
664
  }
664
665
 
@@ -668,7 +669,7 @@ export class ScheduleManager {
668
669
  if (this.batchClientId === undefined || batch === false) {
669
670
  this.batchClientId = undefined;
670
671
  this.emitter.emit("batchEnd", undefined, message);
671
- this.deltaScheduler.batchEnd();
672
+ this.deltaScheduler.batchEnd(message);
672
673
  return;
673
674
  }
674
675
  }
@@ -907,7 +908,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
907
908
  return this._flushMode;
908
909
  }
909
910
 
910
- public get scope(): IFluidObject & FluidObject {
911
+ public get scope(): FluidObject {
911
912
  return this.containerScope;
912
913
  }
913
914
 
@@ -1076,13 +1077,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1076
1077
  this.garbageCollector = GarbageCollector.create(
1077
1078
  this,
1078
1079
  this.runtimeOptions.gcOptions,
1079
- (unusedRoutes: string[]) => this.dataStores.deleteUnusedRoutes(unusedRoutes),
1080
- (nodePath: string) => this.dataStores.getNodePackagePath(nodePath),
1081
- /**
1082
- * Returns the timestamp of the last message seen by this client. This is used by garbage collector as
1083
- * the current reference timestamp for tracking unreferenced objects.
1084
- */
1085
- () => this.deltaManager.lastMessage?.timestamp ?? this.messageAtLastSummary?.timestamp,
1080
+ (nodePath: string) => this.getGCNodePackagePath(nodePath),
1086
1081
  () => this.messageAtLastSummary?.timestamp,
1087
1082
  context.baseSnapshot,
1088
1083
  async <T>(id: string) => readAndParse<T>(this.storage, id),
@@ -1134,7 +1129,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1134
1129
  ),
1135
1130
  (id: string) => this.summarizerNode.deleteChild(id),
1136
1131
  this.mc.logger,
1137
- async () => this.garbageCollector.getDataStoreBaseGCDetails(),
1132
+ async () => this.garbageCollector.getBaseGCDetails(),
1138
1133
  (path: string, timestampMs: number, packagePath?: readonly string[]) => this.garbageCollector.nodeUpdated(
1139
1134
  path,
1140
1135
  "Changed",
@@ -1833,7 +1828,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1833
1828
  */
1834
1829
  private async createRootDataStoreLegacy(pkg: string | string[], rootDataStoreId: string): Promise<IFluidRouter> {
1835
1830
  const fluidDataStore = await this._createDataStore(pkg, true /* isRoot */, rootDataStoreId);
1836
- fluidDataStore.bindToContext();
1831
+ // back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel. For
1832
+ // older versions, we still have to call bindToContext.
1833
+ if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
1834
+ fluidDataStore.makeVisibleAndAttachGraph();
1835
+ } else {
1836
+ fluidDataStore.bindToContext();
1837
+ }
1837
1838
  return fluidDataStore;
1838
1839
  }
1839
1840
 
@@ -1905,7 +1906,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1905
1906
  const fluidDataStore = await this.dataStores._createFluidDataStoreContext(
1906
1907
  Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props).realize();
1907
1908
  if (isRoot) {
1908
- fluidDataStore.bindToContext();
1909
+ // back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel.
1910
+ // For older versions, we still have to call bindToContext.
1911
+ if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
1912
+ fluidDataStore.makeVisibleAndAttachGraph();
1913
+ } else {
1914
+ fluidDataStore.bindToContext();
1915
+ }
1909
1916
  this.logger.sendTelemetryEvent({
1910
1917
  eventName: "Root datastore with props",
1911
1918
  hasProps: props !== undefined,
@@ -2111,7 +2118,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2111
2118
  * @param fullGC - true to bypass optimizations and force full generation of GC data.
2112
2119
  */
2113
2120
  public async getGCData(fullGC?: boolean): Promise<IGarbageCollectionData> {
2114
- return this.dataStores.getGCData(fullGC);
2121
+ const builder = new GCDataBuilder();
2122
+ const dsGCData = await this.dataStores.getGCData(fullGC);
2123
+ builder.addNodes(dsGCData.gcNodes);
2124
+
2125
+ const blobsGCData = this.blobManager.getGCData(fullGC);
2126
+ builder.addNodes(blobsGCData.gcNodes);
2127
+ return builder.getGCData();
2115
2128
  }
2116
2129
 
2117
2130
  /**
@@ -2127,7 +2140,83 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2127
2140
  // always referenced, so the used routes is only self-route (empty string).
2128
2141
  this.summarizerNode.updateUsedRoutes([""]);
2129
2142
 
2130
- return this.dataStores.updateUsedRoutes(usedRoutes, gcTimestamp);
2143
+ const dataStoreUsedRoutes: string[] = [];
2144
+ for (const route of usedRoutes) {
2145
+ if (route.split("/")[1] !== BlobManager.basePath) {
2146
+ dataStoreUsedRoutes.push(route);
2147
+ }
2148
+ }
2149
+
2150
+ return this.dataStores.updateUsedRoutes(dataStoreUsedRoutes, gcTimestamp);
2151
+ }
2152
+
2153
+ /**
2154
+ * When running GC in test mode, this is called to delete objects whose routes are unused. This enables testing
2155
+ * scenarios with accessing deleted content.
2156
+ * @param unusedRoutes - The routes that are unused in all data stores in this Container.
2157
+ */
2158
+ public deleteUnusedRoutes(unusedRoutes: string[]) {
2159
+ const blobManagerUnusedRoutes: string[] = [];
2160
+ const dataStoreUnusedRoutes: string[] = [];
2161
+ for (const route of unusedRoutes) {
2162
+ if (this.isBlobPath(route)) {
2163
+ blobManagerUnusedRoutes.push(route);
2164
+ } else {
2165
+ dataStoreUnusedRoutes.push(route);
2166
+ }
2167
+ }
2168
+
2169
+ this.blobManager.deleteUnusedRoutes(blobManagerUnusedRoutes);
2170
+ this.dataStores.deleteUnusedRoutes(dataStoreUnusedRoutes);
2171
+ }
2172
+
2173
+ /**
2174
+ * Returns a server generated referenced timestamp to be used to track unreferenced nodes by GC.
2175
+ */
2176
+ public getCurrentReferenceTimestampMs(): number | undefined {
2177
+ // Use the timestamp of the last message seen by this client as that is server generated. If no messages have
2178
+ // been processed, use the timestamp of the message from the last summary.
2179
+ return this.deltaManager.lastMessage?.timestamp ?? this.messageAtLastSummary?.timestamp;
2180
+ }
2181
+
2182
+ /**
2183
+ * Returns the type of the GC node. Currently, there are nodes that belong to data store and nodes that belong
2184
+ * to the blob manager.
2185
+ */
2186
+ public getNodeType(nodePath: string): GCNodeType {
2187
+ if (this.isBlobPath(nodePath)) {
2188
+ return GCNodeType.Blob;
2189
+ }
2190
+ if (this.dataStores.isDataStoreNode(nodePath)) {
2191
+ return GCNodeType.DataStore;
2192
+ }
2193
+ // Root node ("/") and DDS nodes belong to "Other" node types.
2194
+ return GCNodeType.Other;
2195
+ }
2196
+
2197
+ /**
2198
+ * Called by GC to retrieve the package path of the node with the given path. The node should belong to a
2199
+ * data store or an attachment blob.
2200
+ */
2201
+ public getGCNodePackagePath(nodePath: string): readonly string[] | undefined {
2202
+ // If the node is a blob, return "_blobs" as the package path.
2203
+ if (this.isBlobPath(nodePath)) {
2204
+ return ["_blobs"];
2205
+ }
2206
+ const dataStorePkgPath = this.dataStores.getDataStorePackagePath(nodePath);
2207
+ assert(dataStorePkgPath !== undefined, 0x2d6 /* "Package path requested for unknown node type." */);
2208
+ return dataStorePkgPath;
2209
+ }
2210
+
2211
+ /**
2212
+ * Returns whether a given path is for attachment blobs that are in the format - "/BlobManager.basePath/...".
2213
+ */
2214
+ private isBlobPath(path: string): boolean {
2215
+ const pathParts = path.split("/");
2216
+ if (pathParts.length < 2 || pathParts[1] !== BlobManager.basePath) {
2217
+ return false;
2218
+ }
2219
+ return true;
2131
2220
  }
2132
2221
 
2133
2222
  /**
package/src/dataStore.ts CHANGED
@@ -75,7 +75,13 @@ class DataStore implements IDataStore {
75
75
  alias,
76
76
  };
77
77
 
78
- this.fluidDataStoreChannel.bindToContext();
78
+ // back-compat 0.58.2000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel. For
79
+ // older versions, we still have to call bindToContext.
80
+ if (this.fluidDataStoreChannel.makeVisibleAndAttachGraph !== undefined) {
81
+ this.fluidDataStoreChannel.makeVisibleAndAttachGraph();
82
+ } else {
83
+ this.fluidDataStoreChannel.bindToContext();
84
+ }
79
85
 
80
86
  if (this.runtime.attachState === AttachState.Detached) {
81
87
  const localResult = this.datastores.processAliasMessageCore(message);
@@ -132,7 +132,7 @@ export interface ILocalFluidDataStoreContextProps extends IFluidDataStoreContext
132
132
  readonly pkg: Readonly<string[]> | undefined;
133
133
  readonly snapshotTree: ISnapshotTree | undefined;
134
134
  readonly isRootDataStore: boolean | undefined;
135
- readonly bindChannelFn: (channel: IFluidDataStoreChannel) => void;
135
+ readonly makeLocallyVisibleFn: () => void;
136
136
  /**
137
137
  * @deprecated 0.16 Issue #1635, #3631
138
138
  */
@@ -261,7 +261,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
261
261
  private readonly existing: boolean,
262
262
  private bindState: BindState,
263
263
  public readonly isLocalDataStore: boolean,
264
- bindChannelFn: (channel: IFluidDataStoreChannel) => void,
264
+ private readonly makeLocallyVisibleFn: () => void,
265
265
  ) {
266
266
  super();
267
267
 
@@ -284,7 +284,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
284
284
  assert(this.bindState === BindState.NotBound, 0x13b /* "datastore context is already in bound state" */);
285
285
  this.bindState = BindState.Binding;
286
286
  assert(this.channel !== undefined, 0x13c /* "undefined channel on datastore context" */);
287
- bindChannelFn(this.channel);
287
+ this.makeLocallyVisible();
288
288
  this.bindState = BindState.Bound;
289
289
  };
290
290
 
@@ -627,6 +627,15 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
627
627
  return this._containerRuntime.submitDataStoreSignal(this.id, type, content);
628
628
  }
629
629
 
630
+ /**
631
+ * This is called by the data store channel when it becomes locally visible indicating that it is ready to become
632
+ * globally visible now.
633
+ */
634
+ public makeLocallyVisible() {
635
+ assert(this.channel !== undefined, 0x2cf /* "undefined channel on datastore context" */);
636
+ this.makeLocallyVisibleFn();
637
+ }
638
+
630
639
  protected bindRuntime(channel: IFluidDataStoreChannel) {
631
640
  if (this.channel) {
632
641
  throw new Error("Runtime already bound");
@@ -864,7 +873,7 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
864
873
  props.snapshotTree !== undefined ? true : false /* existing */,
865
874
  props.snapshotTree ? BindState.Bound : BindState.NotBound,
866
875
  true /* isLocalDataStore */,
867
- props.bindChannelFn,
876
+ props.makeLocallyVisibleFn,
868
877
  );
869
878
 
870
879
  this.snapshotTree = props.snapshotTree;
@@ -993,7 +1002,7 @@ export class LocalDetachedFluidDataStoreContext
993
1002
 
994
1003
  public async attachRuntime(
995
1004
  registry: IProvideFluidDataStoreFactory,
996
- dataStoreRuntime: IFluidDataStoreChannel)
1005
+ dataStoreChannel: IFluidDataStoreChannel)
997
1006
  {
998
1007
  assert(this.detachedRuntimeCreation, 0x154 /* "runtime creation is already attached" */);
999
1008
  assert(this.channelDeferred === undefined, 0x155 /* "channel deferral is already set" */);
@@ -1009,10 +1018,16 @@ export class LocalDetachedFluidDataStoreContext
1009
1018
  this.detachedRuntimeCreation = false;
1010
1019
  this.channelDeferred = new Deferred<IFluidDataStoreChannel>();
1011
1020
 
1012
- super.bindRuntime(dataStoreRuntime);
1021
+ super.bindRuntime(dataStoreChannel);
1013
1022
 
1014
1023
  if (await this.isRoot()) {
1015
- dataStoreRuntime.bindToContext();
1024
+ // back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel.
1025
+ // For older versions, we still have to call bindToContext.
1026
+ if (dataStoreChannel.makeVisibleAndAttachGraph !== undefined) {
1027
+ dataStoreChannel.makeVisibleAndAttachGraph();
1028
+ } else {
1029
+ dataStoreChannel.bindToContext();
1030
+ }
1016
1031
  }
1017
1032
  }
1018
1033
 
package/src/dataStores.ts CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  CreateSummarizerNodeSource,
19
19
  IAttachMessage,
20
20
  IEnvelope,
21
- IFluidDataStoreChannel,
22
21
  IFluidDataStoreContextDetached,
23
22
  IGarbageCollectionData,
24
23
  IGarbageCollectionDetailsBase,
@@ -157,7 +156,7 @@ export class DataStores implements IDisposable {
157
156
  key,
158
157
  { type: CreateSummarizerNodeSource.FromSummary },
159
158
  ),
160
- bindChannelFn: (cr: IFluidDataStoreChannel) => this.bindFluidDataStore(cr),
159
+ makeLocallyVisibleFn: () => this.makeDataStoreLocallyVisible(key),
161
160
  snapshotTree,
162
161
  isRootDataStore: undefined,
163
162
  writeGCDataAtRoot: this.writeGCDataAtRoot,
@@ -291,14 +290,20 @@ export class DataStores implements IDisposable {
291
290
  return this.aliasMap.get(id) !== undefined || this.contexts.get(id) !== undefined;
292
291
  }
293
292
 
294
- public bindFluidDataStore(fluidDataStoreRuntime: IFluidDataStoreChannel): void {
295
- const id = fluidDataStoreRuntime.id;
293
+ /**
294
+ * Make the data stores locally visible in the container graph by moving the data store context from unbound to
295
+ * bound list. This data store can now be reached from the root.
296
+ * @param id - The id of the data store context to make visible.
297
+ */
298
+ private makeDataStoreLocallyVisible(id: string): void {
296
299
  const localContext = this.contexts.getUnbound(id);
297
300
  assert(!!localContext, 0x15f /* "Could not find unbound context to bind" */);
298
301
 
299
- // If the container is detached, we don't need to send OP or add to pending attach because
300
- // we will summarize it while uploading the create new summary and make it known to other
301
- // clients.
302
+ /**
303
+ * If the container is not detached, it is globally visible to all clients. This data store should also be
304
+ * globally visible. Move it to attaching state and send an "attach" op for it.
305
+ * If the container is detached, this data store will be part of the summary that makes the container attached.
306
+ */
302
307
  if (this.runtime.attachState !== AttachState.Detached) {
303
308
  localContext.emit("attaching");
304
309
  const message = localContext.generateAttachMessage();
@@ -308,7 +313,7 @@ export class DataStores implements IDisposable {
308
313
  this.attachOpFiredForDataStore.add(id);
309
314
  }
310
315
 
311
- this.contexts.bind(fluidDataStoreRuntime.id);
316
+ this.contexts.bind(id);
312
317
  }
313
318
 
314
319
  public createDetachedDataStoreCore(
@@ -326,7 +331,7 @@ export class DataStores implements IDisposable {
326
331
  id,
327
332
  { type: CreateSummarizerNodeSource.Local },
328
333
  ),
329
- bindChannelFn: (cr: IFluidDataStoreChannel) => this.bindFluidDataStore(cr),
334
+ makeLocallyVisibleFn: () => this.makeDataStoreLocallyVisible(id),
330
335
  snapshotTree: undefined,
331
336
  isRootDataStore: isRoot,
332
337
  writeGCDataAtRoot: this.writeGCDataAtRoot,
@@ -347,7 +352,7 @@ export class DataStores implements IDisposable {
347
352
  id,
348
353
  { type: CreateSummarizerNodeSource.Local },
349
354
  ),
350
- bindChannelFn: (cr: IFluidDataStoreChannel) => this.bindFluidDataStore(cr),
355
+ makeLocallyVisibleFn: () => this.makeDataStoreLocallyVisible(id),
351
356
  snapshotTree: undefined,
352
357
  isRootDataStore: isRoot,
353
358
  writeGCDataAtRoot: this.writeGCDataAtRoot,
@@ -587,7 +592,14 @@ export class DataStores implements IDisposable {
587
592
  */
588
593
  public deleteUnusedRoutes(unusedRoutes: string[]) {
589
594
  for (const route of unusedRoutes) {
590
- const dataStoreId = route.split("/")[1];
595
+ const pathParts = route.split("/");
596
+ // Delete data store only if its route (/datastoreId) is in unusedRoutes. We don't want to delete a data
597
+ // store based on its DDS being unused.
598
+ if (pathParts.length > 2) {
599
+ continue;
600
+ }
601
+ const dataStoreId = pathParts[1];
602
+ assert(this.contexts.has(dataStoreId), 0x2d7 /* `${dataStoreId} is not a data store` */);
591
603
  // Delete the contexts of unused data stores.
592
604
  this.contexts.delete(dataStoreId);
593
605
  // Delete the summarizer node of the unused data stores.
@@ -611,15 +623,24 @@ export class DataStores implements IDisposable {
611
623
  }
612
624
 
613
625
  /**
614
- * Returns the package path of the node with the given path. This is used by GC to log when an inactive / deleted
615
- * node is used.
626
+ * Called during GC to retrieve the package path of a data store node with the given path.
627
+ */
628
+ public getDataStorePackagePath(nodePath: string): readonly string[] | undefined {
629
+ // If the node belongs to a data store, return its package path if the data store is loaded. For DDSs, we return
630
+ // the package path of the data store that contains it.
631
+ const context = this.contexts.get(nodePath.split("/")[1]);
632
+ return context?.isLoaded ? context.packagePath : undefined;
633
+ }
634
+
635
+ /**
636
+ * Called by GC to know if a node is a data store or not. Data store ids are of the format "/dataStoreId".
616
637
  */
617
- public getNodePackagePath(nodePath: string): readonly string[] | undefined {
618
- // Currently, only return the data store package path for the node since GC is only interested in data stores.
619
- const dataStoreId = nodePath.split("/")[1];
620
- const context = this.contexts.get(dataStoreId);
621
- assert(context !== undefined, 0x2b9 /* "Data store with given id does not exist" */);
622
- return context.isLoaded ? context.packagePath : undefined;
638
+ public isDataStoreNode(nodePath: string): boolean {
639
+ const pathParts = nodePath.split("/");
640
+ if (pathParts.length === 2 && this.contexts.has(pathParts[1])) {
641
+ return true;
642
+ }
643
+ return false;
623
644
  }
624
645
  }
625
646
 
@@ -11,6 +11,9 @@ import {
11
11
  ISequencedDocumentMessage,
12
12
  } from "@fluidframework/protocol-definitions";
13
13
 
14
+ import {
15
+ TelemetryLogger,
16
+ } from "@fluidframework/telemetry-utils";
14
17
  /**
15
18
  * DeltaScheduler is responsible for the scheduling of inbound delta queue in cases where there
16
19
  * is more than one op a particular run of the queue. It does not schedule if there is just one
@@ -25,18 +28,13 @@ import {
25
28
  export class DeltaScheduler {
26
29
  private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>;
27
30
  // The time for processing ops in a single turn.
28
- public static readonly processingTime = 20;
31
+ public static readonly processingTime = 50;
29
32
 
30
33
  // The increase in time for processing ops after each turn.
31
34
  private readonly processingTimeIncrement = 10;
32
35
 
33
36
  private processingStartTime: number | undefined;
34
- private totalProcessingTime: number = DeltaScheduler.processingTime;
35
-
36
- // This keeps track of whether the delta scheduler is scheduling a particular run of the
37
- // the inbound delta queue. Basically, every time the delta queue starts processing with
38
- // more than one op, this will be set to true until the run completes.
39
- private isScheduling: boolean = false;
37
+ private currentAllowedProcessingTimeForTurn: number = DeltaScheduler.processingTime;
40
38
 
41
39
  // This keeps track of the number of times inbound queue has been scheduled. After a particular
42
40
  // count, we log telemetry for the number of ops processed, the time and number of turns it took
@@ -44,9 +42,13 @@ export class DeltaScheduler {
44
42
  private schedulingCount: number = 0;
45
43
 
46
44
  private schedulingLog: {
47
- numberOfOps: number;
45
+ opsRemainingToProcess: number;
48
46
  totalProcessingTime: number;
49
47
  numberOfTurns: number;
48
+ numberOfBatchesProcessed: number;
49
+ lastSequenceNumber: number;
50
+ firstSequenceNumber: number;
51
+ startTime: number;
50
52
  } | undefined;
51
53
 
52
54
  constructor(
@@ -57,50 +59,72 @@ export class DeltaScheduler {
57
59
  this.deltaManager.inbound.on("idle", () => { this.inboundQueueIdle(); });
58
60
  }
59
61
 
60
- public batchBegin() {
62
+ public batchBegin(message: ISequencedDocumentMessage) {
61
63
  if (!this.processingStartTime) {
62
64
  this.processingStartTime = performance.now();
63
65
  }
66
+ if (this.schedulingLog === undefined && this.schedulingCount % 500 === 0) {
67
+ // Every 500th time we are scheduling the inbound queue, we log telemetry for the
68
+ // number of ops processed, the time and number of turns it took to process the ops.
69
+ this.schedulingLog = {
70
+ opsRemainingToProcess: 0,
71
+ numberOfTurns: 1,
72
+ totalProcessingTime: 0,
73
+ numberOfBatchesProcessed: 0,
74
+ firstSequenceNumber: message.sequenceNumber,
75
+ lastSequenceNumber: message.sequenceNumber,
76
+ startTime: performance.now(),
77
+ };
78
+ }
64
79
  }
65
80
 
66
- public batchEnd() {
67
- if (this.shouldRunScheduler()) {
68
- if (!this.isScheduling) {
69
- this.isScheduling = true;
70
- // Every 2000th time we are scheduling the inbound queue, we log telemetry for the
71
- // number of ops processed, the time and number of turns it took to process the ops.
72
- if (this.schedulingCount % 2000 === 0) {
73
- this.schedulingLog = {
74
- numberOfOps: this.deltaManager.inbound.length,
75
- numberOfTurns: 1,
76
- totalProcessingTime: 0,
77
- };
78
- }
79
- }
81
+ public batchEnd(message: ISequencedDocumentMessage) {
82
+ if (this.schedulingLog) {
83
+ this.schedulingLog.numberOfBatchesProcessed++;
84
+ this.schedulingLog.lastSequenceNumber = message.sequenceNumber;
85
+ this.schedulingLog.opsRemainingToProcess = this.deltaManager.inbound.length;
86
+ }
80
87
 
88
+ if (this.shouldRunScheduler()) {
89
+ const currentTime = performance.now();
81
90
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
82
- const elapsedTime = performance.now() - this.processingStartTime!;
83
- if (elapsedTime > this.totalProcessingTime) {
91
+ const elapsedTime = currentTime - this.processingStartTime!;
92
+ if (elapsedTime > this.currentAllowedProcessingTimeForTurn) {
84
93
  // We have processed ops for more than the total processing time. So, pause the
85
94
  // queue, yield the thread and schedule a resume.
86
95
 
87
96
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
88
97
  this.deltaManager.inbound.pause();
89
- setTimeout(() => {
90
- this.deltaManager.inbound.resume();
91
- });
92
98
 
93
- this.processingStartTime = undefined;
94
- // Increase the total processing time. Keep doing this after each turn until all the ops have
99
+ // Increase the total processing time. Keep doing this after each turn until all the ops have
95
100
  // been processed. This way we keep the responsiveness at the beginning while also making sure
96
101
  // that all the ops process fairly quickly.
97
- this.totalProcessingTime += this.processingTimeIncrement;
102
+ this.currentAllowedProcessingTimeForTurn += this.processingTimeIncrement;
98
103
 
99
104
  // If we are logging the telemetry this time, update the telemetry log object.
100
105
  if (this.schedulingLog) {
101
106
  this.schedulingLog.numberOfTurns++;
102
107
  this.schedulingLog.totalProcessingTime += elapsedTime;
103
108
  }
109
+
110
+ setTimeout(() => {
111
+ if (this.schedulingLog) {
112
+ this.logger.sendTelemetryEvent({
113
+ eventName: "InboundOpsPartialProcessingTime",
114
+ duration: TelemetryLogger.formatTick(elapsedTime),
115
+ opsProcessed: this.schedulingLog.lastSequenceNumber -
116
+ this.schedulingLog.firstSequenceNumber + 1,
117
+ opsRemainingToProcess: this.deltaManager.inbound.length,
118
+ processingTime: TelemetryLogger.formatTick(this.schedulingLog.totalProcessingTime),
119
+ numberOfTurns: this.schedulingLog.numberOfTurns,
120
+ batchesProcessed: this.schedulingLog.numberOfBatchesProcessed,
121
+ timeToResume: TelemetryLogger.formatTick(performance.now() - currentTime),
122
+ });
123
+ }
124
+ this.deltaManager.inbound.resume();
125
+ });
126
+
127
+ this.processingStartTime = undefined;
104
128
  }
105
129
  }
106
130
  }
@@ -109,14 +133,19 @@ export class DeltaScheduler {
109
133
  if (this.schedulingLog) {
110
134
  // Add the time taken for processing the final ops to the total processing time in the
111
135
  // telemetry log object.
136
+ const currentTime = performance.now();
112
137
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
113
- this.schedulingLog.totalProcessingTime += performance.now() - this.processingStartTime!;
138
+ this.schedulingLog.totalProcessingTime += currentTime - this.processingStartTime!;
114
139
 
115
140
  this.logger.sendTelemetryEvent({
116
141
  eventName: "InboundOpsProcessingTime",
117
- numberOfOps: this.schedulingLog.numberOfOps,
142
+ opsRemainingToProcess: this.schedulingLog.opsRemainingToProcess,
118
143
  numberOfTurns: this.schedulingLog.numberOfTurns,
119
- processingTime: this.schedulingLog.totalProcessingTime,
144
+ processingTime: TelemetryLogger.formatTick(this.schedulingLog.totalProcessingTime),
145
+ opsProcessed: this.schedulingLog.lastSequenceNumber - this.schedulingLog.firstSequenceNumber + 1,
146
+ batchesProcessed: this.schedulingLog.numberOfBatchesProcessed,
147
+ duration: TelemetryLogger.formatTick(currentTime - this.schedulingLog.startTime),
148
+ schedulingCount: this.schedulingCount,
120
149
  });
121
150
 
122
151
  this.schedulingLog = undefined;
@@ -124,14 +153,11 @@ export class DeltaScheduler {
124
153
 
125
154
  // If we scheduled this batch of the inbound queue, increment the counter that tracks the
126
155
  // number of times we have done this.
127
- if (this.isScheduling) {
128
- this.isScheduling = false;
129
- this.schedulingCount++;
130
- }
156
+ this.schedulingCount++;
131
157
 
132
158
  // Reset the processing times.
133
159
  this.processingStartTime = undefined;
134
- this.totalProcessingTime = DeltaScheduler.processingTime;
160
+ this.currentAllowedProcessingTimeForTurn = DeltaScheduler.processingTime;
135
161
  }
136
162
 
137
163
  /**