@fluidframework/container-runtime 2.4.0 → 2.5.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 (95) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/api-report/container-runtime.legacy.alpha.api.md +3 -1
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/blobManager/blobManager.d.ts +3 -3
  5. package/dist/blobManager/blobManager.d.ts.map +1 -1
  6. package/dist/blobManager/blobManager.js +1 -1
  7. package/dist/blobManager/blobManager.js.map +1 -1
  8. package/dist/channelCollection.d.ts +20 -5
  9. package/dist/channelCollection.d.ts.map +1 -1
  10. package/dist/channelCollection.js +183 -119
  11. package/dist/channelCollection.js.map +1 -1
  12. package/dist/containerRuntime.d.ts +12 -4
  13. package/dist/containerRuntime.d.ts.map +1 -1
  14. package/dist/containerRuntime.js +155 -66
  15. package/dist/containerRuntime.js.map +1 -1
  16. package/dist/dataStoreContext.d.ts +15 -3
  17. package/dist/dataStoreContext.d.ts.map +1 -1
  18. package/dist/dataStoreContext.js +48 -19
  19. package/dist/dataStoreContext.js.map +1 -1
  20. package/dist/gc/garbageCollection.d.ts +5 -6
  21. package/dist/gc/garbageCollection.d.ts.map +1 -1
  22. package/dist/gc/garbageCollection.js +23 -22
  23. package/dist/gc/garbageCollection.js.map +1 -1
  24. package/dist/gc/gcDefinitions.d.ts +2 -2
  25. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  26. package/dist/gc/gcDefinitions.js.map +1 -1
  27. package/dist/opLifecycle/outbox.d.ts +3 -0
  28. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  29. package/dist/opLifecycle/outbox.js +9 -0
  30. package/dist/opLifecycle/outbox.js.map +1 -1
  31. package/dist/opLifecycle/remoteMessageProcessor.d.ts +1 -0
  32. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  33. package/dist/opLifecycle/remoteMessageProcessor.js +2 -0
  34. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  35. package/dist/opProperties.js +1 -1
  36. package/dist/opProperties.js.map +1 -1
  37. package/dist/packageVersion.d.ts +1 -1
  38. package/dist/packageVersion.js +1 -1
  39. package/dist/packageVersion.js.map +1 -1
  40. package/dist/summary/documentSchema.d.ts +11 -0
  41. package/dist/summary/documentSchema.d.ts.map +1 -1
  42. package/dist/summary/documentSchema.js +45 -30
  43. package/dist/summary/documentSchema.js.map +1 -1
  44. package/lib/blobManager/blobManager.d.ts +3 -3
  45. package/lib/blobManager/blobManager.d.ts.map +1 -1
  46. package/lib/blobManager/blobManager.js +1 -1
  47. package/lib/blobManager/blobManager.js.map +1 -1
  48. package/lib/channelCollection.d.ts +20 -5
  49. package/lib/channelCollection.d.ts.map +1 -1
  50. package/lib/channelCollection.js +183 -119
  51. package/lib/channelCollection.js.map +1 -1
  52. package/lib/containerRuntime.d.ts +12 -4
  53. package/lib/containerRuntime.d.ts.map +1 -1
  54. package/lib/containerRuntime.js +155 -66
  55. package/lib/containerRuntime.js.map +1 -1
  56. package/lib/dataStoreContext.d.ts +15 -3
  57. package/lib/dataStoreContext.d.ts.map +1 -1
  58. package/lib/dataStoreContext.js +48 -19
  59. package/lib/dataStoreContext.js.map +1 -1
  60. package/lib/gc/garbageCollection.d.ts +5 -6
  61. package/lib/gc/garbageCollection.d.ts.map +1 -1
  62. package/lib/gc/garbageCollection.js +23 -22
  63. package/lib/gc/garbageCollection.js.map +1 -1
  64. package/lib/gc/gcDefinitions.d.ts +2 -2
  65. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  66. package/lib/gc/gcDefinitions.js.map +1 -1
  67. package/lib/opLifecycle/outbox.d.ts +3 -0
  68. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  69. package/lib/opLifecycle/outbox.js +9 -0
  70. package/lib/opLifecycle/outbox.js.map +1 -1
  71. package/lib/opLifecycle/remoteMessageProcessor.d.ts +1 -0
  72. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  73. package/lib/opLifecycle/remoteMessageProcessor.js +2 -0
  74. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  75. package/lib/opProperties.js +1 -1
  76. package/lib/opProperties.js.map +1 -1
  77. package/lib/packageVersion.d.ts +1 -1
  78. package/lib/packageVersion.js +1 -1
  79. package/lib/packageVersion.js.map +1 -1
  80. package/lib/summary/documentSchema.d.ts +11 -0
  81. package/lib/summary/documentSchema.d.ts.map +1 -1
  82. package/lib/summary/documentSchema.js +45 -30
  83. package/lib/summary/documentSchema.js.map +1 -1
  84. package/package.json +24 -24
  85. package/src/blobManager/blobManager.ts +2 -2
  86. package/src/channelCollection.ts +227 -160
  87. package/src/containerRuntime.ts +197 -80
  88. package/src/dataStoreContext.ts +66 -23
  89. package/src/gc/garbageCollection.ts +32 -32
  90. package/src/gc/gcDefinitions.ts +3 -3
  91. package/src/opLifecycle/outbox.ts +12 -0
  92. package/src/opLifecycle/remoteMessageProcessor.ts +3 -0
  93. package/src/opProperties.ts +1 -1
  94. package/src/packageVersion.ts +1 -1
  95. package/src/summary/documentSchema.ts +58 -39
@@ -89,6 +89,7 @@ import {
89
89
  channelsTreeName,
90
90
  gcTreeKey,
91
91
  IInboundSignalMessage,
92
+ type IRuntimeMessagesContent,
92
93
  } from "@fluidframework/runtime-definitions/internal";
93
94
  import {
94
95
  GCDataBuilder,
@@ -105,6 +106,7 @@ import {
105
106
  import type {
106
107
  IFluidErrorBase,
107
108
  ITelemetryGenericEventExt,
109
+ TelemetryEventPropertyTypeExt,
108
110
  } from "@fluidframework/telemetry-utils/internal";
109
111
  import {
110
112
  ITelemetryLoggerExt,
@@ -159,6 +161,7 @@ import {
159
161
  IGCStats,
160
162
  IGarbageCollector,
161
163
  gcGenerationOptionName,
164
+ type GarbageCollectionMessage,
162
165
  } from "./gc/index.js";
163
166
  import {
164
167
  ContainerMessageType,
@@ -201,6 +204,7 @@ import {
201
204
  IConnectableRuntime,
202
205
  IContainerRuntimeMetadata,
203
206
  ICreateContainerMetadata,
207
+ type IDocumentSchemaChangeMessage,
204
208
  type IDocumentSchemaCurrent,
205
209
  IEnqueueSummarizeOptions,
206
210
  IGenerateSummaryTreeResult,
@@ -2810,6 +2814,9 @@ export class ContainerRuntime
2810
2814
  local,
2811
2815
  savedOp,
2812
2816
  runtimeBatch,
2817
+ inboundResult.type === "fullBatch"
2818
+ ? inboundResult.groupedBatch
2819
+ : false /* groupedBatch */,
2813
2820
  );
2814
2821
  } else {
2815
2822
  if (!runtimeBatch) {
@@ -2829,6 +2836,7 @@ export class ContainerRuntime
2829
2836
  local,
2830
2837
  savedOp,
2831
2838
  runtimeBatch,
2839
+ false /* groupedBatch */,
2832
2840
  );
2833
2841
  }
2834
2842
 
@@ -2844,14 +2852,15 @@ export class ContainerRuntime
2844
2852
 
2845
2853
  /**
2846
2854
  * Processes inbound message(s). It calls schedule manager according to the messages' location in the batch.
2847
- * @param messages - messages to process.
2855
+ * @param messagesWithMetadata - messages to process along with their metadata.
2848
2856
  * @param locationInBatch - Are we processing the start and/or end of a batch?
2849
2857
  * @param local - true if the messages were originally generated by the client receiving it.
2850
2858
  * @param savedOp - true if the message is a replayed saved op.
2851
2859
  * @param runtimeBatch - true if these are runtime messages.
2860
+ * @param groupedBatch - true if these messages are part of a grouped op batch.
2852
2861
  */
2853
2862
  private processInboundMessages(
2854
- messages: {
2863
+ messagesWithMetadata: {
2855
2864
  message: ISequencedDocumentMessage;
2856
2865
  localOpMetadata?: unknown;
2857
2866
  }[],
@@ -2859,35 +2868,110 @@ export class ContainerRuntime
2859
2868
  local: boolean,
2860
2869
  savedOp: boolean | undefined,
2861
2870
  runtimeBatch: boolean,
2871
+ groupedBatch: boolean,
2862
2872
  ) {
2863
2873
  if (locationInBatch.batchStart) {
2864
- const firstMessage = messages[0]?.message;
2874
+ const firstMessage = messagesWithMetadata[0]?.message;
2865
2875
  assert(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
2866
2876
  this.scheduleManager.batchBegin(firstMessage);
2867
2877
  }
2868
2878
 
2869
2879
  let error: unknown;
2870
2880
  try {
2871
- messages.forEach(({ message, localOpMetadata }) => {
2872
- this.ensureNoDataModelChanges(() => {
2873
- if (runtimeBatch) {
2874
- this.validateAndProcessRuntimeMessage({
2875
- message: message as InboundSequencedContainerRuntimeMessage,
2881
+ if (!runtimeBatch) {
2882
+ messagesWithMetadata.forEach(({ message }) => {
2883
+ this.ensureNoDataModelChanges(() => {
2884
+ this.observeNonRuntimeMessage(message);
2885
+ });
2886
+ });
2887
+ return;
2888
+ }
2889
+
2890
+ // Helper that updates a message's minimum sequence number to the minimum sequence number that container
2891
+ // runtime is tracking and sets _processedClientSequenceNumber. It returns the updated message.
2892
+ const updateSequenceNumbers = (message: ISequencedDocumentMessage) => {
2893
+ // Set the minimum sequence number to the containerRuntime's understanding of minimum sequence number.
2894
+ message.minimumSequenceNumber =
2895
+ this.useDeltaManagerOpsProxy &&
2896
+ this.deltaManager.minimumSequenceNumber < message.minimumSequenceNumber
2897
+ ? this.deltaManager.minimumSequenceNumber
2898
+ : message.minimumSequenceNumber;
2899
+ this._processedClientSequenceNumber = message.clientSequenceNumber;
2900
+ return message as InboundSequencedContainerRuntimeMessage;
2901
+ };
2902
+
2903
+ // Non-grouped batch messages are processed one at a time.
2904
+ if (!groupedBatch) {
2905
+ for (const { message, localOpMetadata } of messagesWithMetadata) {
2906
+ updateSequenceNumbers(message);
2907
+ this.ensureNoDataModelChanges(() => {
2908
+ this.validateAndProcessRuntimeMessages(
2909
+ message as InboundSequencedContainerRuntimeMessage,
2910
+ [
2911
+ {
2912
+ contents: message.contents,
2913
+ localOpMetadata,
2914
+ clientSequenceNumber: message.clientSequenceNumber,
2915
+ },
2916
+ ],
2876
2917
  local,
2877
2918
  savedOp,
2878
- localOpMetadata,
2879
- });
2880
- } else {
2881
- this.observeNonRuntimeMessage(message);
2882
- }
2919
+ );
2920
+ this.emit("op", message, true /* runtimeMessage */);
2921
+ });
2922
+ }
2923
+ return;
2924
+ }
2925
+
2926
+ let bunchedMessagesContent: IRuntimeMessagesContent[] = [];
2927
+ let previousMessage: InboundSequencedContainerRuntimeMessage | undefined;
2928
+
2929
+ // Helper that processes the previous bunch of messages.
2930
+ const sendBunchedMessages = () => {
2931
+ assert(previousMessage !== undefined, 0xa67 /* previous message must exist */);
2932
+ this.ensureNoDataModelChanges(() => {
2933
+ this.validateAndProcessRuntimeMessages(
2934
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2935
+ previousMessage!,
2936
+ bunchedMessagesContent,
2937
+ local,
2938
+ savedOp,
2939
+ );
2883
2940
  });
2884
- });
2941
+ bunchedMessagesContent = [];
2942
+ };
2943
+
2944
+ /**
2945
+ * For grouped batch messages, bunch contiguous messages of the same type and process them together.
2946
+ * This is an optimization mainly for DDSes, where it can process a bunch of ops together. DDSes
2947
+ * like merge tree or shared tree can process ops more efficiently when they are bunched together.
2948
+ */
2949
+ for (const { message, localOpMetadata } of messagesWithMetadata) {
2950
+ const currentMessage = updateSequenceNumbers(message);
2951
+ if (previousMessage && previousMessage.type !== currentMessage.type) {
2952
+ sendBunchedMessages();
2953
+ }
2954
+ previousMessage = currentMessage;
2955
+ bunchedMessagesContent.push({
2956
+ contents: message.contents,
2957
+ localOpMetadata,
2958
+ clientSequenceNumber: message.clientSequenceNumber,
2959
+ });
2960
+ }
2961
+
2962
+ // Process the last bunch of messages.
2963
+ sendBunchedMessages();
2964
+
2965
+ // Send the "op" events for the messages now that the ops have been processed.
2966
+ for (const { message } of messagesWithMetadata) {
2967
+ this.emit("op", message, true /* runtimeMessage */);
2968
+ }
2885
2969
  } catch (e) {
2886
2970
  error = e;
2887
2971
  throw error;
2888
2972
  } finally {
2889
2973
  if (locationInBatch.batchEnd) {
2890
- const lastMessage = messages[messages.length - 1]?.message;
2974
+ const lastMessage = messagesWithMetadata[messagesWithMetadata.length - 1]?.message;
2891
2975
  assert(lastMessage !== undefined, 0xa32 /* Batch must have at least one message */);
2892
2976
  this.scheduleManager.batchEnd(error, lastMessage);
2893
2977
  }
@@ -2916,80 +3000,62 @@ export class ContainerRuntime
2916
3000
  }
2917
3001
 
2918
3002
  /**
2919
- * Assuming the given message is also a TypedContainerRuntimeMessage,
2920
- * checks its type and dispatches the message to the appropriate handler in the runtime.
3003
+ * Process runtime messages. The messages here are contiguous messages in a batch.
3004
+ * Assuming the messages in the given bunch are also a TypedContainerRuntimeMessage, checks its type and dispatch
3005
+ * the messages to the appropriate handler in the runtime.
2921
3006
  * Throws a DataProcessingError if the message looks like but doesn't conform to a known TypedContainerRuntimeMessage type.
3007
+ * @param message - The core message with common properties for all the messages.
3008
+ * @param messageContents - The contents, local metadata and clientSequenceNumbers of the messages.
3009
+ * @param local - true if the messages were originally generated by the client receiving it.
3010
+ * @param savedOp - true if the message is a replayed saved op.
3011
+ *
2922
3012
  */
2923
-
2924
- private validateAndProcessRuntimeMessage(messageWithContext: {
2925
- message: InboundSequencedContainerRuntimeMessage;
2926
- local: boolean;
2927
- savedOp?: boolean;
2928
- localOpMetadata?: unknown;
2929
- }): void {
2930
- const { local, message, savedOp, localOpMetadata } = messageWithContext;
2931
-
2932
- // Set the minimum sequence number to the containerRuntime's understanding of minimum sequence number.
2933
- if (
2934
- this.useDeltaManagerOpsProxy &&
2935
- this.deltaManager.minimumSequenceNumber < message.minimumSequenceNumber
2936
- ) {
2937
- message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
2938
- }
2939
-
2940
- this._processedClientSequenceNumber = message.clientSequenceNumber;
2941
-
3013
+ private validateAndProcessRuntimeMessages(
3014
+ message: Omit<InboundSequencedContainerRuntimeMessage, "contents">,
3015
+ messagesContent: IRuntimeMessagesContent[],
3016
+ local: boolean,
3017
+ savedOp?: boolean,
3018
+ ): void {
2942
3019
  // If there are no more pending messages after processing a local message,
2943
3020
  // the document is no longer dirty.
2944
3021
  if (!this.hasPendingMessages()) {
2945
3022
  this.updateDocumentDirtyState(false);
2946
3023
  }
2947
3024
 
3025
+ // Get the contents without the localOpMetadata because not all message types know about localOpMetadata.
3026
+ const contents = messagesContent.map((c) => c.contents);
3027
+
2948
3028
  switch (message.type) {
3029
+ case ContainerMessageType.FluidDataStoreOp:
2949
3030
  case ContainerMessageType.Attach:
2950
3031
  case ContainerMessageType.Alias:
2951
- case ContainerMessageType.FluidDataStoreOp:
2952
- this.channelCollection.process(message, local, localOpMetadata);
3032
+ // Remove the metadata from the message before sending it to the channel collection. The metadata
3033
+ // is added by the container runtime and is not part of the message that the channel collection and
3034
+ // layers below it expect.
3035
+ this.channelCollection.processMessages({ envelope: message, messagesContent, local });
2953
3036
  break;
2954
3037
  case ContainerMessageType.BlobAttach:
2955
- this.blobManager.processBlobAttachOp(message, local);
3038
+ this.blobManager.processBlobAttachMessage(message, local);
2956
3039
  break;
2957
3040
  case ContainerMessageType.IdAllocation:
2958
- // Don't re-finalize the range if we're processing a "savedOp" in
2959
- // stashed ops flow. The compressor is stashed with these ops already processed.
2960
- // That said, in idCompressorMode === "delayed", we might not serialize ID compressor, and
2961
- // thus we need to process all the ops.
2962
- if (!(this.skipSavedCompressorOps && savedOp === true)) {
2963
- const range = message.contents;
2964
- // Some other client turned on the id compressor. If we have not turned it on,
2965
- // put it in a pending queue and delay finalization.
2966
- if (this._idCompressor === undefined) {
2967
- assert(
2968
- this.idCompressorMode !== undefined,
2969
- 0x93c /* id compressor should be enabled */,
2970
- );
2971
- this.pendingIdCompressorOps.push(range);
2972
- } else {
2973
- assert(
2974
- this.pendingIdCompressorOps.length === 0,
2975
- 0x979 /* there should be no pending ops! */,
2976
- );
2977
- this._idCompressor.finalizeCreationRange(range);
2978
- }
2979
- }
3041
+ this.processIdCompressorMessages(contents as IdCreationRange[], savedOp);
2980
3042
  break;
2981
3043
  case ContainerMessageType.GC:
2982
- this.garbageCollector.processMessage(message, message.timestamp, local);
3044
+ this.garbageCollector.processMessages(
3045
+ contents as GarbageCollectionMessage[],
3046
+ message.timestamp,
3047
+ local,
3048
+ );
2983
3049
  break;
2984
3050
  case ContainerMessageType.ChunkedOp:
2985
- // From observability POV, we should not exppse the rest of the system (including "op" events on object) to these messages.
3051
+ // From observability POV, we should not expose the rest of the system (including "op" events on object) to these messages.
2986
3052
  // Also resetReconnectCount() would be wrong - see comment that was there before this change was made.
2987
3053
  assert(false, 0x93d /* should not even get here */);
2988
3054
  case ContainerMessageType.Rejoin:
2989
3055
  break;
2990
3056
  case ContainerMessageType.DocumentSchemaChange:
2991
- this.documentsSchemaController.processDocumentSchemaOp(
2992
- message.contents,
3057
+ this.documentsSchemaController.processDocumentSchemaMessages(
3058
+ contents as IDocumentSchemaChangeMessage[],
2993
3059
  local,
2994
3060
  message.sequenceNumber,
2995
3061
  );
@@ -2998,14 +3064,38 @@ export class ContainerRuntime
2998
3064
  const error = getUnknownMessageTypeError(
2999
3065
  message.type,
3000
3066
  "validateAndProcessRuntimeMessage" /* codePath */,
3001
- message,
3067
+ message as ISequencedDocumentMessage,
3002
3068
  );
3003
3069
  this.closeFn(error);
3004
3070
  throw error;
3005
3071
  }
3006
3072
  }
3073
+ }
3007
3074
 
3008
- this.emit("op", message, true /* runtimeMessage */);
3075
+ private processIdCompressorMessages(messageContents: IdCreationRange[], savedOp?: boolean) {
3076
+ for (const range of messageContents) {
3077
+ // Don't re-finalize the range if we're processing a "savedOp" in
3078
+ // stashed ops flow. The compressor is stashed with these ops already processed.
3079
+ // That said, in idCompressorMode === "delayed", we might not serialize ID compressor, and
3080
+ // thus we need to process all the ops.
3081
+ if (!(this.skipSavedCompressorOps && savedOp === true)) {
3082
+ // Some other client turned on the id compressor. If we have not turned it on,
3083
+ // put it in a pending queue and delay finalization.
3084
+ if (this._idCompressor === undefined) {
3085
+ assert(
3086
+ this.idCompressorMode !== undefined,
3087
+ 0x93c /* id compressor should be enabled */,
3088
+ );
3089
+ this.pendingIdCompressorOps.push(range);
3090
+ } else {
3091
+ assert(
3092
+ this.pendingIdCompressorOps.length === 0,
3093
+ 0x979 /* there should be no pending ops! */,
3094
+ );
3095
+ this._idCompressor.finalizeCreationRange(range);
3096
+ }
3097
+ }
3098
+ }
3009
3099
  }
3010
3100
 
3011
3101
  /**
@@ -3015,11 +3105,13 @@ export class ContainerRuntime
3015
3105
  const duration = Date.now() - this._signalTracking.signalTimestamp;
3016
3106
  this.mc.logger.sendPerformanceEvent({
3017
3107
  eventName: "SignalLatency",
3018
- duration, // Roundtrip duration of the tracked signal in milliseconds.
3019
- signalsSent: this._signalTracking.totalSignalsSentInLatencyWindow, // Signals sent since the last logged SignalLatency event.
3020
- signalsLost: this._signalTracking.signalsLost, // Signals lost since the last logged SignalLatency event.
3021
- outOfOrderSignals: this._signalTracking.signalsOutOfOrder, // Out of order signals since the last logged SignalLatency event.
3022
- reconnectCount: this.consecutiveReconnects, // Container reconnect count.
3108
+ details: {
3109
+ duration, // Roundtrip duration of the tracked signal in milliseconds.
3110
+ sent: this._signalTracking.totalSignalsSentInLatencyWindow, // Signals sent since the last logged SignalLatency event.
3111
+ lost: this._signalTracking.signalsLost, // Signals lost since the last logged SignalLatency event.
3112
+ outOfOrder: this._signalTracking.signalsOutOfOrder, // Out of order signals since the last logged SignalLatency event.
3113
+ reconnectCount: this.consecutiveReconnects, // Container reconnect count.
3114
+ },
3023
3115
  });
3024
3116
  this._signalTracking.signalsLost = 0;
3025
3117
  this._signalTracking.signalsOutOfOrder = 0;
@@ -3054,9 +3146,11 @@ export class ContainerRuntime
3054
3146
  this._signalTracking.signalsLost += signalsLost;
3055
3147
  this.mc.logger.sendErrorEvent({
3056
3148
  eventName: "SignalLost",
3057
- signalsLost, // Number of lost signals detected.
3058
- trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3059
- clientBroadcastSignalSequenceNumber, // Actual signal sequence number received.
3149
+ details: {
3150
+ signalsLost, // Number of lost signals detected.
3151
+ expectedSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3152
+ clientBroadcastSignalSequenceNumber, // Actual signal sequence number received.
3153
+ },
3060
3154
  });
3061
3155
  }
3062
3156
  // Update the tracking signal sequence number to the next expected signal in the sequence.
@@ -3068,11 +3162,18 @@ export class ContainerRuntime
3068
3162
  this._signalTracking.minimumTrackingSignalSequenceNumber
3069
3163
  ) {
3070
3164
  this._signalTracking.signalsOutOfOrder++;
3165
+ const details: TelemetryEventPropertyTypeExt = {
3166
+ expectedSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3167
+ clientBroadcastSignalSequenceNumber, // Sequence number of the out of order signal.
3168
+ };
3169
+ // Only log `contents.type` when address is for container to avoid
3170
+ // chance that contents type is customer data.
3171
+ if (envelope.address === undefined) {
3172
+ details.contentsType = envelope.contents.type; // Type of signal that was received out of order.
3173
+ }
3071
3174
  this.mc.logger.sendTelemetryEvent({
3072
3175
  eventName: "SignalOutOfOrder",
3073
- type: envelope.contents.type, // Type of signal that was received out of order.
3074
- trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3075
- clientBroadcastSignalSequenceNumber, // Sequence number of the out of order signal.
3176
+ details,
3076
3177
  });
3077
3178
  }
3078
3179
  if (
@@ -3085,7 +3186,8 @@ export class ContainerRuntime
3085
3186
  ) {
3086
3187
  // Latency tracked signal has been received.
3087
3188
  // We now log the roundtrip duration of the tracked signal.
3088
- // This telemetry event also logs metrics for signals sent, signals lost, and out of order signals received.
3189
+ // This telemetry event also logs metrics for broadcast signals
3190
+ // sent, lost, and out of order.
3089
3191
  // These metrics are reset after logging the telemetry event.
3090
3192
  this.sendSignalTelemetryEvent();
3091
3193
  }
@@ -3774,7 +3876,22 @@ export class ContainerRuntime
3774
3876
  },
3775
3877
  });
3776
3878
 
3777
- assert(this.outbox.isEmpty, 0x3d1 /* Can't trigger summary in the middle of a batch */);
3879
+ // legacy: assert 0x3d1
3880
+ if (!this.outbox.isEmpty) {
3881
+ throw DataProcessingError.create(
3882
+ "Can't trigger summary in the middle of a batch",
3883
+ "submitSummary",
3884
+ undefined,
3885
+ {
3886
+ summaryNumber,
3887
+ pendingMessages: this.pendingMessagesCount,
3888
+ outboxLength: this.outbox.messageCount,
3889
+ mainBatchLength: this.outbox.mainBatchMessageCount,
3890
+ blobAttachBatchLength: this.outbox.blobAttachBatchMessageCount,
3891
+ idAllocationBatchLength: this.outbox.idAllocationBatchMessageCount,
3892
+ },
3893
+ );
3894
+ }
3778
3895
 
3779
3896
  // If the container is dirty, i.e., there are pending unacked ops, the summary will not be eventual consistent
3780
3897
  // and it may even be incorrect. So, wait for the container to be saved with a timeout. If the container is not
@@ -53,6 +53,8 @@ import {
53
53
  SummarizeInternalFn,
54
54
  channelsTreeName,
55
55
  IInboundSignalMessage,
56
+ type IPendingMessagesState,
57
+ type IRuntimeMessageCollection,
56
58
  } from "@fluidframework/runtime-definitions/internal";
57
59
  import {
58
60
  addBlobToSummary,
@@ -313,7 +315,7 @@ export abstract class FluidDataStoreContext
313
315
  * Returns the count of pending messages that are stored until the data store is realized.
314
316
  */
315
317
  public get pendingCount(): number {
316
- return this.pending?.length ?? 0;
318
+ return this.pendingMessagesState?.pendingCount ?? 0;
317
319
  }
318
320
 
319
321
  protected registry: IFluidDataStoreRegistry | undefined;
@@ -321,7 +323,11 @@ export abstract class FluidDataStoreContext
321
323
  protected detachedRuntimeCreation = false;
322
324
  protected channel: IFluidDataStoreChannel | undefined;
323
325
  private loaded = false;
324
- protected pending: ISequencedDocumentMessage[] | undefined = [];
326
+ /** Tracks the messages for this data store that are sent while it's not loaded */
327
+ private pendingMessagesState: IPendingMessagesState | undefined = {
328
+ messageCollections: [],
329
+ pendingCount: 0,
330
+ };
325
331
  protected channelP: Promise<IFluidDataStoreChannel> | undefined;
326
332
  protected _baseSnapshot: ISnapshotTree | undefined;
327
333
  protected _attachState: AttachState;
@@ -561,25 +567,61 @@ export abstract class FluidDataStoreContext
561
567
  this.channel!.setConnectionState(connected, clientId);
562
568
  }
563
569
 
564
- public process(
565
- message: ISequencedDocumentMessage,
566
- local: boolean,
567
- localOpMetadata: unknown,
568
- ): void {
569
- const safeTelemetryProps = extractSafePropertiesFromMessage(message);
570
- // On op process, tombstone error is logged in garbage collector. So, set "checkTombstone" to false when calling
570
+ /**
571
+ * back-compat ADO 21575: This is temporary and will be removed once the compat requirement across Runtime and
572
+ * Datastore boundary is satisfied.
573
+ * Process the messages to maintain backwards compatibility. The `processMessages` function is added to
574
+ * IFluidDataStoreChannel in 2.5.0. For channels before that, call `process` for each message.
575
+ */
576
+ private processMessagesCompat(
577
+ channel: IFluidDataStoreChannel,
578
+ messageCollection: IRuntimeMessageCollection,
579
+ ) {
580
+ if (channel.processMessages !== undefined) {
581
+ channel.processMessages(messageCollection);
582
+ } else {
583
+ const { envelope, messagesContent, local } = messageCollection;
584
+ for (const { contents, localOpMetadata, clientSequenceNumber } of messagesContent) {
585
+ channel.process(
586
+ { ...envelope, contents, clientSequenceNumber },
587
+ local,
588
+ localOpMetadata,
589
+ );
590
+ }
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Process messages for this data store. The messages here are contiguous messages for this data store in a batch.
596
+ * @param messageCollection - The collection of messages to process.
597
+ */
598
+ public processMessages(messageCollection: IRuntimeMessageCollection): void {
599
+ const { envelope, messagesContent, local } = messageCollection;
600
+ const safeTelemetryProps = extractSafePropertiesFromMessage(envelope);
601
+ // Tombstone error is logged in garbage collector. So, set "checkTombstone" to false when calling
571
602
  // "verifyNotClosed" which logs tombstone errors.
572
603
  this.verifyNotClosed("process", false /* checkTombstone */, safeTelemetryProps);
573
604
 
574
- this.summarizerNode.recordChange(message);
605
+ this.summarizerNode.recordChange(envelope as ISequencedDocumentMessage);
575
606
 
576
607
  if (this.loaded) {
577
- return this.channel?.process(message, local, localOpMetadata);
608
+ assert(this.channel !== undefined, 0xa68 /* Channel is not loaded */);
609
+ this.processMessagesCompat(this.channel, messageCollection);
578
610
  } else {
579
611
  assert(!local, 0x142 /* "local store channel is not loaded" */);
580
- assert(this.pending !== undefined, 0x23d /* "pending is undefined" */);
581
- this.pending.push(message);
582
- this.thresholdOpsCounter.sendIfMultiple("StorePendingOps", this.pending.length);
612
+ assert(
613
+ this.pendingMessagesState !== undefined,
614
+ 0xa69 /* pending messages queue is undefined */,
615
+ );
616
+ this.pendingMessagesState.messageCollections.push({
617
+ ...messageCollection,
618
+ messagesContent: Array.from(messagesContent),
619
+ });
620
+ this.pendingMessagesState.pendingCount += messagesContent.length;
621
+ this.thresholdOpsCounter.sendIfMultiple(
622
+ "StorePendingOps",
623
+ this.pendingMessagesState.pendingCount,
624
+ );
583
625
  }
584
626
  }
585
627
 
@@ -788,20 +830,21 @@ export abstract class FluidDataStoreContext
788
830
  }
789
831
 
790
832
  protected processPendingOps(channel: IFluidDataStoreChannel) {
791
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
792
- const pending = this.pending!;
833
+ const baseSequenceNumber = this.baseSnapshotSequenceNumber ?? -1;
793
834
 
794
- // Apply all pending ops
795
- for (const op of pending) {
835
+ assert(
836
+ this.pendingMessagesState !== undefined,
837
+ 0xa6a /* pending messages queue is undefined */,
838
+ );
839
+ for (const messageCollection of this.pendingMessagesState.messageCollections) {
796
840
  // Only process ops whose seq number is greater than snapshot sequence number from which it loaded.
797
- const seqNumber = this.baseSnapshotSequenceNumber ?? -1;
798
- if (op.sequenceNumber > seqNumber) {
799
- channel.process(op, false, undefined /* localOpMetadata */);
841
+ if (messageCollection.envelope.sequenceNumber > baseSequenceNumber) {
842
+ this.processMessagesCompat(channel, messageCollection);
800
843
  }
801
844
  }
802
- this.pending = undefined;
803
845
 
804
- this.thresholdOpsCounter.send("ProcessPendingOps", pending.length);
846
+ this.thresholdOpsCounter.send("ProcessPendingOps", this.pendingMessagesState.pendingCount);
847
+ this.pendingMessagesState = undefined;
805
848
  }
806
849
 
807
850
  protected completeBindingRuntime(channel: IFluidDataStoreChannel) {
@@ -861,44 +861,44 @@ export class GarbageCollector implements IGarbageCollector {
861
861
  }
862
862
 
863
863
  /**
864
- * Process a GC message.
865
- * @param message - The GC message from the container runtime.
866
- * @param messageTimestampMs - The timestamp of the message.
864
+ * Process GC messages.
865
+ * @param messageContents - The contents of the messages.
866
+ * @param messageTimestampMs - The timestamp of the messages.
867
867
  * @param local - Whether it was send by this client.
868
868
  */
869
- public processMessage(
870
- message: ContainerRuntimeGCMessage,
869
+ public processMessages(
870
+ messageContents: GarbageCollectionMessage[],
871
871
  messageTimestampMs: number,
872
872
  local: boolean,
873
873
  ) {
874
- const gcMessageType = message.contents.type;
875
- switch (gcMessageType) {
876
- case GarbageCollectionMessageType.Sweep: {
877
- // Delete the nodes whose ids are present in the contents.
878
- this.deleteSweepReadyNodes(message.contents.deletedNodeIds);
879
- break;
880
- }
881
- case GarbageCollectionMessageType.TombstoneLoaded: {
882
- // Mark the node as referenced to ensure it isn't Swept
883
- const tombstonedNodePath = message.contents.nodePath;
884
- this.addedOutboundReference(
885
- "/",
886
- tombstonedNodePath,
887
- messageTimestampMs,
888
- true /* autorecovery */,
889
- );
890
-
891
- // In case the cause of the TombstoneLoaded event is incorrect GC Data (i.e. the object is actually reachable),
892
- // do fullGC on the next run to get a chance to repair (in the likely case the bug is not deterministic)
893
- this.summaryStateTracker.autoRecovery.requestFullGCOnNextRun();
874
+ for (const gcMessage of messageContents) {
875
+ const gcMessageType = gcMessage.type;
876
+ switch (gcMessageType) {
877
+ case GarbageCollectionMessageType.Sweep: {
878
+ // Delete the nodes whose ids are present in the contents.
879
+ this.deleteSweepReadyNodes(gcMessage.deletedNodeIds);
880
+ break;
881
+ }
882
+ case GarbageCollectionMessageType.TombstoneLoaded: {
883
+ // Mark the node as referenced to ensure it isn't Swept
884
+ const tombstonedNodePath = gcMessage.nodePath;
885
+ this.addedOutboundReference(
886
+ "/",
887
+ tombstonedNodePath,
888
+ messageTimestampMs,
889
+ true /* autorecovery */,
890
+ );
894
891
 
895
- break;
896
- }
897
- default: {
898
- throw DataProcessingError.create(
899
- `Garbage collection message of unknown type ${gcMessageType}`,
900
- "processMessage",
901
- );
892
+ // In case the cause of the TombstoneLoaded event is incorrect GC Data (i.e. the object is actually reachable),
893
+ // do fullGC on the next run to get a chance to repair (in the likely case the bug is not deterministic)
894
+ this.summaryStateTracker.autoRecovery.requestFullGCOnNextRun();
895
+ break;
896
+ }
897
+ default:
898
+ throw DataProcessingError.create(
899
+ `Garbage collection message of unknown type ${gcMessageType}`,
900
+ "processMessage",
901
+ );
902
902
  }
903
903
  }
904
904
  }
@@ -352,9 +352,9 @@ export interface IGarbageCollector {
352
352
  timestampMs: number,
353
353
  autorecovery?: true,
354
354
  ): void;
355
- /** Called to process a garbage collection message. */
356
- processMessage(
357
- message: ContainerRuntimeGCMessage,
355
+ /** Called to process garbage collection messages */
356
+ processMessages(
357
+ messageContents: GarbageCollectionMessage[],
358
358
  messageTimestampMs: number,
359
359
  local: boolean,
360
360
  ): void;