@fluidframework/container-runtime 1.0.1 → 1.1.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 (58) hide show
  1. package/dist/connectionTelemetry.d.ts +19 -0
  2. package/dist/connectionTelemetry.d.ts.map +1 -1
  3. package/dist/connectionTelemetry.js +21 -21
  4. package/dist/connectionTelemetry.js.map +1 -1
  5. package/dist/containerRuntime.d.ts +13 -1
  6. package/dist/containerRuntime.d.ts.map +1 -1
  7. package/dist/containerRuntime.js +99 -11
  8. package/dist/containerRuntime.js.map +1 -1
  9. package/dist/dataStore.d.ts.map +1 -1
  10. package/dist/dataStore.js +14 -3
  11. package/dist/dataStore.js.map +1 -1
  12. package/dist/dataStoreRegistry.d.ts +0 -4
  13. package/dist/dataStoreRegistry.d.ts.map +1 -1
  14. package/dist/dataStoreRegistry.js +12 -1
  15. package/dist/dataStoreRegistry.js.map +1 -1
  16. package/dist/dataStores.d.ts.map +1 -1
  17. package/dist/dataStores.js +4 -4
  18. package/dist/dataStores.js.map +1 -1
  19. package/dist/garbageCollection.d.ts +20 -24
  20. package/dist/garbageCollection.d.ts.map +1 -1
  21. package/dist/garbageCollection.js +40 -117
  22. package/dist/garbageCollection.js.map +1 -1
  23. package/dist/packageVersion.d.ts +1 -1
  24. package/dist/packageVersion.js +1 -1
  25. package/dist/packageVersion.js.map +1 -1
  26. package/lib/connectionTelemetry.d.ts +19 -0
  27. package/lib/connectionTelemetry.d.ts.map +1 -1
  28. package/lib/connectionTelemetry.js +21 -21
  29. package/lib/connectionTelemetry.js.map +1 -1
  30. package/lib/containerRuntime.d.ts +13 -1
  31. package/lib/containerRuntime.d.ts.map +1 -1
  32. package/lib/containerRuntime.js +101 -13
  33. package/lib/containerRuntime.js.map +1 -1
  34. package/lib/dataStore.d.ts.map +1 -1
  35. package/lib/dataStore.js +15 -4
  36. package/lib/dataStore.js.map +1 -1
  37. package/lib/dataStoreRegistry.d.ts +0 -4
  38. package/lib/dataStoreRegistry.d.ts.map +1 -1
  39. package/lib/dataStoreRegistry.js +12 -1
  40. package/lib/dataStoreRegistry.js.map +1 -1
  41. package/lib/dataStores.d.ts.map +1 -1
  42. package/lib/dataStores.js +5 -5
  43. package/lib/dataStores.js.map +1 -1
  44. package/lib/garbageCollection.d.ts +20 -24
  45. package/lib/garbageCollection.d.ts.map +1 -1
  46. package/lib/garbageCollection.js +39 -115
  47. package/lib/garbageCollection.js.map +1 -1
  48. package/lib/packageVersion.d.ts +1 -1
  49. package/lib/packageVersion.js +1 -1
  50. package/lib/packageVersion.js.map +1 -1
  51. package/package.json +20 -48
  52. package/src/connectionTelemetry.ts +58 -37
  53. package/src/containerRuntime.ts +119 -26
  54. package/src/dataStore.ts +21 -4
  55. package/src/dataStoreRegistry.ts +8 -1
  56. package/src/dataStores.ts +6 -5
  57. package/src/garbageCollection.ts +66 -158
  58. package/src/packageVersion.ts +1 -1
@@ -19,17 +19,17 @@ import { assert, performance } from "@fluidframework/common-utils";
19
19
  export const latencyThreshold = 5000;
20
20
 
21
21
  // Phases in OpPerfTelemetry:
22
- // 1. Op sits in a buffer in DeltaManager (DM) queue, then in outbound queue for some time.
23
- // - Note: We do not differentiate these two today in telemetry, but first one is due to batches,
24
- // second one might happen due to outbound queue being paused.
25
- // 2. Op is sent to service and back.
26
- // 3. Op sits in inbound queue.
22
+ // 1. Op is added to DeltaManager (DM) buffer.
23
+ // 2. Op is sent to service (op leaves outbound queue).
24
+ // - Note: We do not know for sure when op is sent, we only track when it is added to outbound queue.
25
+ // If outbound queue is paused, time queue is paused is counted as network time.
26
+ // 3. Op received from service back (pushed to inbound queue).
27
27
  // 4. Op is processed.
28
28
  interface IOpPerfTelemetryProperties {
29
29
  /** Measure time between (1) and (2) - Measure time outbound op is sitting in queue due to active batch */
30
- durationOutboundQueue: number;
31
- /** Measure time between (2) and (3) - Track how long op is sitting in inbound queue until it is processed */
32
- durationInboundQueue: number;
30
+ durationOutboundBatching: number; // was durationOutboundQueue in previous versions
31
+ /** Measure time between (2) and (3) - Track how long it took for op to be acked by service */
32
+ durationNetwork: number; // was durationInboundQueue
33
33
  /** Measure time between (3) and (4) - Time between DM's inbound "push" event until DM's "op" event */
34
34
  durationInboundToProcessing: number;
35
35
  /** Length of the DeltaManager's inbound queue at the time of the DM's inbound "push" event (3) */
@@ -41,11 +41,11 @@ interface IOpPerfTelemetryProperties {
41
41
  */
42
42
  interface IOpPerfTimings {
43
43
  /** Starting time for (1) */
44
- opStartTimeForLatencyStatistics: number;
44
+ submitOpEventTime: number;
45
45
  /** Starting time for (2) */
46
- opStartTimeSittingInboundQueue: number;
46
+ outboundPushEventTime: number;
47
47
  /** Starting time for (3) */
48
- opStartTimeInboundPushEvent: number;
48
+ inboundPushEventTime: number;
49
49
  }
50
50
 
51
51
  class OpPerfTelemetry {
@@ -102,26 +102,27 @@ class OpPerfTelemetry {
102
102
  this.opPerfData = {};
103
103
  this.connectionOpSeqNumber = undefined;
104
104
  this.firstConnection = false;
105
+ this.pongCount = 0;
105
106
  });
106
107
 
107
108
  this.deltaManager.outbound.on("push", (messages) => {
108
109
  for (const msg of messages) {
109
110
  if (msg.type === MessageType.Operation &&
110
111
  this.clientSequenceNumberForLatencyStatistics === msg.clientSequenceNumber) {
111
- assert(this.opProcessingTimes.opStartTimeSittingInboundQueue === undefined,
112
- 0x2c8 /* "opStartTimeSittingInboundQueue should be undefined" */);
113
- assert(this.opPerfData.durationInboundQueue === undefined,
114
- 0x2c9 /* "durationInboundQueue should be undefined" */);
115
- this.opProcessingTimes.opStartTimeSittingInboundQueue = Date.now();
112
+ assert(this.opProcessingTimes.outboundPushEventTime === undefined,
113
+ 0x2c8 /* "outboundPushEventTime should be undefined" */);
114
+ assert(this.opPerfData.durationNetwork === undefined,
115
+ 0x2c9 /* "durationNetwork should be undefined" */);
116
+ this.opProcessingTimes.outboundPushEventTime = Date.now();
116
117
 
117
- assert(this.opPerfData.durationOutboundQueue === undefined,
118
- 0x2ca /* "durationOutboundQueue should be undefined" */);
118
+ assert(this.opPerfData.durationOutboundBatching === undefined,
119
+ 0x2ca /* "durationOutboundBatching should be undefined" */);
119
120
 
120
- assert(this.opProcessingTimes.opStartTimeForLatencyStatistics !== undefined,
121
- 0x2cb /* "opStartTimeForLatencyStatistics should be undefined" */);
121
+ assert(this.opProcessingTimes.submitOpEventTime !== undefined,
122
+ 0x2cb /* "submitOpEventTime should be undefined" */);
122
123
 
123
- this.opPerfData.durationOutboundQueue = this.opProcessingTimes.opStartTimeSittingInboundQueue
124
- - this.opProcessingTimes.opStartTimeForLatencyStatistics;
124
+ this.opPerfData.durationOutboundBatching = this.opProcessingTimes.outboundPushEventTime
125
+ - this.opProcessingTimes.submitOpEventTime;
125
126
  }
126
127
  }
127
128
  });
@@ -130,11 +131,11 @@ class OpPerfTelemetry {
130
131
  if (this.clientId === message.clientId &&
131
132
  message.type === MessageType.Operation &&
132
133
  this.clientSequenceNumberForLatencyStatistics === message.clientSequenceNumber &&
133
- this.opProcessingTimes.opStartTimeSittingInboundQueue !== undefined) {
134
- this.opProcessingTimes.opStartTimeInboundPushEvent = Date.now();
135
- this.opPerfData.durationInboundQueue = this.opProcessingTimes.opStartTimeInboundPushEvent
136
- - this.opProcessingTimes.opStartTimeSittingInboundQueue;
137
- this.opProcessingTimes.opStartTimeSittingInboundQueue = undefined;
134
+ this.opProcessingTimes.outboundPushEventTime !== undefined) {
135
+ this.opProcessingTimes.inboundPushEventTime = Date.now();
136
+ this.opPerfData.durationNetwork = this.opProcessingTimes.inboundPushEventTime
137
+ - this.opProcessingTimes.outboundPushEventTime;
138
+ this.opProcessingTimes.outboundPushEventTime = undefined;
138
139
  this.opPerfData.lengthInboundQueue = this.deltaManager.inbound.length;
139
140
  }
140
141
  });
@@ -173,12 +174,11 @@ class OpPerfTelemetry {
173
174
  private recordPingTime(latency: number) {
174
175
  this.pingLatency = latency;
175
176
  // logging one in every 1000 pongs, including the first time, if it is a "write" client.
176
- if (this.pongCount % 1000 === 0 && this.deltaManager.active) {
177
+ if (this.pongCount % 100 === 0 && this.deltaManager.active) {
177
178
  this.logger.sendPerformanceEvent({
178
179
  eventName: "DeltaLatency",
179
180
  duration: latency,
180
181
  });
181
- this.pongCount = 0;
182
182
  }
183
183
  this.pongCount++;
184
184
  }
@@ -187,11 +187,11 @@ class OpPerfTelemetry {
187
187
  // start with first client op and measure latency every 500 client ops
188
188
  if (this.clientSequenceNumberForLatencyStatistics === undefined &&
189
189
  message.clientSequenceNumber % 500 === 1) {
190
- assert(this.opProcessingTimes.opStartTimeSittingInboundQueue === undefined,
190
+ assert(this.opProcessingTimes.outboundPushEventTime === undefined,
191
191
  0x2cc /* "OpTimeSittingInboundQueue should be undefined" */);
192
- assert(this.opPerfData.durationInboundQueue === undefined,
193
- 0x2cd /* "durationInboundQueue should be undefined" */);
194
- this.opProcessingTimes.opStartTimeForLatencyStatistics = Date.now();
192
+ assert(this.opPerfData.durationNetwork === undefined,
193
+ 0x2cd /* "durationNetwork should be undefined" */);
194
+ this.opProcessingTimes.submitOpEventTime = Date.now();
195
195
  this.clientSequenceNumberForLatencyStatistics = message.clientSequenceNumber;
196
196
  }
197
197
  }
@@ -223,16 +223,16 @@ class OpPerfTelemetry {
223
223
 
224
224
  if (this.clientId === message.clientId &&
225
225
  this.clientSequenceNumberForLatencyStatistics === message.clientSequenceNumber) {
226
- assert(this.opProcessingTimes.opStartTimeForLatencyStatistics !== undefined,
226
+ assert(this.opProcessingTimes.submitOpEventTime !== undefined,
227
227
  0x120 /* "Undefined latency statistics (op send time)" */);
228
228
  const currentTime = Date.now();
229
229
 
230
- if (this.opProcessingTimes.opStartTimeInboundPushEvent !== undefined) {
230
+ if (this.opProcessingTimes.inboundPushEventTime !== undefined) {
231
231
  this.opPerfData.durationInboundToProcessing = currentTime
232
- - this.opProcessingTimes.opStartTimeInboundPushEvent;
232
+ - this.opProcessingTimes.inboundPushEventTime;
233
233
  }
234
234
 
235
- const duration = currentTime - this.opProcessingTimes.opStartTimeForLatencyStatistics;
235
+ const duration = currentTime - this.opProcessingTimes.submitOpEventTime;
236
236
 
237
237
  // One of the core expectations for Fluid service is to be fast.
238
238
  // When it's not the case, we want to learn about it and be able to investigate, so
@@ -258,6 +258,27 @@ class OpPerfTelemetry {
258
258
  }
259
259
  }
260
260
  }
261
+ export interface IPerfSignalReport {
262
+ /**
263
+ * Identifier for the signal being submitted in order to
264
+ * allow collection of data around the roundtrip of signal messages.
265
+ */
266
+ signalSequenceNumber: number;
267
+ /**
268
+ * Number of signals that were expected but not received.
269
+ */
270
+ signalsLost: number;
271
+
272
+ /**
273
+ * Timestamp before submitting the signal we will trace.
274
+ */
275
+ signalTimestamp: number;
276
+
277
+ /**
278
+ * Expected Signal Sequence to be received.
279
+ */
280
+ trackingSignalSequenceNumber: number | undefined;
281
+ }
261
282
 
262
283
  export function ReportOpPerfTelemetry(
263
284
  clientId: string | undefined,
@@ -49,6 +49,7 @@ import { DriverHeader, IDocumentStorageService, ISummaryContext } from "@fluidfr
49
49
  import { readAndParse } from "@fluidframework/driver-utils";
50
50
  import {
51
51
  DataCorruptionError,
52
+ DataProcessingError,
52
53
  GenericError,
53
54
  UsageError,
54
55
  extractSafePropertiesFromMessage,
@@ -108,7 +109,11 @@ import { FluidDataStoreRegistry } from "./dataStoreRegistry";
108
109
  import { Summarizer } from "./summarizer";
109
110
  import { SummaryManager } from "./summaryManager";
110
111
  import { DeltaScheduler } from "./deltaScheduler";
111
- import { ReportOpPerfTelemetry, latencyThreshold } from "./connectionTelemetry";
112
+ import {
113
+ ReportOpPerfTelemetry,
114
+ latencyThreshold,
115
+ IPerfSignalReport,
116
+ } from "./connectionTelemetry";
112
117
  import { IPendingLocalState, PendingStateManager } from "./pendingStateManager";
113
118
  import { pkgVersion } from "./packageVersion";
114
119
  import { BlobManager, IBlobManagerLoadInfo } from "./blobManager";
@@ -221,7 +226,7 @@ export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfigurati
221
226
  /**
222
227
  * Defines the maximum allowed time in between summarizations.
223
228
  */
224
- idleTime: number;
229
+ idleTime: number;
225
230
  /**
226
231
  * Defines the maximum allowed time, since the last received Ack, before running the summary
227
232
  * with reason maxTime.
@@ -231,9 +236,9 @@ export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfigurati
231
236
  * Defines the maximum number of Ops, since the last received Ack, that can be allowed
232
237
  * before running the summary with reason maxOps.
233
238
  */
234
- maxOps: number;
239
+ maxOps: number;
235
240
  /**
236
- * Defnines the minimum number of Ops, since the last received Ack, that can be allowed
241
+ * Defines the minimum number of Ops, since the last received Ack, that can be allowed
237
242
  * before running the last summary.
238
243
  */
239
244
  minOpsForLastSummaryAttempt: number;
@@ -334,7 +339,7 @@ export interface ISummaryRuntimeOptions {
334
339
  * @deprecated - use `summaryConfigOverrides.maxOpsSinceLastSummary` instead.
335
340
  * Defaults to 7000 ops
336
341
  */
337
- maxOpsSinceLastSummary?: number;
342
+ maxOpsSinceLastSummary?: number;
338
343
 
339
344
  /**
340
345
  * @deprecated - use `summaryConfigOverrides.summarizerClientElection` instead.
@@ -606,7 +611,7 @@ class ScheduleManagerCore {
606
611
 
607
612
  private resumeQueue(startBatch: number, messageEndBatch: ISequencedDocumentMessage) {
608
613
  const endBatch = messageEndBatch.sequenceNumber;
609
- const duration = performance.now() - this.timePaused;
614
+ const duration = this.localPaused ? (performance.now() - this.timePaused) : undefined;
610
615
 
611
616
  this.batchCount++;
612
617
  if (this.batchCount % 1000 === 1) {
@@ -629,7 +634,7 @@ class ScheduleManagerCore {
629
634
  this.localPaused = false;
630
635
 
631
636
  // Random round number - we want to know when batch waiting paused op processing.
632
- if (duration > latencyThreshold) {
637
+ if (duration !== undefined && duration > latencyThreshold) {
633
638
  this.logger.sendErrorEvent({
634
639
  eventName: "MaxBatchWaitTimeExceeded",
635
640
  duration,
@@ -891,7 +896,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
891
896
 
892
897
  // Verify summary runtime sequence number matches protocol sequence number.
893
898
  const runtimeSequenceNumber = metadata?.message?.sequenceNumber;
894
- if (runtimeSequenceNumber !== undefined) {
899
+ // When we load with pending state, we reuse an old snapshot so we don't expect these numbers to match
900
+ if (!pendingRuntimeState && runtimeSequenceNumber !== undefined) {
895
901
  const protocolSequenceNumber = context.deltaManager.initialSequenceNumber;
896
902
  // Unless bypass is explicitly set, then take action when sequence numbers mismatch.
897
903
  if (loadSequenceNumberVerification !== "bypass" && runtimeSequenceNumber !== protocolSequenceNumber) {
@@ -1044,6 +1050,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1044
1050
  private dirtyContainer: boolean;
1045
1051
  private emitDirtyDocumentEvent = true;
1046
1052
 
1053
+ private readonly defaultTelemetrySignalSampleCount = 100;
1054
+ private _perfSignalData: IPerfSignalReport = {
1055
+ signalsLost: 0,
1056
+ signalSequenceNumber: 0,
1057
+ signalTimestamp: 0,
1058
+ trackingSignalSequenceNumber: undefined,
1059
+ };
1060
+
1047
1061
  /**
1048
1062
  * Summarizer is responsible for coordinating when to send generate and send summaries.
1049
1063
  * It is the main entry point for summary work.
@@ -1201,18 +1215,18 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1201
1215
  const pendingRuntimeState = context.pendingLocalState as IPendingRuntimeState | undefined;
1202
1216
  const baseSnapshot: ISnapshotTree | undefined = pendingRuntimeState?.baseSnapshot ?? context.baseSnapshot;
1203
1217
 
1204
- this.garbageCollector = GarbageCollector.create(
1205
- this,
1206
- this.runtimeOptions.gcOptions,
1207
- (nodePath: string) => this.getGCNodePackagePath(nodePath),
1208
- () => this.messageAtLastSummary?.timestamp,
1218
+ this.garbageCollector = GarbageCollector.create({
1219
+ runtime: this,
1220
+ gcOptions: this.runtimeOptions.gcOptions,
1209
1221
  baseSnapshot,
1210
- async <T>(id: string) => readAndParse<T>(this.storage, id),
1211
- this.mc.logger,
1222
+ baseLogger: this.mc.logger,
1212
1223
  existing,
1213
1224
  metadata,
1214
- this.context.clientDetails.type === summarizerClientType,
1215
- );
1225
+ isSummarizerClient: this.context.clientDetails.type === summarizerClientType,
1226
+ getNodePackagePath: (nodePath: string) => this.getGCNodePackagePath(nodePath),
1227
+ getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
1228
+ readAndParseBlob: async <T>(id: string) => readAndParse<T>(this.storage, id),
1229
+ });
1216
1230
 
1217
1231
  const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
1218
1232
  this.summarizerNode = createRootSummarizerNodeWithGC(
@@ -1557,11 +1571,17 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1557
1571
  }
1558
1572
  }
1559
1573
 
1574
+ private internalId(maybeAlias: string): string {
1575
+ return this.dataStores.aliases().get(maybeAlias) ?? maybeAlias;
1576
+ }
1577
+
1560
1578
  private async getDataStoreFromRequest(id: string, request: IRequest): Promise<IFluidRouter> {
1561
1579
  const wait = typeof request.headers?.[RuntimeHeaders.wait] === "boolean"
1562
1580
  ? request.headers?.[RuntimeHeaders.wait]
1563
1581
  : true;
1564
- const dataStoreContext = await this.dataStores.getDataStore(id, wait);
1582
+
1583
+ const internalId = this.internalId(id);
1584
+ const dataStoreContext = await this.dataStores.getDataStore(internalId, wait);
1565
1585
 
1566
1586
  /**
1567
1587
  * If GC should run and this an external app request with "externalRequest" header, we need to return
@@ -1743,15 +1763,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1743
1763
  const reconnection = changeOfState && connected;
1744
1764
  this._connected = connected;
1745
1765
 
1766
+ if (!connected) {
1767
+ this._perfSignalData.signalsLost = 0;
1768
+ this._perfSignalData.signalTimestamp = 0;
1769
+ this._perfSignalData.trackingSignalSequenceNumber = undefined;
1770
+ }
1771
+
1746
1772
  if (reconnection) {
1747
1773
  this.consecutiveReconnects++;
1748
1774
 
1749
1775
  if (!this.shouldContinueReconnecting()) {
1750
- this.closeFn(new GenericError(
1776
+ this.closeFn(
1751
1777
  // pre-0.58 error message: MaxReconnectsWithNoProgress
1752
- "Runtime detected too many reconnects with no progress syncing local ops",
1753
- undefined, // error
1754
- {
1778
+ DataProcessingError.create(
1779
+ "Runtime detected too many reconnects with no progress syncing local ops",
1780
+ "setConnectionState",
1781
+ undefined,
1782
+ {
1783
+ dataLoss: 1,
1755
1784
  attempts: this.consecutiveReconnects,
1756
1785
  pendingMessages: this.pendingStateManager.pendingMessagesCount,
1757
1786
  }));
@@ -1853,6 +1882,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1853
1882
  this.dataStores.processAliasMessage(message, localOpMetadata, local);
1854
1883
  }
1855
1884
 
1885
+ /**
1886
+ * Emits the Signal event and update the perf signal data.
1887
+ * @param clientSignalSequenceNumber - is the client signal sequence number to be uploaded.
1888
+ */
1889
+ private sendSignalTelemetryEvent(clientSignalSequenceNumber: number) {
1890
+ const duration = Date.now() - this._perfSignalData.signalTimestamp;
1891
+ this.logger.sendPerformanceEvent({
1892
+ eventName: "SignalLatency",
1893
+ duration,
1894
+ signalsLost: this._perfSignalData.signalsLost,
1895
+ });
1896
+
1897
+ this._perfSignalData.signalsLost = 0;
1898
+ this._perfSignalData.signalTimestamp = 0;
1899
+ }
1900
+
1856
1901
  public processSignal(message: ISignalMessage, local: boolean) {
1857
1902
  const envelope = message.content as ISignalEnvelope;
1858
1903
  const transformed: IInboundSignalMessage = {
@@ -1861,6 +1906,26 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1861
1906
  type: envelope.contents.type,
1862
1907
  };
1863
1908
 
1909
+ // Only collect signal telemetry for messages sent by the current client.
1910
+ if (message.clientId === this.clientId && this.connected) {
1911
+ // Check to see if the signal was lost.
1912
+ if (this._perfSignalData.trackingSignalSequenceNumber !== undefined &&
1913
+ envelope.clientSignalSequenceNumber > this._perfSignalData.trackingSignalSequenceNumber) {
1914
+ this._perfSignalData.signalsLost++;
1915
+ this._perfSignalData.trackingSignalSequenceNumber = undefined;
1916
+ this.logger.sendErrorEvent({
1917
+ eventName: "SignalLost",
1918
+ type: envelope.contents.type,
1919
+ signalsLost: this._perfSignalData.signalsLost,
1920
+ trackingSequenceNumber: this._perfSignalData.trackingSignalSequenceNumber,
1921
+ clientSignalSequenceNumber: envelope.clientSignalSequenceNumber,
1922
+ });
1923
+ } else if (envelope.clientSignalSequenceNumber === this._perfSignalData.trackingSignalSequenceNumber) {
1924
+ this.sendSignalTelemetryEvent(envelope.clientSignalSequenceNumber);
1925
+ this._perfSignalData.trackingSignalSequenceNumber = undefined;
1926
+ }
1927
+ }
1928
+
1864
1929
  if (envelope.address === undefined) {
1865
1930
  // No address indicates a container signal message.
1866
1931
  this.emit("signal", transformed, local);
@@ -1871,7 +1936,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1871
1936
  }
1872
1937
 
1873
1938
  public async getRootDataStore(id: string, wait = true): Promise<IFluidRouter> {
1874
- const context = await this.dataStores.getDataStore(id, wait);
1939
+ const internalId = this.internalId(id);
1940
+ const context = await this.dataStores.getDataStore(internalId, wait);
1875
1941
  assert(await context.isRoot(), 0x12b /* "did not get root data store" */);
1876
1942
  return context.realize();
1877
1943
  }
@@ -2001,7 +2067,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2001
2067
  return fluidDataStore;
2002
2068
  }
2003
2069
 
2070
+ /**
2071
+ * @deprecated - will be removed in an upcoming release. See #9660.
2072
+ */
2004
2073
  public async createRootDataStore(pkg: string | string[], rootDataStoreId: string): Promise<IFluidRouter> {
2074
+ if (rootDataStoreId.includes("/")) {
2075
+ throw new UsageError(`Id cannot contain slashes: '${rootDataStoreId}'`);
2076
+ }
2005
2077
  return this._aliasingEnabled === true ?
2006
2078
  this.createAndAliasDataStore(pkg, rootDataStoreId) :
2007
2079
  this.createRootDataStoreLegacy(pkg, rootDataStoreId);
@@ -2046,6 +2118,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2046
2118
  public createDetachedRootDataStore(
2047
2119
  pkg: Readonly<string[]>,
2048
2120
  rootDataStoreId: string): IFluidDataStoreContextDetached {
2121
+ if (rootDataStoreId.includes("/")) {
2122
+ throw new UsageError(`Id cannot contain slashes: '${rootDataStoreId}'`);
2123
+ }
2049
2124
  return this.dataStores.createDetachedDataStoreCore(pkg, true, rootDataStoreId);
2050
2125
  }
2051
2126
 
@@ -2143,6 +2218,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2143
2218
  return true;
2144
2219
  }
2145
2220
 
2221
+ private createNewSignalEnvelope(address: string | undefined, type: string, content: any): ISignalEnvelope {
2222
+ const newSequenceNumber = ++this._perfSignalData.signalSequenceNumber;
2223
+ const newEnvelope: ISignalEnvelope = {
2224
+ address,
2225
+ clientSignalSequenceNumber: newSequenceNumber,
2226
+ contents: { type, content },
2227
+ };
2228
+
2229
+ // We should not track any signals in case we already have a tracking number.
2230
+ if (newSequenceNumber % this.defaultTelemetrySignalSampleCount === 1 &&
2231
+ this._perfSignalData.trackingSignalSequenceNumber === undefined) {
2232
+ this._perfSignalData.signalTimestamp = Date.now();
2233
+ this._perfSignalData.trackingSignalSequenceNumber = newSequenceNumber;
2234
+ }
2235
+
2236
+ return newEnvelope;
2237
+ }
2238
+
2146
2239
  /**
2147
2240
  * Submits the signal to be sent to other clients.
2148
2241
  * @param type - Type of the signal.
@@ -2150,13 +2243,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2150
2243
  */
2151
2244
  public submitSignal(type: string, content: any) {
2152
2245
  this.verifyNotClosed();
2153
- const envelope: ISignalEnvelope = { address: undefined, contents: { type, content } };
2246
+ const envelope = this.createNewSignalEnvelope(undefined /* address */, type, content);
2154
2247
  return this.context.submitSignalFn(envelope);
2155
2248
  }
2156
2249
 
2157
2250
  public submitDataStoreSignal(address: string, type: string, content: any) {
2158
- const envelope: ISignalEnvelope = { address, contents: { type, content } };
2159
- return this.context.submitSignalFn(envelope);
2251
+ const envelope = this.createNewSignalEnvelope(address, type, content);
2252
+ return this.context.submitSignalFn(envelope);
2160
2253
  }
2161
2254
 
2162
2255
  public setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void {
package/src/dataStore.ts CHANGED
@@ -4,8 +4,9 @@
4
4
  */
5
5
 
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
- import { unreachableCase } from "@fluidframework/common-utils";
7
+ import { assert, unreachableCase } from "@fluidframework/common-utils";
8
8
  import { AttachState } from "@fluidframework/container-definitions";
9
+ import { UsageError } from "@fluidframework/container-utils";
9
10
  import { IRequest, IResponse } from "@fluidframework/core-interfaces";
10
11
  import { AliasResult, IDataStore, IFluidDataStoreChannel } from "@fluidframework/runtime-definitions";
11
12
  import { TelemetryDataTag } from "@fluidframework/telemetry-utils";
@@ -53,16 +54,27 @@ enum AliasState {
53
54
  class DataStore implements IDataStore {
54
55
  private aliasState: AliasState = AliasState.None;
55
56
  private alias: string | undefined;
57
+ private aliasResult: Promise<AliasResult> | undefined;
56
58
 
57
59
  async trySetAlias(alias: string): Promise<AliasResult> {
60
+ if (alias.includes("/")) {
61
+ throw new UsageError(`The alias cannot contain slashes: '${alias}'`);
62
+ }
63
+
58
64
  switch (this.aliasState) {
59
- // If we're already aliasing, throw an exception
65
+ // If we're already aliasing, check if it's for the same value and return
66
+ // the stored promise, otherwise return 'AlreadyAliased'
60
67
  case AliasState.Aliasing:
61
- return "Aliasing";
68
+ assert(this.aliasResult !== undefined,
69
+ 0x316 /* There should be a cached promise of in-progress aliasing */);
70
+ await this.aliasResult;
71
+ return this.alias === alias ? "Success" : "AlreadyAliased";
72
+
62
73
  // If this datastore is already aliased, return true only if this
63
74
  // is a repeated call for the same alias
64
75
  case AliasState.Aliased:
65
76
  return this.alias === alias ? "Success" : "AlreadyAliased";
77
+
66
78
  // There is no current or past alias operation for this datastore,
67
79
  // it is safe to continue execution
68
80
  case AliasState.None: break;
@@ -70,6 +82,11 @@ class DataStore implements IDataStore {
70
82
  }
71
83
 
72
84
  this.aliasState = AliasState.Aliasing;
85
+ this.aliasResult = this.trySetAliasInternal(alias);
86
+ return this.aliasResult;
87
+ }
88
+
89
+ async trySetAliasInternal(alias: string): Promise<AliasResult> {
73
90
  const message: IDataStoreAliasMessage = {
74
91
  internalId: this.internalId,
75
92
  alias,
@@ -85,7 +102,7 @@ class DataStore implements IDataStore {
85
102
 
86
103
  if (this.runtime.attachState === AttachState.Detached) {
87
104
  const localResult = this.datastores.processAliasMessageCore(message);
88
- // Explicitly Lock-out future attempts of aliasing,
105
+ // Explicitly lock-out future attempts of aliasing,
89
106
  // regardless of result
90
107
  this.aliasState = AliasState.Aliased;
91
108
  return localResult ? "Success" : "Conflict";
@@ -2,6 +2,7 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
+ import { UsageError } from "@fluidframework/container-utils";
5
6
  import {
6
7
  FluidDataStoreRegistryEntry,
7
8
  IFluidDataStoreRegistry,
@@ -14,7 +15,13 @@ export class FluidDataStoreRegistry implements IFluidDataStoreRegistry {
14
15
  public get IFluidDataStoreRegistry() { return this; }
15
16
 
16
17
  constructor(namedEntries: NamedFluidDataStoreRegistryEntries) {
17
- this.map = new Map(namedEntries);
18
+ this.map = new Map();
19
+ for (const entry of namedEntries) {
20
+ if (this.map.has(entry[0])) {
21
+ throw new UsageError("Duplicate entry names exist");
22
+ }
23
+ this.map.set(entry[0], entry[1]);
24
+ }
18
25
  }
19
26
 
20
27
  public async get(name: string): Promise<FluidDataStoreRegistryEntry | undefined> {
package/src/dataStores.ts CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  responseToException,
35
35
  SummaryTreeBuilder,
36
36
  } from "@fluidframework/runtime-utils";
37
- import { ChildLogger, TelemetryDataTag } from "@fluidframework/telemetry-utils";
37
+ import { ChildLogger, LoggingError, TelemetryDataTag } from "@fluidframework/telemetry-utils";
38
38
  import { AttachState } from "@fluidframework/container-definitions";
39
39
  import { BlobCacheStorageService, buildSnapshotTree } from "@fluidframework/driver-utils";
40
40
  import { assert, Lazy, LazyPromise } from "@fluidframework/common-utils";
@@ -145,7 +145,7 @@ export class DataStores implements IDisposable {
145
145
  });
146
146
  } else {
147
147
  if (typeof value !== "object") {
148
- throw new Error("Snapshot should be there to load from!!");
148
+ throw new LoggingError("Snapshot should be there to load from!!");
149
149
  }
150
150
  const snapshotTree = value;
151
151
  dataStoreContext = new LocalFluidDataStoreContext({
@@ -329,6 +329,8 @@ export class DataStores implements IDisposable {
329
329
  pkg: Readonly<string[]>,
330
330
  isRoot: boolean,
331
331
  id = uuid()): IFluidDataStoreContextDetached {
332
+ assert(!id.includes("/"), 0x30c /* Id cannot contain slashes */);
333
+
332
334
  const context = new LocalDetachedFluidDataStoreContext({
333
335
  id,
334
336
  pkg,
@@ -350,6 +352,7 @@ export class DataStores implements IDisposable {
350
352
  }
351
353
 
352
354
  public _createFluidDataStoreContext(pkg: string[], id: string, isRoot: boolean, props?: any) {
355
+ assert(!id.includes("/"), 0x30d /* Id cannot contain slashes */);
353
356
  const context = new LocalFluidDataStoreContext({
354
357
  id,
355
358
  pkg,
@@ -418,9 +421,7 @@ export class DataStores implements IDisposable {
418
421
  }
419
422
 
420
423
  public async getDataStore(id: string, wait: boolean): Promise<FluidDataStoreContext> {
421
- const internalId = this.aliasMap.get(id) ?? id;
422
-
423
- const context = await this.contexts.getBoundOrRemoted(internalId, wait);
424
+ const context = await this.contexts.getBoundOrRemoted(id, wait);
424
425
  if (context === undefined) {
425
426
  // The requested data store does not exits. Throw a 404 response exception.
426
427
  const request = { url: id };