@fluidframework/container-runtime 2.4.0-299374 → 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 (48) hide show
  1. package/container-runtime.test-files.tar +0 -0
  2. package/dist/containerRuntime.d.ts.map +1 -1
  3. package/dist/containerRuntime.js +22 -59
  4. package/dist/containerRuntime.js.map +1 -1
  5. package/dist/gc/garbageCollection.d.ts +1 -1
  6. package/dist/gc/garbageCollection.d.ts.map +1 -1
  7. package/dist/gc/garbageCollection.js +2 -6
  8. package/dist/gc/garbageCollection.js.map +1 -1
  9. package/dist/opLifecycle/batchManager.d.ts +2 -0
  10. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  11. package/dist/opLifecycle/batchManager.js.map +1 -1
  12. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  13. package/dist/opLifecycle/outbox.js +16 -10
  14. package/dist/opLifecycle/outbox.js.map +1 -1
  15. package/dist/packageVersion.d.ts +1 -1
  16. package/dist/packageVersion.js +1 -1
  17. package/dist/packageVersion.js.map +1 -1
  18. package/dist/pendingStateManager.d.ts +8 -2
  19. package/dist/pendingStateManager.d.ts.map +1 -1
  20. package/dist/pendingStateManager.js +14 -7
  21. package/dist/pendingStateManager.js.map +1 -1
  22. package/lib/containerRuntime.d.ts.map +1 -1
  23. package/lib/containerRuntime.js +22 -59
  24. package/lib/containerRuntime.js.map +1 -1
  25. package/lib/gc/garbageCollection.d.ts +1 -1
  26. package/lib/gc/garbageCollection.d.ts.map +1 -1
  27. package/lib/gc/garbageCollection.js +3 -7
  28. package/lib/gc/garbageCollection.js.map +1 -1
  29. package/lib/opLifecycle/batchManager.d.ts +2 -0
  30. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  31. package/lib/opLifecycle/batchManager.js.map +1 -1
  32. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  33. package/lib/opLifecycle/outbox.js +16 -10
  34. package/lib/opLifecycle/outbox.js.map +1 -1
  35. package/lib/packageVersion.d.ts +1 -1
  36. package/lib/packageVersion.js +1 -1
  37. package/lib/packageVersion.js.map +1 -1
  38. package/lib/pendingStateManager.d.ts +8 -2
  39. package/lib/pendingStateManager.d.ts.map +1 -1
  40. package/lib/pendingStateManager.js +14 -7
  41. package/lib/pendingStateManager.js.map +1 -1
  42. package/package.json +20 -18
  43. package/src/containerRuntime.ts +43 -77
  44. package/src/gc/garbageCollection.ts +5 -12
  45. package/src/opLifecycle/batchManager.ts +3 -0
  46. package/src/opLifecycle/outbox.ts +21 -10
  47. package/src/packageVersion.ts +1 -1
  48. 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
  /**
@@ -2527,27 +2543,12 @@ export class ContainerRuntime
2527
2543
  // GC op is only sent in summarizer which should never have stashed ops.
2528
2544
  throw new LoggingError("GC op not expected to be stashed in summarizer");
2529
2545
  default: {
2530
- // This should be extremely rare for stashed ops.
2531
- // It would require a newer runtime stashing ops and then an older one applying them,
2532
- // e.g. if an app rolled back its container version
2533
- const compatBehavior = opContents.compatDetails?.behavior;
2534
- if (!compatBehaviorAllowsMessageType(opContents.type, compatBehavior)) {
2535
- const error = DataProcessingError.create(
2536
- "Stashed runtime message of unexpected type",
2537
- "applyStashedOp",
2538
- undefined /* sequencedMessage */,
2539
- {
2540
- messageDetails: JSON.stringify({
2541
- type: opContents.type,
2542
- compatBehavior,
2543
- }),
2544
- },
2545
- );
2546
- this.closeFn(error);
2547
- throw error;
2548
- }
2549
- // Note: Even if its compat behavior allows it, we don't know how to apply this stashed op.
2550
- // 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;
2551
2552
  }
2552
2553
  }
2553
2554
  }
@@ -2978,27 +2979,13 @@ export class ContainerRuntime
2978
2979
  );
2979
2980
  break;
2980
2981
  default: {
2981
- const compatBehavior = message.compatDetails?.behavior;
2982
- if (!compatBehaviorAllowsMessageType(message.type, compatBehavior)) {
2983
- const error = DataProcessingError.create(
2984
- // Former assert 0x3ce
2985
- "Runtime message of unknown type",
2986
- "OpProcessing",
2987
- message,
2988
- {
2989
- local,
2990
- messageDetails: JSON.stringify({
2991
- type: message.type,
2992
- contentType: typeof message.contents,
2993
- compatBehavior,
2994
- batch: (message.metadata as IBatchMetadata | undefined)?.batch,
2995
- compression: message.compression,
2996
- }),
2997
- },
2998
- );
2999
- this.closeFn(error);
3000
- throw error;
3001
- }
2982
+ const error = getUnknownMessageTypeError(
2983
+ message.type,
2984
+ "validateAndProcessRuntimeMessage" /* codePath */,
2985
+ message,
2986
+ );
2987
+ this.closeFn(error);
2988
+ throw error;
3002
2989
  }
3003
2990
  }
3004
2991
 
@@ -4462,30 +4449,9 @@ export class ContainerRuntime
4462
4449
  // send any ops, as some other client already changed schema.
4463
4450
  break;
4464
4451
  default: {
4465
- // This case should be very rare - it would imply an op was stashed from a
4466
- // future version of runtime code and now is being applied on an older version.
4467
- const compatBehavior = message.compatDetails?.behavior;
4468
- if (compatBehaviorAllowsMessageType(message.type, compatBehavior)) {
4469
- // We do not ultimately resubmit it, to be consistent with this version of the code.
4470
- this.logger.sendTelemetryEvent({
4471
- eventName: "resubmitUnrecognizedMessageTypeAllowed",
4472
- messageDetails: { type: message.type, compatBehavior },
4473
- });
4474
- } else {
4475
- const error = DataProcessingError.create(
4476
- "Resubmitting runtime message of unexpected type",
4477
- "reSubmitCore",
4478
- undefined /* sequencedMessage */,
4479
- {
4480
- messageDetails: JSON.stringify({
4481
- type: message.type,
4482
- compatBehavior,
4483
- }),
4484
- },
4485
- );
4486
- this.closeFn(error);
4487
- throw error;
4488
- }
4452
+ const error = getUnknownMessageTypeError(message.type, "reSubmitCore" /* codePath */);
4453
+ this.closeFn(error);
4454
+ throw error;
4489
4455
  }
4490
4456
  }
4491
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-299374";
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).