@fluidframework/container-runtime 2.5.0-302463 → 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 (99) 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 +185 -129
  11. package/dist/channelCollection.js.map +1 -1
  12. package/dist/containerRuntime.d.ts +14 -4
  13. package/dist/containerRuntime.d.ts.map +1 -1
  14. package/dist/containerRuntime.js +138 -55
  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/dataStoreContexts.d.ts.map +1 -1
  21. package/dist/dataStoreContexts.js +6 -14
  22. package/dist/dataStoreContexts.js.map +1 -1
  23. package/dist/gc/garbageCollection.d.ts +5 -6
  24. package/dist/gc/garbageCollection.d.ts.map +1 -1
  25. package/dist/gc/garbageCollection.js +23 -22
  26. package/dist/gc/garbageCollection.js.map +1 -1
  27. package/dist/gc/gcDefinitions.d.ts +2 -2
  28. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  29. package/dist/gc/gcDefinitions.js.map +1 -1
  30. package/dist/opLifecycle/outbox.d.ts +3 -0
  31. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  32. package/dist/opLifecycle/outbox.js +9 -0
  33. package/dist/opLifecycle/outbox.js.map +1 -1
  34. package/dist/opLifecycle/remoteMessageProcessor.d.ts +1 -0
  35. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  36. package/dist/opLifecycle/remoteMessageProcessor.js +2 -0
  37. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  38. package/dist/packageVersion.d.ts +1 -1
  39. package/dist/packageVersion.d.ts.map +1 -1
  40. package/dist/packageVersion.js +1 -1
  41. package/dist/packageVersion.js.map +1 -1
  42. package/dist/summary/documentSchema.d.ts +11 -0
  43. package/dist/summary/documentSchema.d.ts.map +1 -1
  44. package/dist/summary/documentSchema.js +43 -28
  45. package/dist/summary/documentSchema.js.map +1 -1
  46. package/lib/blobManager/blobManager.d.ts +3 -3
  47. package/lib/blobManager/blobManager.d.ts.map +1 -1
  48. package/lib/blobManager/blobManager.js +1 -1
  49. package/lib/blobManager/blobManager.js.map +1 -1
  50. package/lib/channelCollection.d.ts +20 -5
  51. package/lib/channelCollection.d.ts.map +1 -1
  52. package/lib/channelCollection.js +186 -130
  53. package/lib/channelCollection.js.map +1 -1
  54. package/lib/containerRuntime.d.ts +14 -4
  55. package/lib/containerRuntime.d.ts.map +1 -1
  56. package/lib/containerRuntime.js +137 -54
  57. package/lib/containerRuntime.js.map +1 -1
  58. package/lib/dataStoreContext.d.ts +15 -3
  59. package/lib/dataStoreContext.d.ts.map +1 -1
  60. package/lib/dataStoreContext.js +48 -19
  61. package/lib/dataStoreContext.js.map +1 -1
  62. package/lib/dataStoreContexts.d.ts.map +1 -1
  63. package/lib/dataStoreContexts.js +7 -15
  64. package/lib/dataStoreContexts.js.map +1 -1
  65. package/lib/gc/garbageCollection.d.ts +5 -6
  66. package/lib/gc/garbageCollection.d.ts.map +1 -1
  67. package/lib/gc/garbageCollection.js +23 -22
  68. package/lib/gc/garbageCollection.js.map +1 -1
  69. package/lib/gc/gcDefinitions.d.ts +2 -2
  70. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  71. package/lib/gc/gcDefinitions.js.map +1 -1
  72. package/lib/opLifecycle/outbox.d.ts +3 -0
  73. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  74. package/lib/opLifecycle/outbox.js +9 -0
  75. package/lib/opLifecycle/outbox.js.map +1 -1
  76. package/lib/opLifecycle/remoteMessageProcessor.d.ts +1 -0
  77. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  78. package/lib/opLifecycle/remoteMessageProcessor.js +2 -0
  79. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  80. package/lib/packageVersion.d.ts +1 -1
  81. package/lib/packageVersion.d.ts.map +1 -1
  82. package/lib/packageVersion.js +1 -1
  83. package/lib/packageVersion.js.map +1 -1
  84. package/lib/summary/documentSchema.d.ts +11 -0
  85. package/lib/summary/documentSchema.d.ts.map +1 -1
  86. package/lib/summary/documentSchema.js +43 -28
  87. package/lib/summary/documentSchema.js.map +1 -1
  88. package/package.json +23 -19
  89. package/src/blobManager/blobManager.ts +2 -2
  90. package/src/channelCollection.ts +234 -176
  91. package/src/containerRuntime.ts +179 -68
  92. package/src/dataStoreContext.ts +66 -23
  93. package/src/dataStoreContexts.ts +7 -20
  94. package/src/gc/garbageCollection.ts +32 -32
  95. package/src/gc/gcDefinitions.ts +3 -3
  96. package/src/opLifecycle/outbox.ts +12 -0
  97. package/src/opLifecycle/remoteMessageProcessor.ts +3 -0
  98. package/src/packageVersion.ts +1 -1
  99. package/src/summary/documentSchema.ts +56 -37
@@ -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,
@@ -160,6 +161,7 @@ import {
160
161
  IGCStats,
161
162
  IGarbageCollector,
162
163
  gcGenerationOptionName,
164
+ type GarbageCollectionMessage,
163
165
  } from "./gc/index.js";
164
166
  import {
165
167
  ContainerMessageType,
@@ -202,6 +204,7 @@ import {
202
204
  IConnectableRuntime,
203
205
  IContainerRuntimeMetadata,
204
206
  ICreateContainerMetadata,
207
+ type IDocumentSchemaChangeMessage,
205
208
  type IDocumentSchemaCurrent,
206
209
  IEnqueueSummarizeOptions,
207
210
  IGenerateSummaryTreeResult,
@@ -558,6 +561,13 @@ export interface RuntimeHeaderData {
558
561
  allowTombstone?: boolean;
559
562
  }
560
563
 
564
+ /** Default values for Runtime Headers */
565
+ export const defaultRuntimeHeaderData: Required<RuntimeHeaderData> = {
566
+ wait: true,
567
+ viaHandle: false,
568
+ allowTombstone: false,
569
+ };
570
+
561
571
  /**
562
572
  * Available compression algorithms for op compression.
563
573
  * @legacy
@@ -2804,6 +2814,9 @@ export class ContainerRuntime
2804
2814
  local,
2805
2815
  savedOp,
2806
2816
  runtimeBatch,
2817
+ inboundResult.type === "fullBatch"
2818
+ ? inboundResult.groupedBatch
2819
+ : false /* groupedBatch */,
2807
2820
  );
2808
2821
  } else {
2809
2822
  if (!runtimeBatch) {
@@ -2823,6 +2836,7 @@ export class ContainerRuntime
2823
2836
  local,
2824
2837
  savedOp,
2825
2838
  runtimeBatch,
2839
+ false /* groupedBatch */,
2826
2840
  );
2827
2841
  }
2828
2842
 
@@ -2838,14 +2852,15 @@ export class ContainerRuntime
2838
2852
 
2839
2853
  /**
2840
2854
  * Processes inbound message(s). It calls schedule manager according to the messages' location in the batch.
2841
- * @param messages - messages to process.
2855
+ * @param messagesWithMetadata - messages to process along with their metadata.
2842
2856
  * @param locationInBatch - Are we processing the start and/or end of a batch?
2843
2857
  * @param local - true if the messages were originally generated by the client receiving it.
2844
2858
  * @param savedOp - true if the message is a replayed saved op.
2845
2859
  * @param runtimeBatch - true if these are runtime messages.
2860
+ * @param groupedBatch - true if these messages are part of a grouped op batch.
2846
2861
  */
2847
2862
  private processInboundMessages(
2848
- messages: {
2863
+ messagesWithMetadata: {
2849
2864
  message: ISequencedDocumentMessage;
2850
2865
  localOpMetadata?: unknown;
2851
2866
  }[],
@@ -2853,35 +2868,110 @@ export class ContainerRuntime
2853
2868
  local: boolean,
2854
2869
  savedOp: boolean | undefined,
2855
2870
  runtimeBatch: boolean,
2871
+ groupedBatch: boolean,
2856
2872
  ) {
2857
2873
  if (locationInBatch.batchStart) {
2858
- const firstMessage = messages[0]?.message;
2874
+ const firstMessage = messagesWithMetadata[0]?.message;
2859
2875
  assert(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
2860
2876
  this.scheduleManager.batchBegin(firstMessage);
2861
2877
  }
2862
2878
 
2863
2879
  let error: unknown;
2864
2880
  try {
2865
- messages.forEach(({ message, localOpMetadata }) => {
2866
- this.ensureNoDataModelChanges(() => {
2867
- if (runtimeBatch) {
2868
- this.validateAndProcessRuntimeMessage({
2869
- 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
+ ],
2870
2917
  local,
2871
2918
  savedOp,
2872
- localOpMetadata,
2873
- });
2874
- } else {
2875
- this.observeNonRuntimeMessage(message);
2876
- }
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
+ );
2877
2940
  });
2878
- });
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
+ }
2879
2969
  } catch (e) {
2880
2970
  error = e;
2881
2971
  throw error;
2882
2972
  } finally {
2883
2973
  if (locationInBatch.batchEnd) {
2884
- const lastMessage = messages[messages.length - 1]?.message;
2974
+ const lastMessage = messagesWithMetadata[messagesWithMetadata.length - 1]?.message;
2885
2975
  assert(lastMessage !== undefined, 0xa32 /* Batch must have at least one message */);
2886
2976
  this.scheduleManager.batchEnd(error, lastMessage);
2887
2977
  }
@@ -2910,80 +3000,62 @@ export class ContainerRuntime
2910
3000
  }
2911
3001
 
2912
3002
  /**
2913
- * Assuming the given message is also a TypedContainerRuntimeMessage,
2914
- * 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.
2915
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
+ *
2916
3012
  */
2917
-
2918
- private validateAndProcessRuntimeMessage(messageWithContext: {
2919
- message: InboundSequencedContainerRuntimeMessage;
2920
- local: boolean;
2921
- savedOp?: boolean;
2922
- localOpMetadata?: unknown;
2923
- }): void {
2924
- const { local, message, savedOp, localOpMetadata } = messageWithContext;
2925
-
2926
- // Set the minimum sequence number to the containerRuntime's understanding of minimum sequence number.
2927
- if (
2928
- this.useDeltaManagerOpsProxy &&
2929
- this.deltaManager.minimumSequenceNumber < message.minimumSequenceNumber
2930
- ) {
2931
- message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
2932
- }
2933
-
2934
- this._processedClientSequenceNumber = message.clientSequenceNumber;
2935
-
3013
+ private validateAndProcessRuntimeMessages(
3014
+ message: Omit<InboundSequencedContainerRuntimeMessage, "contents">,
3015
+ messagesContent: IRuntimeMessagesContent[],
3016
+ local: boolean,
3017
+ savedOp?: boolean,
3018
+ ): void {
2936
3019
  // If there are no more pending messages after processing a local message,
2937
3020
  // the document is no longer dirty.
2938
3021
  if (!this.hasPendingMessages()) {
2939
3022
  this.updateDocumentDirtyState(false);
2940
3023
  }
2941
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
+
2942
3028
  switch (message.type) {
3029
+ case ContainerMessageType.FluidDataStoreOp:
2943
3030
  case ContainerMessageType.Attach:
2944
3031
  case ContainerMessageType.Alias:
2945
- case ContainerMessageType.FluidDataStoreOp:
2946
- 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 });
2947
3036
  break;
2948
3037
  case ContainerMessageType.BlobAttach:
2949
- this.blobManager.processBlobAttachOp(message, local);
3038
+ this.blobManager.processBlobAttachMessage(message, local);
2950
3039
  break;
2951
3040
  case ContainerMessageType.IdAllocation:
2952
- // Don't re-finalize the range if we're processing a "savedOp" in
2953
- // stashed ops flow. The compressor is stashed with these ops already processed.
2954
- // That said, in idCompressorMode === "delayed", we might not serialize ID compressor, and
2955
- // thus we need to process all the ops.
2956
- if (!(this.skipSavedCompressorOps && savedOp === true)) {
2957
- const range = message.contents;
2958
- // Some other client turned on the id compressor. If we have not turned it on,
2959
- // put it in a pending queue and delay finalization.
2960
- if (this._idCompressor === undefined) {
2961
- assert(
2962
- this.idCompressorMode !== undefined,
2963
- 0x93c /* id compressor should be enabled */,
2964
- );
2965
- this.pendingIdCompressorOps.push(range);
2966
- } else {
2967
- assert(
2968
- this.pendingIdCompressorOps.length === 0,
2969
- 0x979 /* there should be no pending ops! */,
2970
- );
2971
- this._idCompressor.finalizeCreationRange(range);
2972
- }
2973
- }
3041
+ this.processIdCompressorMessages(contents as IdCreationRange[], savedOp);
2974
3042
  break;
2975
3043
  case ContainerMessageType.GC:
2976
- this.garbageCollector.processMessage(message, message.timestamp, local);
3044
+ this.garbageCollector.processMessages(
3045
+ contents as GarbageCollectionMessage[],
3046
+ message.timestamp,
3047
+ local,
3048
+ );
2977
3049
  break;
2978
3050
  case ContainerMessageType.ChunkedOp:
2979
- // 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.
2980
3052
  // Also resetReconnectCount() would be wrong - see comment that was there before this change was made.
2981
3053
  assert(false, 0x93d /* should not even get here */);
2982
3054
  case ContainerMessageType.Rejoin:
2983
3055
  break;
2984
3056
  case ContainerMessageType.DocumentSchemaChange:
2985
- this.documentsSchemaController.processDocumentSchemaOp(
2986
- message.contents,
3057
+ this.documentsSchemaController.processDocumentSchemaMessages(
3058
+ contents as IDocumentSchemaChangeMessage[],
2987
3059
  local,
2988
3060
  message.sequenceNumber,
2989
3061
  );
@@ -2992,14 +3064,38 @@ export class ContainerRuntime
2992
3064
  const error = getUnknownMessageTypeError(
2993
3065
  message.type,
2994
3066
  "validateAndProcessRuntimeMessage" /* codePath */,
2995
- message,
3067
+ message as ISequencedDocumentMessage,
2996
3068
  );
2997
3069
  this.closeFn(error);
2998
3070
  throw error;
2999
3071
  }
3000
3072
  }
3073
+ }
3001
3074
 
3002
- 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
+ }
3003
3099
  }
3004
3100
 
3005
3101
  /**
@@ -3780,7 +3876,22 @@ export class ContainerRuntime
3780
3876
  },
3781
3877
  });
3782
3878
 
3783
- 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
+ }
3784
3895
 
3785
3896
  // If the container is dirty, i.e., there are pending unacked ops, the summary will not be eventual consistent
3786
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) {
@@ -7,7 +7,6 @@ import { IDisposable, ITelemetryBaseLogger } from "@fluidframework/core-interfac
7
7
  import { assert, Deferred, Lazy } from "@fluidframework/core-utils/internal";
8
8
  import {
9
9
  ITelemetryLoggerExt,
10
- PerformanceEvent,
11
10
  createChildLogger,
12
11
  } from "@fluidframework/telemetry-utils/internal";
13
12
 
@@ -41,7 +40,7 @@ export class DataStoreContexts
41
40
  .catch((contextError) => {
42
41
  this._logger.sendErrorEvent(
43
42
  {
44
- eventName: "DisposeError",
43
+ eventName: "FluidDataStoreContextDisposeError",
45
44
  fluidDataStoreId,
46
45
  },
47
46
  contextError,
@@ -53,10 +52,7 @@ export class DataStoreContexts
53
52
  private readonly _logger: ITelemetryLoggerExt;
54
53
 
55
54
  constructor(baseLogger: ITelemetryBaseLogger) {
56
- this._logger = createChildLogger({
57
- namespace: "FluidDataStoreContexts",
58
- logger: baseLogger,
59
- });
55
+ this._logger = createChildLogger({ logger: baseLogger });
60
56
  }
61
57
 
62
58
  [Symbol.iterator](): Iterator<[string, FluidDataStoreContext]> {
@@ -144,20 +140,11 @@ export class DataStoreContexts
144
140
  ): Promise<FluidDataStoreContext | undefined> {
145
141
  const deferredContext = this.ensureDeferred(id);
146
142
 
147
- const existing = deferredContext.isCompleted;
148
- return PerformanceEvent.timedExecAsync(
149
- this._logger,
150
- {
151
- eventName: "GetBoundOrRemoted",
152
- wait,
153
- existing,
154
- },
155
- async () => (!wait && !existing ? undefined : deferredContext.promise),
156
- {
157
- start: true,
158
- end: true,
159
- },
160
- );
143
+ if (!wait && !deferredContext.isCompleted) {
144
+ return undefined;
145
+ }
146
+
147
+ return deferredContext.promise;
161
148
  }
162
149
 
163
150
  private ensureDeferred(id: string): Deferred<FluidDataStoreContext> {
@@ -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;
@@ -145,6 +145,18 @@ export class Outbox {
145
145
  return this.mainBatch.length + this.blobAttachBatch.length + this.idAllocationBatch.length;
146
146
  }
147
147
 
148
+ public get mainBatchMessageCount(): number {
149
+ return this.mainBatch.length;
150
+ }
151
+
152
+ public get blobAttachBatchMessageCount(): number {
153
+ return this.blobAttachBatch.length;
154
+ }
155
+
156
+ public get idAllocationBatchMessageCount(): number {
157
+ return this.idAllocationBatch.length;
158
+ }
159
+
148
160
  public get isEmpty(): boolean {
149
161
  return this.messageCount === 0;
150
162
  }