@fluidframework/container-runtime 2.4.0-297385 → 2.4.0-299707

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 (50) hide show
  1. package/container-runtime.test-files.tar +0 -0
  2. package/dist/containerRuntime.d.ts +8 -0
  3. package/dist/containerRuntime.d.ts.map +1 -1
  4. package/dist/containerRuntime.js +94 -111
  5. package/dist/containerRuntime.js.map +1 -1
  6. package/dist/gc/garbageCollection.d.ts +1 -1
  7. package/dist/gc/garbageCollection.d.ts.map +1 -1
  8. package/dist/gc/garbageCollection.js +2 -6
  9. package/dist/gc/garbageCollection.js.map +1 -1
  10. package/dist/opLifecycle/batchManager.d.ts +2 -0
  11. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  12. package/dist/opLifecycle/batchManager.js.map +1 -1
  13. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  14. package/dist/opLifecycle/outbox.js +16 -10
  15. package/dist/opLifecycle/outbox.js.map +1 -1
  16. package/dist/packageVersion.d.ts +1 -1
  17. package/dist/packageVersion.js +1 -1
  18. package/dist/packageVersion.js.map +1 -1
  19. package/dist/pendingStateManager.d.ts +8 -2
  20. package/dist/pendingStateManager.d.ts.map +1 -1
  21. package/dist/pendingStateManager.js +14 -7
  22. package/dist/pendingStateManager.js.map +1 -1
  23. package/lib/containerRuntime.d.ts +8 -0
  24. package/lib/containerRuntime.d.ts.map +1 -1
  25. package/lib/containerRuntime.js +94 -111
  26. package/lib/containerRuntime.js.map +1 -1
  27. package/lib/gc/garbageCollection.d.ts +1 -1
  28. package/lib/gc/garbageCollection.d.ts.map +1 -1
  29. package/lib/gc/garbageCollection.js +3 -7
  30. package/lib/gc/garbageCollection.js.map +1 -1
  31. package/lib/opLifecycle/batchManager.d.ts +2 -0
  32. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  33. package/lib/opLifecycle/batchManager.js.map +1 -1
  34. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  35. package/lib/opLifecycle/outbox.js +16 -10
  36. package/lib/opLifecycle/outbox.js.map +1 -1
  37. package/lib/packageVersion.d.ts +1 -1
  38. package/lib/packageVersion.js +1 -1
  39. package/lib/packageVersion.js.map +1 -1
  40. package/lib/pendingStateManager.d.ts +8 -2
  41. package/lib/pendingStateManager.d.ts.map +1 -1
  42. package/lib/pendingStateManager.js +14 -7
  43. package/lib/pendingStateManager.js.map +1 -1
  44. package/package.json +22 -21
  45. package/src/containerRuntime.ts +133 -147
  46. package/src/gc/garbageCollection.ts +5 -12
  47. package/src/opLifecycle/batchManager.ts +3 -0
  48. package/src/opLifecycle/outbox.ts +21 -10
  49. package/src/packageVersion.ts +1 -1
  50. package/src/pendingStateManager.ts +26 -8
@@ -102,7 +102,10 @@ import {
102
102
  responseToException,
103
103
  seqFromTree,
104
104
  } from "@fluidframework/runtime-utils/internal";
105
- import type { ITelemetryGenericEventExt } from "@fluidframework/telemetry-utils/internal";
105
+ import type {
106
+ IFluidErrorBase,
107
+ ITelemetryGenericEventExt,
108
+ } from "@fluidframework/telemetry-utils/internal";
106
109
  import {
107
110
  ITelemetryLoggerExt,
108
111
  DataCorruptionError,
@@ -167,7 +170,7 @@ import {
167
170
  type OutboundContainerRuntimeMessage,
168
171
  type UnknownContainerRuntimeMessage,
169
172
  } from "./messageTypes.js";
170
- import { IBatchMetadata, ISavedOpMetadata } from "./metadata.js";
173
+ import { ISavedOpMetadata } from "./metadata.js";
171
174
  import {
172
175
  BatchId,
173
176
  BatchMessage,
@@ -236,19 +239,32 @@ import {
236
239
  import { Throttler, formExponentialFn } from "./throttler.js";
237
240
 
238
241
  /**
239
- * Utility to implement compat behaviors given an unknown message type
242
+ * Creates an error object to be thrown / passed to Container's close fn in case of an unknown message type.
240
243
  * The parameters are typed to support compile-time enforcement of handling all known types/behaviors
241
244
  *
242
- * @param _unknownContainerRuntimeMessageType - Typed as something unexpected, to ensure all known types have been
245
+ * @param unknownContainerRuntimeMessageType - Typed as something unexpected, to ensure all known types have been
243
246
  * handled before calling this function (e.g. in a switch statement).
244
- * @param compatBehavior - Typed redundantly with CompatModeBehavior to ensure handling is added when updating that type
247
+ *
248
+ * @param codePath - The code path where the unexpected message type was encountered.
249
+ *
250
+ * @param sequencedMessage - The sequenced message that contained the unexpected message type.
251
+ *
245
252
  */
246
- function compatBehaviorAllowsMessageType(
247
- _unknownContainerRuntimeMessageType: UnknownContainerRuntimeMessage["type"],
248
- compatBehavior: "Ignore" | "FailToProcess" | undefined,
249
- ): boolean {
250
- // undefined defaults to same behavior as "FailToProcess"
251
- return compatBehavior === "Ignore";
253
+ function getUnknownMessageTypeError(
254
+ unknownContainerRuntimeMessageType: UnknownContainerRuntimeMessage["type"],
255
+ codePath: string,
256
+ sequencedMessage?: ISequencedDocumentMessage,
257
+ ): IFluidErrorBase {
258
+ return DataProcessingError.create(
259
+ "Runtime message of unknown type",
260
+ codePath,
261
+ sequencedMessage,
262
+ {
263
+ messageDetails: {
264
+ type: unknownContainerRuntimeMessageType,
265
+ },
266
+ },
267
+ );
252
268
  }
253
269
 
254
270
  /**
@@ -1148,6 +1164,9 @@ export class ContainerRuntime
1148
1164
  summaryOp: ISummaryContent,
1149
1165
  referenceSequenceNumber?: number,
1150
1166
  ) => number;
1167
+ /**
1168
+ * Do not call directly - use submitAddressesSignal
1169
+ */
1151
1170
  private readonly submitSignalFn: (content: ISignalEnvelope, targetClientId?: string) => void;
1152
1171
  public readonly disposeFn: (error?: ICriticalContainerError) => void;
1153
1172
  public readonly closeFn: (error?: ICriticalContainerError) => void;
@@ -1755,7 +1774,7 @@ export class ContainerRuntime
1755
1774
  type,
1756
1775
  envelope1.contents,
1757
1776
  );
1758
- return this.submitSignalFn(envelope2, targetClientId);
1777
+ return this.submitEnvelopedSignal(envelope2, targetClientId);
1759
1778
  };
1760
1779
 
1761
1780
  let snapshot: ISnapshot | ISnapshotTree | undefined = getSummaryForDatastores(
@@ -2524,27 +2543,12 @@ export class ContainerRuntime
2524
2543
  // GC op is only sent in summarizer which should never have stashed ops.
2525
2544
  throw new LoggingError("GC op not expected to be stashed in summarizer");
2526
2545
  default: {
2527
- // This should be extremely rare for stashed ops.
2528
- // It would require a newer runtime stashing ops and then an older one applying them,
2529
- // e.g. if an app rolled back its container version
2530
- const compatBehavior = opContents.compatDetails?.behavior;
2531
- if (!compatBehaviorAllowsMessageType(opContents.type, compatBehavior)) {
2532
- const error = DataProcessingError.create(
2533
- "Stashed runtime message of unexpected type",
2534
- "applyStashedOp",
2535
- undefined /* sequencedMessage */,
2536
- {
2537
- messageDetails: JSON.stringify({
2538
- type: opContents.type,
2539
- compatBehavior,
2540
- }),
2541
- },
2542
- );
2543
- this.closeFn(error);
2544
- throw error;
2545
- }
2546
- // Note: Even if its compat behavior allows it, we don't know how to apply this stashed op.
2547
- // All we can do is ignore it (similar to on process).
2546
+ const error = getUnknownMessageTypeError(
2547
+ opContents.type,
2548
+ "applyStashedOp" /* codePath */,
2549
+ );
2550
+ this.closeFn(error);
2551
+ throw error;
2548
2552
  }
2549
2553
  }
2550
2554
  }
@@ -2975,27 +2979,13 @@ export class ContainerRuntime
2975
2979
  );
2976
2980
  break;
2977
2981
  default: {
2978
- const compatBehavior = message.compatDetails?.behavior;
2979
- if (!compatBehaviorAllowsMessageType(message.type, compatBehavior)) {
2980
- const error = DataProcessingError.create(
2981
- // Former assert 0x3ce
2982
- "Runtime message of unknown type",
2983
- "OpProcessing",
2984
- message,
2985
- {
2986
- local,
2987
- messageDetails: JSON.stringify({
2988
- type: message.type,
2989
- contentType: typeof message.contents,
2990
- compatBehavior,
2991
- batch: (message.metadata as IBatchMetadata | undefined)?.batch,
2992
- compression: message.compression,
2993
- }),
2994
- },
2995
- );
2996
- this.closeFn(error);
2997
- throw error;
2998
- }
2982
+ const error = getUnknownMessageTypeError(
2983
+ message.type,
2984
+ "validateAndProcessRuntimeMessage" /* codePath */,
2985
+ message,
2986
+ );
2987
+ this.closeFn(error);
2988
+ throw error;
2999
2989
  }
3000
2990
  }
3001
2991
 
@@ -3021,6 +3011,72 @@ export class ContainerRuntime
3021
3011
  this._signalTracking.totalSignalsSentInLatencyWindow = 0;
3022
3012
  }
3023
3013
 
3014
+ /**
3015
+ * Updates signal telemetry including emitting telemetry events.
3016
+ */
3017
+ private processSignalForTelemetry(envelope: ISignalEnvelope): void {
3018
+ const { clientBroadcastSignalSequenceNumber } = envelope;
3019
+ if (clientBroadcastSignalSequenceNumber === undefined) {
3020
+ return;
3021
+ }
3022
+
3023
+ if (
3024
+ this._signalTracking.trackingSignalSequenceNumber === undefined ||
3025
+ this._signalTracking.minimumTrackingSignalSequenceNumber === undefined
3026
+ ) {
3027
+ return;
3028
+ }
3029
+
3030
+ if (
3031
+ clientBroadcastSignalSequenceNumber >= this._signalTracking.trackingSignalSequenceNumber
3032
+ ) {
3033
+ // Calculate the number of signals lost and log the event.
3034
+ const signalsLost =
3035
+ clientBroadcastSignalSequenceNumber -
3036
+ this._signalTracking.trackingSignalSequenceNumber;
3037
+ if (signalsLost > 0) {
3038
+ this._signalTracking.signalsLost += signalsLost;
3039
+ this.mc.logger.sendErrorEvent({
3040
+ eventName: "SignalLost",
3041
+ signalsLost, // Number of lost signals detected.
3042
+ trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3043
+ clientBroadcastSignalSequenceNumber, // Actual signal sequence number received.
3044
+ });
3045
+ }
3046
+ // Update the tracking signal sequence number to the next expected signal in the sequence.
3047
+ this._signalTracking.trackingSignalSequenceNumber =
3048
+ clientBroadcastSignalSequenceNumber + 1;
3049
+ } else if (
3050
+ // Check if this is a signal in range of interest.
3051
+ clientBroadcastSignalSequenceNumber >=
3052
+ this._signalTracking.minimumTrackingSignalSequenceNumber
3053
+ ) {
3054
+ this._signalTracking.signalsOutOfOrder++;
3055
+ this.mc.logger.sendTelemetryEvent({
3056
+ eventName: "SignalOutOfOrder",
3057
+ type: envelope.contents.type, // Type of signal that was received out of order.
3058
+ trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3059
+ clientBroadcastSignalSequenceNumber, // Sequence number of the out of order signal.
3060
+ });
3061
+ }
3062
+ if (
3063
+ this._signalTracking.roundTripSignalSequenceNumber !== undefined &&
3064
+ clientBroadcastSignalSequenceNumber >= this._signalTracking.roundTripSignalSequenceNumber
3065
+ ) {
3066
+ if (
3067
+ clientBroadcastSignalSequenceNumber ===
3068
+ this._signalTracking.roundTripSignalSequenceNumber
3069
+ ) {
3070
+ // Latency tracked signal has been received.
3071
+ // We now log the roundtrip duration of the tracked signal.
3072
+ // This telemetry event also logs metrics for signals sent, signals lost, and out of order signals received.
3073
+ // These metrics are reset after logging the telemetry event.
3074
+ this.sendSignalTelemetryEvent();
3075
+ }
3076
+ this._signalTracking.roundTripSignalSequenceNumber = undefined;
3077
+ }
3078
+ }
3079
+
3024
3080
  public processSignal(message: ISignalMessage, local: boolean) {
3025
3081
  const envelope = message.content as ISignalEnvelope;
3026
3082
  const transformed: IInboundSignalMessage = {
@@ -3033,64 +3089,15 @@ export class ContainerRuntime
3033
3089
  // Only collect signal telemetry for broadcast messages sent by the current client.
3034
3090
  if (
3035
3091
  message.clientId === this.clientId &&
3036
- this.connected &&
3037
- envelope.clientBroadcastSignalSequenceNumber !== undefined
3092
+ // jason-ha: This `connected` check seems incorrect. Signals that come through
3093
+ // here must have been received while connected to service and there is no need
3094
+ // to avoid processing when connection has dropped. Because container runtime's
3095
+ // `connected` (and `_connected`) state also reflects some ops state, it may
3096
+ // easily be false when newly connected and signal tracking may very well
3097
+ // complain lost signals (that were simply skipped per this check).
3098
+ this.connected
3038
3099
  ) {
3039
- if (
3040
- this._signalTracking.trackingSignalSequenceNumber !== undefined &&
3041
- this._signalTracking.minimumTrackingSignalSequenceNumber !== undefined
3042
- ) {
3043
- if (
3044
- envelope.clientBroadcastSignalSequenceNumber >=
3045
- this._signalTracking.trackingSignalSequenceNumber
3046
- ) {
3047
- // Calculate the number of signals lost and log the event.
3048
- const signalsLost =
3049
- envelope.clientBroadcastSignalSequenceNumber -
3050
- this._signalTracking.trackingSignalSequenceNumber;
3051
- if (signalsLost > 0) {
3052
- this._signalTracking.signalsLost += signalsLost;
3053
- this.mc.logger.sendErrorEvent({
3054
- eventName: "SignalLost",
3055
- signalsLost, // Number of lost signals detected.
3056
- trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3057
- clientBroadcastSignalSequenceNumber:
3058
- envelope.clientBroadcastSignalSequenceNumber, // Actual signal sequence number received.
3059
- });
3060
- }
3061
- // Update the tracking signal sequence number to the next expected signal in the sequence.
3062
- this._signalTracking.trackingSignalSequenceNumber =
3063
- envelope.clientBroadcastSignalSequenceNumber + 1;
3064
- } else if (
3065
- envelope.clientBroadcastSignalSequenceNumber >=
3066
- this._signalTracking.minimumTrackingSignalSequenceNumber
3067
- ) {
3068
- this._signalTracking.signalsOutOfOrder++;
3069
- this.mc.logger.sendTelemetryEvent({
3070
- eventName: "SignalOutOfOrder",
3071
- type: envelope.contents.type, // Type of signal that was received out of order.
3072
- trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3073
- clientBroadcastSignalSequenceNumber: envelope.clientBroadcastSignalSequenceNumber, // Sequence number of the out of order signal.
3074
- });
3075
- }
3076
- if (
3077
- this._signalTracking.roundTripSignalSequenceNumber !== undefined &&
3078
- envelope.clientBroadcastSignalSequenceNumber >=
3079
- this._signalTracking.roundTripSignalSequenceNumber
3080
- ) {
3081
- if (
3082
- envelope.clientBroadcastSignalSequenceNumber ===
3083
- this._signalTracking.roundTripSignalSequenceNumber
3084
- ) {
3085
- // Latency tracked signal has been received.
3086
- // We now log the roundtrip duration of the tracked signal.
3087
- // This telemetry event also logs metrics for signals sent, signals lost, and out of order signals received.
3088
- // These metrics are reset after logging the telemetry event.
3089
- this.sendSignalTelemetryEvent();
3090
- }
3091
- this._signalTracking.roundTripSignalSequenceNumber = undefined;
3092
- }
3093
- }
3100
+ this.processSignalForTelemetry(envelope);
3094
3101
  }
3095
3102
 
3096
3103
  if (envelope.address === undefined) {
@@ -3337,19 +3344,24 @@ export class ContainerRuntime
3337
3344
  address: string | undefined,
3338
3345
  type: string,
3339
3346
  content: any,
3340
- targetClientId?: string,
3341
- ): ISignalEnvelope {
3342
- const newEnvelope: ISignalEnvelope = {
3347
+ ): Omit<ISignalEnvelope, "broadcastSignalSequenceNumber"> {
3348
+ const newEnvelope: Omit<ISignalEnvelope, "broadcastSignalSequenceNumber"> = {
3343
3349
  address,
3344
3350
  contents: { type, content },
3345
3351
  };
3346
3352
 
3353
+ return newEnvelope;
3354
+ }
3355
+
3356
+ private submitEnvelopedSignal(envelope: ISignalEnvelope, targetClientId?: string) {
3347
3357
  const isBroadcastSignal = targetClientId === undefined;
3348
3358
 
3349
3359
  if (isBroadcastSignal) {
3350
3360
  const clientBroadcastSignalSequenceNumber = ++this._signalTracking
3351
3361
  .broadcastSignalSequenceNumber;
3352
- newEnvelope.clientBroadcastSignalSequenceNumber = clientBroadcastSignalSequenceNumber;
3362
+ // Stamp with the broadcast signal sequence number.
3363
+ envelope.clientBroadcastSignalSequenceNumber = clientBroadcastSignalSequenceNumber;
3364
+
3353
3365
  this._signalTracking.signalsSentSinceLastLatencyMeasurement++;
3354
3366
 
3355
3367
  if (
@@ -3378,7 +3390,7 @@ export class ContainerRuntime
3378
3390
  }
3379
3391
  }
3380
3392
 
3381
- return newEnvelope;
3393
+ this.submitSignalFn(envelope, targetClientId);
3382
3394
  }
3383
3395
 
3384
3396
  /**
@@ -3395,13 +3407,8 @@ export class ContainerRuntime
3395
3407
  */
3396
3408
  public submitSignal(type: string, content: unknown, targetClientId?: string) {
3397
3409
  this.verifyNotClosed();
3398
- const envelope = this.createNewSignalEnvelope(
3399
- undefined /* address */,
3400
- type,
3401
- content,
3402
- targetClientId,
3403
- );
3404
- return this.submitSignalFn(envelope, targetClientId);
3410
+ const envelope = this.createNewSignalEnvelope(undefined /* address */, type, content);
3411
+ return this.submitEnvelopedSignal(envelope, targetClientId);
3405
3412
  }
3406
3413
 
3407
3414
  public setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void {
@@ -4442,30 +4449,9 @@ export class ContainerRuntime
4442
4449
  // send any ops, as some other client already changed schema.
4443
4450
  break;
4444
4451
  default: {
4445
- // This case should be very rare - it would imply an op was stashed from a
4446
- // future version of runtime code and now is being applied on an older version.
4447
- const compatBehavior = message.compatDetails?.behavior;
4448
- if (compatBehaviorAllowsMessageType(message.type, compatBehavior)) {
4449
- // We do not ultimately resubmit it, to be consistent with this version of the code.
4450
- this.logger.sendTelemetryEvent({
4451
- eventName: "resubmitUnrecognizedMessageTypeAllowed",
4452
- messageDetails: { type: message.type, compatBehavior },
4453
- });
4454
- } else {
4455
- const error = DataProcessingError.create(
4456
- "Resubmitting runtime message of unexpected type",
4457
- "reSubmitCore",
4458
- undefined /* sequencedMessage */,
4459
- {
4460
- messageDetails: JSON.stringify({
4461
- type: message.type,
4462
- compatBehavior,
4463
- }),
4464
- },
4465
- );
4466
- this.closeFn(error);
4467
- throw error;
4468
- }
4452
+ const error = getUnknownMessageTypeError(message.type, "reSubmitCore" /* codePath */);
4453
+ this.closeFn(error);
4454
+ throw error;
4469
4455
  }
4470
4456
  }
4471
4457
  }
@@ -51,7 +51,6 @@ import {
51
51
  } from "./gcDefinitions.js";
52
52
  import {
53
53
  cloneGCData,
54
- compatBehaviorAllowsGCMessageType,
55
54
  concatGarbageCollectionData,
56
55
  dataStoreNodePathOnly,
57
56
  getGCDataFromSnapshot,
@@ -896,16 +895,10 @@ export class GarbageCollector implements IGarbageCollector {
896
895
  break;
897
896
  }
898
897
  default: {
899
- if (
900
- !compatBehaviorAllowsGCMessageType(gcMessageType, message.compatDetails?.behavior)
901
- ) {
902
- const error = DataProcessingError.create(
903
- `Garbage collection message of unknown type ${gcMessageType}`,
904
- "processMessage",
905
- );
906
- throw error;
907
- }
908
- break;
898
+ throw DataProcessingError.create(
899
+ `Garbage collection message of unknown type ${gcMessageType}`,
900
+ "processMessage",
901
+ );
909
902
  }
910
903
  }
911
904
  }
@@ -1034,7 +1027,7 @@ export class GarbageCollector implements IGarbageCollector {
1034
1027
  *
1035
1028
  * Submit a GC op indicating that the Tombstone with the given path has been loaded.
1036
1029
  * Broadcasting this information in the op stream allows the Summarizer to reset unreferenced state
1037
- * before runnint GC next.
1030
+ * before running GC next.
1038
1031
  */
1039
1032
  private triggerAutoRecovery(nodePath: string) {
1040
1033
  // If sweep isn't enabled, auto-recovery isn't needed since its purpose is to prevent this object from being
@@ -20,6 +20,9 @@ export interface IBatchManagerOptions {
20
20
  * If true, the outbox is allowed to rebase the batch during flushing.
21
21
  */
22
22
  readonly canRebase: boolean;
23
+
24
+ /** If true, don't compare batchID of incoming batches to this. e.g. ID Allocation Batch IDs should be ignored */
25
+ readonly ignoreBatchId?: boolean;
23
26
  }
24
27
 
25
28
  export interface BatchSequenceNumbers {
@@ -123,7 +123,11 @@ export class Outbox {
123
123
 
124
124
  this.mainBatch = new BatchManager({ hardLimit, canRebase: true });
125
125
  this.blobAttachBatch = new BatchManager({ hardLimit, canRebase: true });
126
- this.idAllocationBatch = new BatchManager({ hardLimit, canRebase: false });
126
+ this.idAllocationBatch = new BatchManager({
127
+ hardLimit,
128
+ canRebase: false,
129
+ ignoreBatchId: true,
130
+ });
127
131
  }
128
132
 
129
133
  public get messageCount(): number {
@@ -251,17 +255,20 @@ export class Outbox {
251
255
  }
252
256
 
253
257
  private flushAll(resubmittingBatchId?: BatchId) {
254
- // Don't use resubmittingBatchId for idAllocationBatch.
255
- // ID Allocation messages are not directly resubmitted so we don't want to reuse the batch ID.
256
- this.flushInternal(this.idAllocationBatch);
257
- // We need to flush an empty batch if the main batch *becomes* empty on resubmission.
258
- // When resubmitting the main batch, the blobAttach batch will always be empty since we don't resubmit them simultaneously.
259
- // And conversely, the blobAttach will never *become* empty on resubmit.
260
- // So if both blobAttachBatch and mainBatch are empty, we must submit an empty main batch.
261
- if (resubmittingBatchId && this.blobAttachBatch.empty && this.mainBatch.empty) {
258
+ // If we're resubmitting and all batches are empty, we need to flush an empty batch.
259
+ // Note that we currently resubmit one batch at a time, so on resubmit, 2 of the 3 batches will *always* be empty.
260
+ // It's theoretically possible that we don't *need* to resubmit this empty batch, and in those cases, it'll safely be ignored
261
+ // by the rest of the system, including remote clients.
262
+ // In some cases we *must* resubmit the empty batch (to match up with a non-empty version tracked locally by a container fork), so we do it always.
263
+ const allBatchesEmpty =
264
+ this.idAllocationBatch.empty && this.blobAttachBatch.empty && this.mainBatch.empty;
265
+ if (resubmittingBatchId && allBatchesEmpty) {
262
266
  this.flushEmptyBatch(resubmittingBatchId);
263
267
  return;
264
268
  }
269
+ // Don't use resubmittingBatchId for idAllocationBatch.
270
+ // ID Allocation messages are not directly resubmitted so we don't want to reuse the batch ID.
271
+ this.flushInternal(this.idAllocationBatch);
265
272
  this.flushInternal(
266
273
  this.blobAttachBatch,
267
274
  true /* disableGroupedBatching */,
@@ -332,7 +339,11 @@ export class Outbox {
332
339
  );
333
340
  }
334
341
 
335
- this.params.pendingStateManager.onFlushBatch(rawBatch.messages, clientSequenceNumber);
342
+ this.params.pendingStateManager.onFlushBatch(
343
+ rawBatch.messages,
344
+ clientSequenceNumber,
345
+ batchManager.options.ignoreBatchId,
346
+ );
336
347
  }
337
348
 
338
349
  /**
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.4.0-297385";
9
+ export const pkgVersion = "2.4.0-299707";
@@ -41,7 +41,10 @@ export interface IPendingMessage {
41
41
  localOpMetadata: unknown;
42
42
  opMetadata: Record<string, unknown> | undefined;
43
43
  sequenceNumber?: number;
44
- /** Info about the batch this pending message belongs to, for validation and for computing the batchId on reconnect */
44
+ /**
45
+ * Info about the batch this pending message belongs to, for validation and for computing the batchId on reconnect
46
+ * We don't include batchId itself to avoid redundancy, because that's stamped on opMetadata above
47
+ */
45
48
  batchInfo: {
46
49
  /**
47
50
  * The Batch's original clientId, from when it was first flushed to be submitted.
@@ -55,6 +58,8 @@ export interface IPendingMessage {
55
58
  batchStartCsn: number;
56
59
  /** length of the batch (how many runtime messages here) */
57
60
  length: number;
61
+ /** If true, don't compare batchID of incoming batches to this. e.g. ID Allocation Batch IDs should be ignored */
62
+ ignoreBatchId?: boolean;
58
63
  };
59
64
  }
60
65
 
@@ -239,8 +244,13 @@ export class PendingStateManager implements IDisposable {
239
244
  * @param batch - The batch that was flushed
240
245
  * @param clientSequenceNumber - The CSN of the first message in the batch,
241
246
  * or undefined if the batch was not yet sent (e.g. by the time we flushed we lost the connection)
247
+ * @param ignoreBatchId - Whether to ignore the batchId in the batchStartInfo
242
248
  */
243
- public onFlushBatch(batch: BatchMessage[], clientSequenceNumber: number | undefined) {
249
+ public onFlushBatch(
250
+ batch: BatchMessage[],
251
+ clientSequenceNumber: number | undefined,
252
+ ignoreBatchId?: boolean,
253
+ ) {
244
254
  // clientId and batchStartCsn are used for generating the batchId so we can detect container forks
245
255
  // where this batch was submitted by two different clients rehydrating from the same local state.
246
256
  // In the typical case where the batch was actually sent, use the clientId and clientSequenceNumber.
@@ -269,7 +279,7 @@ export class PendingStateManager implements IDisposable {
269
279
  localOpMetadata,
270
280
  opMetadata,
271
281
  // Note: We only will read this off the first message, but put it on all for simplicity
272
- batchInfo: { clientId, batchStartCsn, length: batch.length },
282
+ batchInfo: { clientId, batchStartCsn, length: batch.length, ignoreBatchId },
273
283
  };
274
284
  this.pendingMessages.push(pendingMessage);
275
285
  }
@@ -328,15 +338,23 @@ export class PendingStateManager implements IDisposable {
328
338
  * @returns whether the batch IDs match
329
339
  */
330
340
  private remoteBatchMatchesPendingBatch(remoteBatchStart: BatchStartInfo): boolean {
331
- // We may have no pending changes - if so, no match, no problem.
332
- const pendingMessage = this.pendingMessages.peekFront();
333
- if (pendingMessage === undefined) {
341
+ // Find the first pending message that uses Batch ID, to compare to the incoming remote batch.
342
+ // If there is no such message, then the incoming remote batch doesn't have a match here and we can return.
343
+ const firstIndexUsingBatchId = Array.from({
344
+ length: this.pendingMessages.length,
345
+ }).findIndex((_, i) => this.pendingMessages.get(i)?.batchInfo.ignoreBatchId !== true);
346
+ const pendingMessageUsingBatchId =
347
+ firstIndexUsingBatchId === -1
348
+ ? undefined
349
+ : this.pendingMessages.get(firstIndexUsingBatchId);
350
+
351
+ if (pendingMessageUsingBatchId === undefined) {
334
352
  return false;
335
353
  }
336
354
 
337
355
  // We must compare the effective batch IDs, since one of these ops
338
356
  // may have been the original, not resubmitted, so wouldn't have its batch ID stamped yet.
339
- const pendingBatchId = getEffectiveBatchId(pendingMessage);
357
+ const pendingBatchId = getEffectiveBatchId(pendingMessageUsingBatchId);
340
358
  const inboundBatchId = getEffectiveBatchId(remoteBatchStart);
341
359
 
342
360
  return pendingBatchId === inboundBatchId;
@@ -488,7 +506,7 @@ export class PendingStateManager implements IDisposable {
488
506
  0xa21 /* No pending message found as we start processing this remote batch */,
489
507
  );
490
508
 
491
- // If this batch became empty on resubmit, batch.messages will be empty (so firstMessage undefined)
509
+ // If this batch became empty on resubmit, batch.messages will be empty (but keyMessage is always set)
492
510
  // and the next pending message should be an empty batch marker.
493
511
  // More Info: We must submit empty batches and track them in case a different fork
494
512
  // of this container also submitted the same batch (and it may not be empty for that fork).