@fluidframework/container-runtime 2.3.0-288113 → 2.4.0-294316

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 (181) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/api-report/container-runtime.legacy.alpha.api.md +15 -0
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/channelCollection.d.ts +1 -1
  5. package/dist/channelCollection.d.ts.map +1 -1
  6. package/dist/channelCollection.js +1 -16
  7. package/dist/channelCollection.js.map +1 -1
  8. package/dist/connectionTelemetry.d.ts +27 -3
  9. package/dist/connectionTelemetry.d.ts.map +1 -1
  10. package/dist/connectionTelemetry.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +68 -13
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +262 -180
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/deltaManagerProxies.d.ts.map +1 -1
  16. package/dist/deltaManagerProxies.js +11 -4
  17. package/dist/deltaManagerProxies.js.map +1 -1
  18. package/dist/gc/garbageCollection.d.ts.map +1 -1
  19. package/dist/gc/garbageCollection.js +0 -2
  20. package/dist/gc/garbageCollection.js.map +1 -1
  21. package/dist/gc/gcHelpers.d.ts.map +1 -1
  22. package/dist/gc/gcHelpers.js +0 -8
  23. package/dist/gc/gcHelpers.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/legacy.d.ts +3 -1
  29. package/dist/messageTypes.d.ts +0 -9
  30. package/dist/messageTypes.d.ts.map +1 -1
  31. package/dist/messageTypes.js.map +1 -1
  32. package/dist/opLifecycle/batchManager.d.ts +9 -0
  33. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  34. package/dist/opLifecycle/batchManager.js +19 -6
  35. package/dist/opLifecycle/batchManager.js.map +1 -1
  36. package/dist/opLifecycle/duplicateBatchDetector.d.ts +32 -0
  37. package/dist/opLifecycle/duplicateBatchDetector.d.ts.map +1 -0
  38. package/dist/opLifecycle/duplicateBatchDetector.js +68 -0
  39. package/dist/opLifecycle/duplicateBatchDetector.js.map +1 -0
  40. package/dist/opLifecycle/index.d.ts +3 -2
  41. package/dist/opLifecycle/index.d.ts.map +1 -1
  42. package/dist/opLifecycle/index.js +4 -1
  43. package/dist/opLifecycle/index.js.map +1 -1
  44. package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
  45. package/dist/opLifecycle/opCompressor.js +0 -4
  46. package/dist/opLifecycle/opCompressor.js.map +1 -1
  47. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  48. package/dist/opLifecycle/opGroupingManager.js +0 -4
  49. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  50. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  51. package/dist/opLifecycle/opSplitter.js +1 -6
  52. package/dist/opLifecycle/opSplitter.js.map +1 -1
  53. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  54. package/dist/opLifecycle/outbox.js +1 -4
  55. package/dist/opLifecycle/outbox.js.map +1 -1
  56. package/dist/opLifecycle/remoteMessageProcessor.d.ts +37 -17
  57. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  58. package/dist/opLifecycle/remoteMessageProcessor.js +47 -37
  59. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  60. package/dist/packageVersion.d.ts +1 -1
  61. package/dist/packageVersion.js +1 -1
  62. package/dist/packageVersion.js.map +1 -1
  63. package/dist/pendingStateManager.d.ts +27 -17
  64. package/dist/pendingStateManager.d.ts.map +1 -1
  65. package/dist/pendingStateManager.js +85 -56
  66. package/dist/pendingStateManager.js.map +1 -1
  67. package/dist/scheduleManager.d.ts +2 -4
  68. package/dist/scheduleManager.d.ts.map +1 -1
  69. package/dist/scheduleManager.js +6 -37
  70. package/dist/scheduleManager.js.map +1 -1
  71. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  72. package/dist/summary/summarizerNode/summarizerNodeUtils.js +0 -2
  73. package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  74. package/dist/summary/summaryCollection.d.ts.map +1 -1
  75. package/dist/summary/summaryCollection.js +5 -7
  76. package/dist/summary/summaryCollection.js.map +1 -1
  77. package/dist/summary/summaryFormat.d.ts.map +1 -1
  78. package/dist/summary/summaryFormat.js +1 -4
  79. package/dist/summary/summaryFormat.js.map +1 -1
  80. package/lib/channelCollection.d.ts +1 -1
  81. package/lib/channelCollection.d.ts.map +1 -1
  82. package/lib/channelCollection.js +1 -16
  83. package/lib/channelCollection.js.map +1 -1
  84. package/lib/connectionTelemetry.d.ts +27 -3
  85. package/lib/connectionTelemetry.d.ts.map +1 -1
  86. package/lib/connectionTelemetry.js.map +1 -1
  87. package/lib/containerRuntime.d.ts +68 -13
  88. package/lib/containerRuntime.d.ts.map +1 -1
  89. package/lib/containerRuntime.js +262 -181
  90. package/lib/containerRuntime.js.map +1 -1
  91. package/lib/deltaManagerProxies.d.ts.map +1 -1
  92. package/lib/deltaManagerProxies.js +11 -4
  93. package/lib/deltaManagerProxies.js.map +1 -1
  94. package/lib/gc/garbageCollection.d.ts.map +1 -1
  95. package/lib/gc/garbageCollection.js +0 -2
  96. package/lib/gc/garbageCollection.js.map +1 -1
  97. package/lib/gc/gcHelpers.d.ts.map +1 -1
  98. package/lib/gc/gcHelpers.js +0 -8
  99. package/lib/gc/gcHelpers.js.map +1 -1
  100. package/lib/index.d.ts +1 -1
  101. package/lib/index.d.ts.map +1 -1
  102. package/lib/index.js +1 -1
  103. package/lib/index.js.map +1 -1
  104. package/lib/legacy.d.ts +3 -1
  105. package/lib/messageTypes.d.ts +0 -9
  106. package/lib/messageTypes.d.ts.map +1 -1
  107. package/lib/messageTypes.js.map +1 -1
  108. package/lib/opLifecycle/batchManager.d.ts +9 -0
  109. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  110. package/lib/opLifecycle/batchManager.js +17 -5
  111. package/lib/opLifecycle/batchManager.js.map +1 -1
  112. package/lib/opLifecycle/duplicateBatchDetector.d.ts +32 -0
  113. package/lib/opLifecycle/duplicateBatchDetector.d.ts.map +1 -0
  114. package/lib/opLifecycle/duplicateBatchDetector.js +64 -0
  115. package/lib/opLifecycle/duplicateBatchDetector.js.map +1 -0
  116. package/lib/opLifecycle/index.d.ts +3 -2
  117. package/lib/opLifecycle/index.d.ts.map +1 -1
  118. package/lib/opLifecycle/index.js +2 -1
  119. package/lib/opLifecycle/index.js.map +1 -1
  120. package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
  121. package/lib/opLifecycle/opCompressor.js +0 -4
  122. package/lib/opLifecycle/opCompressor.js.map +1 -1
  123. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  124. package/lib/opLifecycle/opGroupingManager.js +0 -4
  125. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  126. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  127. package/lib/opLifecycle/opSplitter.js +1 -6
  128. package/lib/opLifecycle/opSplitter.js.map +1 -1
  129. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  130. package/lib/opLifecycle/outbox.js +1 -4
  131. package/lib/opLifecycle/outbox.js.map +1 -1
  132. package/lib/opLifecycle/remoteMessageProcessor.d.ts +37 -17
  133. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  134. package/lib/opLifecycle/remoteMessageProcessor.js +47 -37
  135. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  136. package/lib/packageVersion.d.ts +1 -1
  137. package/lib/packageVersion.js +1 -1
  138. package/lib/packageVersion.js.map +1 -1
  139. package/lib/pendingStateManager.d.ts +27 -17
  140. package/lib/pendingStateManager.d.ts.map +1 -1
  141. package/lib/pendingStateManager.js +85 -56
  142. package/lib/pendingStateManager.js.map +1 -1
  143. package/lib/scheduleManager.d.ts +2 -4
  144. package/lib/scheduleManager.d.ts.map +1 -1
  145. package/lib/scheduleManager.js +6 -37
  146. package/lib/scheduleManager.js.map +1 -1
  147. package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  148. package/lib/summary/summarizerNode/summarizerNodeUtils.js +0 -2
  149. package/lib/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  150. package/lib/summary/summaryCollection.d.ts.map +1 -1
  151. package/lib/summary/summaryCollection.js +5 -7
  152. package/lib/summary/summaryCollection.js.map +1 -1
  153. package/lib/summary/summaryFormat.d.ts.map +1 -1
  154. package/lib/summary/summaryFormat.js +1 -4
  155. package/lib/summary/summaryFormat.js.map +1 -1
  156. package/lib/tsdoc-metadata.json +1 -1
  157. package/package.json +26 -25
  158. package/src/channelCollection.ts +7 -21
  159. package/src/connectionTelemetry.ts +33 -3
  160. package/src/containerRuntime.ts +382 -233
  161. package/src/deltaManagerProxies.ts +11 -4
  162. package/src/gc/garbageCollection.ts +1 -3
  163. package/src/gc/gcHelpers.ts +4 -12
  164. package/src/index.ts +2 -0
  165. package/src/messageTypes.ts +0 -10
  166. package/src/opLifecycle/batchManager.ts +29 -7
  167. package/src/opLifecycle/duplicateBatchDetector.ts +78 -0
  168. package/src/opLifecycle/index.ts +4 -1
  169. package/src/opLifecycle/opCompressor.ts +2 -6
  170. package/src/opLifecycle/opGroupingManager.ts +2 -6
  171. package/src/opLifecycle/opSplitter.ts +2 -6
  172. package/src/opLifecycle/outbox.ts +1 -3
  173. package/src/opLifecycle/remoteMessageProcessor.ts +87 -59
  174. package/src/packageVersion.ts +1 -1
  175. package/src/pendingStateManager.ts +114 -66
  176. package/src/scheduleManager.ts +8 -47
  177. package/src/summary/summarizerNode/summarizerNodeUtils.ts +1 -3
  178. package/src/summary/summaryCollection.ts +7 -9
  179. package/src/summary/summaryFormat.ts +1 -3
  180. package/src/summary/summaryFormats.md +11 -9
  181. package/tsconfig.json +1 -0
@@ -11,7 +11,7 @@ import { DriverHeader, FetchSource, MessageType, } from "@fluidframework/driver-
11
11
  import { readAndParse } from "@fluidframework/driver-utils/internal";
12
12
  import { FlushMode, FlushModeExperimental, channelsTreeName, gcTreeKey, } from "@fluidframework/runtime-definitions/internal";
13
13
  import { GCDataBuilder, RequestParser, TelemetryContext, addBlobToSummary, addSummarizeResultToSummary, calculateStats, create404Response, exceptionToResponse, responseToException, seqFromTree, } from "@fluidframework/runtime-utils/internal";
14
- import { DataCorruptionError, DataProcessingError, GenericError, LoggingError, PerformanceEvent,
14
+ import { DataCorruptionError, DataProcessingError, extractSafePropertiesFromMessage, GenericError, LoggingError, PerformanceEvent,
15
15
  // eslint-disable-next-line import/no-deprecated
16
16
  TaggedLoggerAdapter, UsageError, createChildLogger, createChildMonitoringContext, createSampledLogger, loggerToMonitoringContext, raiseConnectedEvent, wrapError, tagCodeArtifacts, } from "@fluidframework/telemetry-utils/internal";
17
17
  import { v4 as uuid } from "uuid";
@@ -25,7 +25,7 @@ import { FluidDataStoreRegistry } from "./dataStoreRegistry.js";
25
25
  import { DeltaManagerPendingOpsProxy, DeltaManagerSummarizerProxy, } from "./deltaManagerProxies.js";
26
26
  import { GCNodeType, GarbageCollector, gcGenerationOptionName, } from "./gc/index.js";
27
27
  import { ContainerMessageType, } from "./messageTypes.js";
28
- import { ensureContentsDeserialized, OpCompressor, OpDecompressor, OpGroupingManager, OpSplitter, Outbox, RemoteMessageProcessor, } from "./opLifecycle/index.js";
28
+ import { DuplicateBatchDetector, ensureContentsDeserialized, OpCompressor, OpDecompressor, OpGroupingManager, OpSplitter, Outbox, RemoteMessageProcessor, } from "./opLifecycle/index.js";
29
29
  import { pkgVersion } from "./packageVersion.js";
30
30
  import { PendingStateManager, } from "./pendingStateManager.js";
31
31
  import { ScheduleManager } from "./scheduleManager.js";
@@ -228,15 +228,25 @@ function lastMessageFromMetadata(metadata) {
228
228
  * We only want to log this once, to avoid spamming telemetry if we are wrong and these cases are hit commonly.
229
229
  */
230
230
  let getSingleUseLegacyLogCallback = (logger, type) => {
231
- // We only want to log this once per ContainerRuntime instance, to avoid spamming telemetry.
232
- getSingleUseLegacyLogCallback = () => () => { };
233
231
  return (codePath) => {
234
232
  logger.sendTelemetryEvent({
235
233
  eventName: "LegacyMessageFormat",
236
234
  details: { codePath, type },
237
235
  });
236
+ // Now that we've logged, prevent future logging (globally).
237
+ getSingleUseLegacyLogCallback = () => () => { };
238
238
  };
239
239
  };
240
+ /**
241
+ * This is meant to be used by a {@link @fluidframework/container-definitions#IRuntimeFactory} to instantiate a container runtime.
242
+ * @param params - An object which specifies all required and optional params necessary to instantiate a runtime.
243
+ * @returns A runtime which provides all the functionality necessary to bind with the loader layer via the {@link @fluidframework/container-definitions#IRuntime} interface and provide a runtime environment via the {@link @fluidframework/container-runtime-definitions#IContainerRuntime} interface.
244
+ * @legacy
245
+ * @alpha
246
+ */
247
+ export async function loadContainerRuntime(params) {
248
+ return ContainerRuntime.loadRuntime(params);
249
+ }
240
250
  /**
241
251
  * Represents the runtime of the container. Contains helper functions/state of the container.
242
252
  * It will define the store level mappings.
@@ -588,11 +598,16 @@ export class ContainerRuntime extends TypedEventEmitter {
588
598
  this._disposed = false;
589
599
  this.emitDirtyDocumentEvent = true;
590
600
  this.defaultTelemetrySignalSampleCount = 100;
591
- this._perfSignalData = {
601
+ this._signalTracking = {
602
+ totalSignalsSentInLatencyWindow: 0,
592
603
  signalsLost: 0,
593
- signalSequenceNumber: 0,
604
+ signalsOutOfOrder: 0,
605
+ signalsSentSinceLastLatencyMeasurement: 0,
606
+ broadcastSignalSequenceNumber: 0,
594
607
  signalTimestamp: 0,
608
+ roundTripSignalSequenceNumber: undefined,
595
609
  trackingSignalSequenceNumber: undefined,
610
+ minimumTrackingSignalSequenceNumber: undefined,
596
611
  };
597
612
  /**
598
613
  * It a cache for holding mapping for loading groupIds with its snapshot from the service. Add expiry policy of 1 minute.
@@ -700,13 +715,14 @@ export class ContainerRuntime extends TypedEventEmitter {
700
715
  isAttached: () => this.attachState !== AttachState.Detached,
701
716
  }, pendingRuntimeState?.pending, this.logger);
702
717
  let outerDeltaManager;
703
- const useDeltaManagerOpsProxy = this.mc.config.getBoolean("Fluid.ContainerRuntime.DeltaManagerOpsProxy") !== false;
718
+ this.useDeltaManagerOpsProxy =
719
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.DeltaManagerOpsProxy") === true;
704
720
  // The summarizerDeltaManager Proxy is used to lie to the summarizer to convince it is in the right state as a summarizer client.
705
721
  const summarizerDeltaManagerProxy = new DeltaManagerSummarizerProxy(this.innerDeltaManager);
706
722
  outerDeltaManager = summarizerDeltaManagerProxy;
707
723
  // The DeltaManagerPendingOpsProxy is used to control the minimum sequence number
708
724
  // It allows us to lie to the layers below so that they can maintain enough local state for rebasing ops.
709
- if (useDeltaManagerOpsProxy) {
725
+ if (this.useDeltaManagerOpsProxy) {
710
726
  const pendingOpsDeltaManagerProxy = new DeltaManagerPendingOpsProxy(summarizerDeltaManagerProxy, this.pendingStateManager);
711
727
  outerDeltaManager = pendingOpsDeltaManagerProxy;
712
728
  }
@@ -737,6 +753,12 @@ export class ContainerRuntime extends TypedEventEmitter {
737
753
  this.closeFn(error);
738
754
  throw error;
739
755
  }
756
+ // DuplicateBatchDetection is only enabled if Offline Load is enabled
757
+ // It maintains a cache of all batchIds/sequenceNumbers within the collab window.
758
+ // Don't waste resources doing so if not needed.
759
+ if (this.offlineEnabled) {
760
+ this.duplicateBatchDetector = new DuplicateBatchDetector();
761
+ }
740
762
  if (context.attachState === AttachState.Attached) {
741
763
  const maxSnapshotCacheDurationMs = this._storage?.policies?.maximumCacheDurationMs;
742
764
  if (maxSnapshotCacheDurationMs !== undefined &&
@@ -1134,13 +1156,9 @@ export class ContainerRuntime extends TypedEventEmitter {
1134
1156
  let childTree = snapshotTree;
1135
1157
  for (const part of pathParts) {
1136
1158
  if (hasIsolatedChannels) {
1137
- // TODO Why are we non null asserting here
1138
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1139
- childTree = childTree.trees[channelsTreeName];
1159
+ childTree = childTree?.trees[channelsTreeName];
1140
1160
  }
1141
- // TODO Why are we non null asserting here
1142
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1143
- childTree = childTree.trees[part];
1161
+ childTree = childTree?.trees[part];
1144
1162
  }
1145
1163
  return childTree;
1146
1164
  }
@@ -1187,8 +1205,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1187
1205
  return this.resolveHandle(requestParser.createSubRequest(1));
1188
1206
  }
1189
1207
  if (id === blobManagerBasePath && requestParser.isLeaf(2)) {
1190
- // TODO why are we non null asserting here?
1191
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1192
1208
  const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
1193
1209
  return blob
1194
1210
  ? {
@@ -1461,9 +1477,14 @@ export class ContainerRuntime extends TypedEventEmitter {
1461
1477
  }
1462
1478
  this._connected = connected;
1463
1479
  if (!connected) {
1464
- this._perfSignalData.signalsLost = 0;
1465
- this._perfSignalData.signalTimestamp = 0;
1466
- this._perfSignalData.trackingSignalSequenceNumber = undefined;
1480
+ this._signalTracking.signalsLost = 0;
1481
+ this._signalTracking.signalsOutOfOrder = 0;
1482
+ this._signalTracking.signalTimestamp = 0;
1483
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement = 0;
1484
+ this._signalTracking.totalSignalsSentInLatencyWindow = 0;
1485
+ this._signalTracking.roundTripSignalSequenceNumber = undefined;
1486
+ this._signalTracking.trackingSignalSequenceNumber = undefined;
1487
+ this._signalTracking.minimumTrackingSignalSequenceNumber = undefined;
1467
1488
  }
1468
1489
  else {
1469
1490
  assert(this.attachState === AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
@@ -1510,158 +1531,159 @@ export class ContainerRuntime extends TypedEventEmitter {
1510
1531
  if (hasModernRuntimeMessageEnvelope) {
1511
1532
  // If the message has the modern message envelope, then process it here.
1512
1533
  // Here we unpack the message (decompress, unchunk, and/or ungroup) into a batch of messages with ContainerMessageType
1513
- const inboundBatch = this.remoteMessageProcessor.process(messageCopy, logLegacyCase);
1514
- if (inboundBatch === undefined) {
1534
+ const inboundResult = this.remoteMessageProcessor.process(messageCopy, logLegacyCase);
1535
+ if (inboundResult === undefined) {
1515
1536
  // This means the incoming message is an incomplete part of a message or batch
1516
1537
  // and we need to process more messages before the rest of the system can understand it.
1517
1538
  return;
1518
1539
  }
1519
- // Reach out to PendingStateManager to zip localOpMetadata into the message list if it's a local batch
1520
- const messagesWithPendingState = this.pendingStateManager.processInboundBatch(inboundBatch, local);
1521
- if (messagesWithPendingState.length > 0) {
1522
- messagesWithPendingState.forEach(({ message, localOpMetadata }) => {
1523
- const msg = {
1524
- message,
1525
- local,
1526
- isRuntimeMessage: true,
1527
- savedOp,
1528
- localOpMetadata,
1529
- };
1530
- this.ensureNoDataModelChanges(() => this.processRuntimeMessage(msg));
1531
- });
1540
+ if ("batchStart" in inboundResult) {
1541
+ const batchStart = inboundResult.batchStart;
1542
+ const result = this.duplicateBatchDetector?.processInboundBatch(batchStart);
1543
+ if (result?.duplicate) {
1544
+ const error = new DataCorruptionError("Duplicate batch - The same batch was sequenced twice", { batchId: batchStart.batchId });
1545
+ this.mc.logger.sendTelemetryEvent({
1546
+ eventName: "DuplicateBatch",
1547
+ details: {
1548
+ batchId: batchStart.batchId,
1549
+ clientId: batchStart.clientId,
1550
+ batchStartCsn: batchStart.batchStartCsn,
1551
+ size: inboundResult.length,
1552
+ duplicateBatchSequenceNumber: result.otherSequenceNumber,
1553
+ ...extractSafePropertiesFromMessage(batchStart.keyMessage),
1554
+ },
1555
+ }, error);
1556
+ throw error;
1557
+ }
1532
1558
  }
1533
- else {
1534
- this.ensureNoDataModelChanges(() => this.processEmptyBatch(inboundBatch, local));
1559
+ let runtimeBatch = true;
1560
+ // Reach out to PendingStateManager, either to zip localOpMetadata into the *local* message list,
1561
+ // or to check to ensure the *remote* messages don't match the batchId of a pending local batch.
1562
+ // This latter case would indicate that the container has forked - two copies are trying to persist the same local changes.
1563
+ let messagesWithPendingState = this.pendingStateManager.processInboundMessages(inboundResult, local);
1564
+ if (inboundResult.type !== "fullBatch") {
1565
+ assert(messagesWithPendingState.length === 1, 0xa3d /* Partial batch should have exactly one message */);
1566
+ }
1567
+ if (messagesWithPendingState.length === 0) {
1568
+ assert(inboundResult.type === "fullBatch", 0xa3e /* Empty batch is always considered a full batch */);
1569
+ /**
1570
+ * We need to process an empty batch, which will execute expected actions while processing even if there
1571
+ * are no inner runtime messages.
1572
+ *
1573
+ * Empty batches are produced by the outbox on resubmit when the resubmit flow resulted in no runtime
1574
+ * messages.
1575
+ * This can happen if changes from a remote client "cancel out" the pending changes being resubmitted by
1576
+ * this client. We submit an empty batch if "offline load" (aka rehydrating from stashed state) is
1577
+ * enabled, to ensure we account for this batch when comparing batchIds, checking for a forked container.
1578
+ * Otherwise, we would not realize this container has forked in the case where it did fork, and a batch
1579
+ * became empty but wasn't submitted as such.
1580
+ */
1581
+ messagesWithPendingState = [
1582
+ {
1583
+ message: inboundResult.batchStart.keyMessage,
1584
+ localOpMetadata: undefined,
1585
+ },
1586
+ ];
1587
+ // Empty batch message is a non-runtime message as it was generated by the op grouping manager.
1588
+ runtimeBatch = false;
1535
1589
  }
1590
+ const locationInBatch = inboundResult.type === "fullBatch"
1591
+ ? { batchStart: true, batchEnd: true }
1592
+ : inboundResult.type === "batchStartingMessage"
1593
+ ? { batchStart: true, batchEnd: false }
1594
+ : { batchStart: false, batchEnd: inboundResult.batchEnd === true };
1595
+ this.processInboundMessages(messagesWithPendingState, locationInBatch, local, savedOp, runtimeBatch);
1536
1596
  }
1537
1597
  else {
1538
- // Check if message.type is one of values in ContainerMessageType
1539
- // eslint-disable-next-line import/no-deprecated
1540
- if (isRuntimeMessage(messageCopy)) {
1541
- // Legacy op received
1542
- this.ensureNoDataModelChanges(() => this.processRuntimeMessage({
1543
- message: messageCopy,
1544
- local,
1545
- isRuntimeMessage: true,
1546
- savedOp,
1547
- }));
1548
- }
1549
- else {
1550
- // A non container runtime message (like other system ops - join, ack, leave, nack etc.)
1551
- this.ensureNoDataModelChanges(() => this.observeNonRuntimeMessage({
1552
- message: messageCopy,
1553
- local,
1554
- isRuntimeMessage: false,
1555
- savedOp,
1556
- }));
1557
- }
1598
+ this.processInboundMessages([{ message: messageCopy, localOpMetadata: undefined }], { batchStart: true, batchEnd: true }, // Single message
1599
+ local, savedOp, isRuntimeMessage(messageCopy) /* runtimeBatch */);
1600
+ }
1601
+ if (local) {
1602
+ // If we have processed a local op, this means that the container is
1603
+ // making progress and we can reset the counter for how many times
1604
+ // we have consecutively replayed the pending states
1605
+ this.resetReconnectCount();
1558
1606
  }
1559
1607
  }
1560
1608
  /**
1561
- * Processes messages that are intended for the runtime layer to process.
1562
- * It redirects the message to the correct subsystem for processing, and implement other side effects
1563
- * @param messageWithContext - message to process with additional context and isRuntimeMessage prop as true
1609
+ * Processes inbound message(s). It calls schedule manager according to the messages' location in the batch.
1610
+ * @param messages - messages to process.
1611
+ * @param locationInBatch - Are we processing the start and/or end of a batch?
1612
+ * @param local - true if the messages were originally generated by the client receiving it.
1613
+ * @param savedOp - true if the message is a replayed saved op.
1614
+ * @param runtimeBatch - true if these are runtime messages.
1564
1615
  */
1565
- processRuntimeMessage(messageWithContext) {
1566
- const { message, local, localOpMetadata } = messageWithContext;
1567
- // Intercept to reduce minimum sequence number to the delta manager's minimum sequence number.
1568
- // Sequence numbers are not guaranteed to follow any sort of order. Re-entrancy is one of those situations
1569
- if (this.deltaManager.minimumSequenceNumber <
1570
- messageWithContext.message.minimumSequenceNumber) {
1571
- messageWithContext.message.minimumSequenceNumber =
1572
- this.deltaManager.minimumSequenceNumber;
1573
- }
1574
- // Surround the actual processing of the operation with messages to the schedule manager indicating
1575
- // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
1576
- // messages once a batch has been fully processed.
1577
- this.scheduleManager.beforeOpProcessing(message);
1578
- this._processedClientSequenceNumber = message.clientSequenceNumber;
1616
+ processInboundMessages(messages, locationInBatch, local, savedOp, runtimeBatch) {
1617
+ if (locationInBatch.batchStart) {
1618
+ const firstMessage = messages[0]?.message;
1619
+ assert(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
1620
+ this.scheduleManager.batchBegin(firstMessage);
1621
+ }
1622
+ let error;
1579
1623
  try {
1580
- // RemoteMessageProcessor would have already reconstituted Chunked Ops into the original op type
1581
- assert(message.type !== ContainerMessageType.ChunkedOp, 0x93b /* we should never get here with chunked ops */);
1582
- // If there are no more pending messages after processing a local message,
1583
- // the document is no longer dirty.
1584
- if (!this.hasPendingMessages()) {
1585
- this.updateDocumentDirtyState(false);
1586
- }
1587
- this.validateAndProcessRuntimeMessage(messageWithContext, localOpMetadata);
1588
- this.emit("op", message, messageWithContext.isRuntimeMessage);
1589
- this.scheduleManager.afterOpProcessing(undefined, message);
1590
- if (local) {
1591
- // If we have processed a local op, this means that the container is
1592
- // making progress and we can reset the counter for how many times
1593
- // we have consecutively replayed the pending states
1594
- this.resetReconnectCount();
1595
- }
1624
+ messages.forEach(({ message, localOpMetadata }) => {
1625
+ this.ensureNoDataModelChanges(() => {
1626
+ if (runtimeBatch) {
1627
+ this.validateAndProcessRuntimeMessage({
1628
+ message: message,
1629
+ local,
1630
+ savedOp,
1631
+ localOpMetadata,
1632
+ });
1633
+ }
1634
+ else {
1635
+ this.observeNonRuntimeMessage(message);
1636
+ }
1637
+ });
1638
+ });
1596
1639
  }
1597
1640
  catch (e) {
1598
- this.scheduleManager.afterOpProcessing(e, message);
1599
- throw e;
1600
- }
1601
- }
1602
- /**
1603
- * Process an empty batch, which will execute expected actions while processing even if there are no messages.
1604
- * This is a separate function because the processCore function expects at least one message to process.
1605
- * It is expected to happen only when the outbox produces an empty batch due to a resubmit flow.
1606
- */
1607
- processEmptyBatch(emptyBatch, local) {
1608
- const { emptyBatchSequenceNumber: sequenceNumber, batchStartCsn } = emptyBatch;
1609
- assert(sequenceNumber !== undefined, 0x9fa /* emptyBatchSequenceNumber must be defined */);
1610
- this.emit("batchBegin", { sequenceNumber });
1611
- this._processedClientSequenceNumber = batchStartCsn;
1612
- if (!this.hasPendingMessages()) {
1613
- this.updateDocumentDirtyState(false);
1641
+ error = e;
1642
+ throw error;
1614
1643
  }
1615
- this.emit("batchEnd", undefined, { sequenceNumber });
1616
- if (local) {
1617
- this.resetReconnectCount();
1644
+ finally {
1645
+ if (locationInBatch.batchEnd) {
1646
+ const lastMessage = messages[messages.length - 1]?.message;
1647
+ assert(lastMessage !== undefined, 0xa32 /* Batch must have at least one message */);
1648
+ this.scheduleManager.batchEnd(error, lastMessage);
1649
+ }
1618
1650
  }
1619
1651
  }
1620
1652
  /**
1621
1653
  * Observes messages that are not intended for the runtime layer, updating/notifying Runtime systems as needed.
1622
- * @param messageWithContext - non-runtime messages to process with additional context and isRuntimeMessage prop as false
1654
+ * @param message - non-runtime message to process.
1623
1655
  */
1624
- observeNonRuntimeMessage(messageWithContext) {
1625
- const { message, local } = messageWithContext;
1626
- // Intercept to reduce minimum sequence number to the delta manager's minimum sequence number.
1627
- // Sequence numbers are not guaranteed to follow any sort of order. Re-entrancy is one of those situations
1628
- if (this.deltaManager.minimumSequenceNumber <
1629
- messageWithContext.message.minimumSequenceNumber) {
1630
- messageWithContext.message.minimumSequenceNumber =
1631
- this.deltaManager.minimumSequenceNumber;
1632
- }
1633
- // Surround the actual processing of the operation with messages to the schedule manager indicating
1634
- // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
1635
- // messages once a batch has been fully processed.
1636
- this.scheduleManager.beforeOpProcessing(message);
1637
- this._processedClientSequenceNumber = message.clientSequenceNumber;
1638
- try {
1639
- // If there are no more pending messages after processing a local message,
1640
- // the document is no longer dirty.
1641
- if (!this.hasPendingMessages()) {
1642
- this.updateDocumentDirtyState(false);
1643
- }
1644
- this.emit("op", message, messageWithContext.isRuntimeMessage);
1645
- this.scheduleManager.afterOpProcessing(undefined, message);
1646
- if (local) {
1647
- // If we have processed a local op, this means that the container is
1648
- // making progress and we can reset the counter for how many times
1649
- // we have consecutively replayed the pending states
1650
- this.resetReconnectCount();
1651
- }
1656
+ observeNonRuntimeMessage(message) {
1657
+ // Set the minimum sequence number to the containerRuntime's understanding of minimum sequence number.
1658
+ if (this.deltaManager.minimumSequenceNumber < message.minimumSequenceNumber) {
1659
+ message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
1652
1660
  }
1653
- catch (e) {
1654
- this.scheduleManager.afterOpProcessing(e, message);
1655
- throw e;
1661
+ this._processedClientSequenceNumber = message.clientSequenceNumber;
1662
+ // If there are no more pending messages after processing a local message,
1663
+ // the document is no longer dirty.
1664
+ if (!this.hasPendingMessages()) {
1665
+ this.updateDocumentDirtyState(false);
1656
1666
  }
1667
+ this.emit("op", message, false /* runtimeMessage */);
1657
1668
  }
1658
1669
  /**
1659
1670
  * Assuming the given message is also a TypedContainerRuntimeMessage,
1660
1671
  * checks its type and dispatches the message to the appropriate handler in the runtime.
1661
1672
  * Throws a DataProcessingError if the message looks like but doesn't conform to a known TypedContainerRuntimeMessage type.
1662
1673
  */
1663
- validateAndProcessRuntimeMessage(messageWithContext, localOpMetadata) {
1664
- const { local, message, savedOp } = messageWithContext;
1674
+ validateAndProcessRuntimeMessage(messageWithContext) {
1675
+ const { local, message, savedOp, localOpMetadata } = messageWithContext;
1676
+ // Set the minimum sequence number to the containerRuntime's understanding of minimum sequence number.
1677
+ if (this.useDeltaManagerOpsProxy &&
1678
+ this.deltaManager.minimumSequenceNumber < message.minimumSequenceNumber) {
1679
+ message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
1680
+ }
1681
+ this._processedClientSequenceNumber = message.clientSequenceNumber;
1682
+ // If there are no more pending messages after processing a local message,
1683
+ // the document is no longer dirty.
1684
+ if (!this.hasPendingMessages()) {
1685
+ this.updateDocumentDirtyState(false);
1686
+ }
1665
1687
  switch (message.type) {
1666
1688
  case ContainerMessageType.Attach:
1667
1689
  case ContainerMessageType.Alias:
@@ -1722,20 +1744,25 @@ export class ContainerRuntime extends TypedEventEmitter {
1722
1744
  }
1723
1745
  }
1724
1746
  }
1747
+ this.emit("op", message, true /* runtimeMessage */);
1725
1748
  }
1726
1749
  /**
1727
1750
  * Emits the Signal event and update the perf signal data.
1728
- * @param clientSignalSequenceNumber - is the client signal sequence number to be uploaded.
1729
1751
  */
1730
- sendSignalTelemetryEvent(clientSignalSequenceNumber) {
1731
- const duration = Date.now() - this._perfSignalData.signalTimestamp;
1752
+ sendSignalTelemetryEvent() {
1753
+ const duration = Date.now() - this._signalTracking.signalTimestamp;
1732
1754
  this.mc.logger.sendPerformanceEvent({
1733
1755
  eventName: "SignalLatency",
1734
- duration,
1735
- signalsLost: this._perfSignalData.signalsLost,
1756
+ duration, // Roundtrip duration of the tracked signal in milliseconds.
1757
+ signalsSent: this._signalTracking.totalSignalsSentInLatencyWindow, // Signals sent since the last logged SignalLatency event.
1758
+ signalsLost: this._signalTracking.signalsLost, // Signals lost since the last logged SignalLatency event.
1759
+ outOfOrderSignals: this._signalTracking.signalsOutOfOrder, // Out of order signals since the last logged SignalLatency event.
1760
+ reconnectCount: this.consecutiveReconnects, // Container reconnect count.
1736
1761
  });
1737
- this._perfSignalData.signalsLost = 0;
1738
- this._perfSignalData.signalTimestamp = 0;
1762
+ this._signalTracking.signalsLost = 0;
1763
+ this._signalTracking.signalsOutOfOrder = 0;
1764
+ this._signalTracking.signalTimestamp = 0;
1765
+ this._signalTracking.totalSignalsSentInLatencyWindow = 0;
1739
1766
  }
1740
1767
  processSignal(message, local) {
1741
1768
  const envelope = message.content;
@@ -1743,29 +1770,55 @@ export class ContainerRuntime extends TypedEventEmitter {
1743
1770
  clientId: message.clientId,
1744
1771
  content: envelope.contents.content,
1745
1772
  type: envelope.contents.type,
1773
+ targetClientId: message.targetClientId,
1746
1774
  };
1747
- // Only collect signal telemetry for messages sent by the current client.
1748
- if (message.clientId === this.clientId && this.connected) {
1749
- // Check to see if the signal was lost.
1750
- if (this._perfSignalData.trackingSignalSequenceNumber !== undefined &&
1751
- envelope.clientSignalSequenceNumber > this._perfSignalData.trackingSignalSequenceNumber) {
1752
- this._perfSignalData.signalsLost++;
1753
- this._perfSignalData.trackingSignalSequenceNumber = undefined;
1754
- this.mc.logger.sendErrorEvent({
1755
- eventName: "SignalLost",
1756
- type: envelope.contents.type,
1757
- signalsLost: this._perfSignalData.signalsLost,
1758
- trackingSequenceNumber: this._perfSignalData.trackingSignalSequenceNumber,
1759
- clientSignalSequenceNumber: envelope.clientSignalSequenceNumber,
1760
- });
1761
- }
1762
- else if (envelope.clientSignalSequenceNumber ===
1763
- this._perfSignalData.trackingSignalSequenceNumber) {
1764
- // only logging for the first connection and the trackingSignalSequenceNUmber.
1765
- if (this.consecutiveReconnects === 0) {
1766
- this.sendSignalTelemetryEvent(envelope.clientSignalSequenceNumber);
1775
+ // Only collect signal telemetry for broadcast messages sent by the current client.
1776
+ if (message.clientId === this.clientId &&
1777
+ this.connected &&
1778
+ envelope.clientBroadcastSignalSequenceNumber !== undefined) {
1779
+ if (this._signalTracking.trackingSignalSequenceNumber !== undefined &&
1780
+ this._signalTracking.minimumTrackingSignalSequenceNumber !== undefined) {
1781
+ if (envelope.clientBroadcastSignalSequenceNumber >=
1782
+ this._signalTracking.trackingSignalSequenceNumber) {
1783
+ // Calculate the number of signals lost and log the event.
1784
+ const signalsLost = envelope.clientBroadcastSignalSequenceNumber -
1785
+ this._signalTracking.trackingSignalSequenceNumber;
1786
+ if (signalsLost > 0) {
1787
+ this._signalTracking.signalsLost += signalsLost;
1788
+ this.mc.logger.sendErrorEvent({
1789
+ eventName: "SignalLost",
1790
+ signalsLost, // Number of lost signals detected.
1791
+ trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
1792
+ clientBroadcastSignalSequenceNumber: envelope.clientBroadcastSignalSequenceNumber, // Actual signal sequence number received.
1793
+ });
1794
+ }
1795
+ // Update the tracking signal sequence number to the next expected signal in the sequence.
1796
+ this._signalTracking.trackingSignalSequenceNumber =
1797
+ envelope.clientBroadcastSignalSequenceNumber + 1;
1798
+ }
1799
+ else if (envelope.clientBroadcastSignalSequenceNumber >=
1800
+ this._signalTracking.minimumTrackingSignalSequenceNumber) {
1801
+ this._signalTracking.signalsOutOfOrder++;
1802
+ this.mc.logger.sendTelemetryEvent({
1803
+ eventName: "SignalOutOfOrder",
1804
+ type: envelope.contents.type, // Type of signal that was received out of order.
1805
+ trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
1806
+ clientBroadcastSignalSequenceNumber: envelope.clientBroadcastSignalSequenceNumber, // Sequence number of the out of order signal.
1807
+ });
1808
+ }
1809
+ if (this._signalTracking.roundTripSignalSequenceNumber !== undefined &&
1810
+ envelope.clientBroadcastSignalSequenceNumber >=
1811
+ this._signalTracking.roundTripSignalSequenceNumber) {
1812
+ if (envelope.clientBroadcastSignalSequenceNumber ===
1813
+ this._signalTracking.roundTripSignalSequenceNumber) {
1814
+ // Latency tracked signal has been received.
1815
+ // We now log the roundtrip duration of the tracked signal.
1816
+ // This telemetry event also logs metrics for signals sent, signals lost, and out of order signals received.
1817
+ // These metrics are reset after logging the telemetry event.
1818
+ this.sendSignalTelemetryEvent();
1819
+ }
1820
+ this._signalTracking.roundTripSignalSequenceNumber = undefined;
1767
1821
  }
1768
- this._perfSignalData.trackingSignalSequenceNumber = undefined;
1769
1822
  }
1770
1823
  }
1771
1824
  if (envelope.address === undefined) {
@@ -1941,18 +1994,36 @@ export class ContainerRuntime extends TypedEventEmitter {
1941
1994
  }
1942
1995
  return true;
1943
1996
  }
1944
- createNewSignalEnvelope(address, type, content) {
1945
- const newSequenceNumber = ++this._perfSignalData.signalSequenceNumber;
1997
+ createNewSignalEnvelope(address, type, content, targetClientId) {
1946
1998
  const newEnvelope = {
1947
1999
  address,
1948
- clientSignalSequenceNumber: newSequenceNumber,
1949
2000
  contents: { type, content },
1950
2001
  };
1951
- // We should not track any signals in case we already have a tracking number.
1952
- if (newSequenceNumber % this.defaultTelemetrySignalSampleCount === 1 &&
1953
- this._perfSignalData.trackingSignalSequenceNumber === undefined) {
1954
- this._perfSignalData.signalTimestamp = Date.now();
1955
- this._perfSignalData.trackingSignalSequenceNumber = newSequenceNumber;
2002
+ const isBroadcastSignal = targetClientId === undefined;
2003
+ if (isBroadcastSignal) {
2004
+ const clientBroadcastSignalSequenceNumber = ++this._signalTracking
2005
+ .broadcastSignalSequenceNumber;
2006
+ newEnvelope.clientBroadcastSignalSequenceNumber = clientBroadcastSignalSequenceNumber;
2007
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement++;
2008
+ if (this._signalTracking.minimumTrackingSignalSequenceNumber === undefined ||
2009
+ this._signalTracking.trackingSignalSequenceNumber === undefined) {
2010
+ // Signal monitoring window is undefined
2011
+ // Initialize tracking to expect the next signal sent by the connected client.
2012
+ this._signalTracking.minimumTrackingSignalSequenceNumber =
2013
+ clientBroadcastSignalSequenceNumber;
2014
+ this._signalTracking.trackingSignalSequenceNumber =
2015
+ clientBroadcastSignalSequenceNumber;
2016
+ }
2017
+ // We should not track the round trip of a new signal in the case we are already tracking one.
2018
+ if (clientBroadcastSignalSequenceNumber % this.defaultTelemetrySignalSampleCount === 1 &&
2019
+ this._signalTracking.roundTripSignalSequenceNumber === undefined) {
2020
+ this._signalTracking.signalTimestamp = Date.now();
2021
+ this._signalTracking.roundTripSignalSequenceNumber =
2022
+ clientBroadcastSignalSequenceNumber;
2023
+ this._signalTracking.totalSignalsSentInLatencyWindow +=
2024
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement;
2025
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement = 0;
2026
+ }
1956
2027
  }
1957
2028
  return newEnvelope;
1958
2029
  }
@@ -1961,10 +2032,16 @@ export class ContainerRuntime extends TypedEventEmitter {
1961
2032
  * @param type - Type of the signal.
1962
2033
  * @param content - Content of the signal. Should be a JSON serializable object or primitive.
1963
2034
  * @param targetClientId - When specified, the signal is only sent to the provided client id.
2035
+ *
2036
+ * @remarks
2037
+ *
2038
+ * The `targetClientId` parameter here is currently intended for internal testing purposes only.
2039
+ * Support for this option at container runtime is planned to be deprecated in the future.
2040
+ *
1964
2041
  */
1965
2042
  submitSignal(type, content, targetClientId) {
1966
2043
  this.verifyNotClosed();
1967
- const envelope = this.createNewSignalEnvelope(undefined /* address */, type, content);
2044
+ const envelope = this.createNewSignalEnvelope(undefined /* address */, type, content, targetClientId);
1968
2045
  return this.submitSignalFn(envelope, targetClientId);
1969
2046
  }
1970
2047
  setAttachState(attachState) {
@@ -2367,8 +2444,6 @@ export class ContainerRuntime extends TypedEventEmitter {
2367
2444
  // Counting dataStores and handles
2368
2445
  // Because handles are unchanged dataStores in the current logic,
2369
2446
  // summarized dataStore count is total dataStore count minus handle count
2370
- // TODO why are we non null asserting here
2371
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2372
2447
  const dataStoreTree = summaryTree.tree[channelsTreeName];
2373
2448
  assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
2374
2449
  const handleCount = Object.values(dataStoreTree.tree).filter((value) => value.type === SummaryType.Handle).length;
@@ -2691,6 +2766,12 @@ export class ContainerRuntime extends TypedEventEmitter {
2691
2766
  throw new Error("Runtime is closed");
2692
2767
  }
2693
2768
  }
2769
+ /**
2770
+ * Resubmits each message in the batch, and then flushes the outbox.
2771
+ *
2772
+ * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
2773
+ * for correlation to detect container forking.
2774
+ */
2694
2775
  reSubmitBatch(batch, batchId) {
2695
2776
  this.orderSequentially(() => {
2696
2777
  for (const message of batch) {