@fluidframework/container-runtime 2.1.1 → 2.2.1

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 (196) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +2 -2
  3. package/api-report/container-runtime.legacy.alpha.api.md +4 -3
  4. package/container-runtime.test-files.tar +0 -0
  5. package/dist/batchTracker.d.ts.map +1 -1
  6. package/dist/batchTracker.js.map +1 -1
  7. package/dist/blobManager/blobManager.d.ts.map +1 -1
  8. package/dist/blobManager/blobManager.js +9 -0
  9. package/dist/blobManager/blobManager.js.map +1 -1
  10. package/dist/channelCollection.d.ts +0 -14
  11. package/dist/channelCollection.d.ts.map +1 -1
  12. package/dist/channelCollection.js +2 -12
  13. package/dist/channelCollection.js.map +1 -1
  14. package/dist/containerRuntime.d.ts +34 -6
  15. package/dist/containerRuntime.d.ts.map +1 -1
  16. package/dist/containerRuntime.js +181 -90
  17. package/dist/containerRuntime.js.map +1 -1
  18. package/dist/dataStoreContext.d.ts +9 -18
  19. package/dist/dataStoreContext.d.ts.map +1 -1
  20. package/dist/dataStoreContext.js +40 -78
  21. package/dist/dataStoreContext.js.map +1 -1
  22. package/dist/gc/garbageCollection.d.ts +0 -6
  23. package/dist/gc/garbageCollection.d.ts.map +1 -1
  24. package/dist/gc/garbageCollection.js +23 -66
  25. package/dist/gc/garbageCollection.js.map +1 -1
  26. package/dist/gc/gcConfigs.d.ts.map +1 -1
  27. package/dist/gc/gcConfigs.js +11 -34
  28. package/dist/gc/gcConfigs.js.map +1 -1
  29. package/dist/gc/gcDefinitions.d.ts +9 -52
  30. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  31. package/dist/gc/gcDefinitions.js +3 -23
  32. package/dist/gc/gcDefinitions.js.map +1 -1
  33. package/dist/gc/gcHelpers.d.ts.map +1 -1
  34. package/dist/gc/gcHelpers.js +2 -6
  35. package/dist/gc/gcHelpers.js.map +1 -1
  36. package/dist/gc/gcSummaryStateTracker.d.ts +1 -1
  37. package/dist/gc/gcSummaryStateTracker.d.ts.map +1 -1
  38. package/dist/gc/gcSummaryStateTracker.js +4 -8
  39. package/dist/gc/gcSummaryStateTracker.js.map +1 -1
  40. package/dist/gc/gcTelemetry.d.ts +1 -9
  41. package/dist/gc/gcTelemetry.d.ts.map +1 -1
  42. package/dist/gc/gcTelemetry.js +3 -25
  43. package/dist/gc/gcTelemetry.js.map +1 -1
  44. package/dist/gc/index.d.ts +2 -2
  45. package/dist/gc/index.d.ts.map +1 -1
  46. package/dist/gc/index.js +2 -7
  47. package/dist/gc/index.js.map +1 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/messageTypes.d.ts +6 -5
  53. package/dist/messageTypes.d.ts.map +1 -1
  54. package/dist/messageTypes.js.map +1 -1
  55. package/dist/metadata.d.ts +9 -1
  56. package/dist/metadata.d.ts.map +1 -1
  57. package/dist/metadata.js +6 -1
  58. package/dist/metadata.js.map +1 -1
  59. package/dist/opLifecycle/index.d.ts +1 -1
  60. package/dist/opLifecycle/index.d.ts.map +1 -1
  61. package/dist/opLifecycle/index.js.map +1 -1
  62. package/dist/opLifecycle/opGroupingManager.d.ts +8 -0
  63. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  64. package/dist/opLifecycle/opGroupingManager.js +34 -2
  65. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  66. package/dist/opLifecycle/outbox.d.ts +1 -0
  67. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  68. package/dist/opLifecycle/outbox.js +20 -0
  69. package/dist/opLifecycle/outbox.js.map +1 -1
  70. package/dist/opLifecycle/remoteMessageProcessor.d.ts +38 -19
  71. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  72. package/dist/opLifecycle/remoteMessageProcessor.js +67 -43
  73. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  74. package/dist/packageVersion.d.ts +1 -1
  75. package/dist/packageVersion.js +1 -1
  76. package/dist/packageVersion.js.map +1 -1
  77. package/dist/pendingStateManager.d.ts +33 -22
  78. package/dist/pendingStateManager.d.ts.map +1 -1
  79. package/dist/pendingStateManager.js +148 -105
  80. package/dist/pendingStateManager.js.map +1 -1
  81. package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  82. package/dist/summary/summarizerNode/summarizerNode.js +5 -1
  83. package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
  84. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts +3 -4
  85. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  86. package/dist/summary/summarizerNode/summarizerNodeWithGc.js +16 -15
  87. package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  88. package/lib/batchTracker.d.ts.map +1 -1
  89. package/lib/batchTracker.js.map +1 -1
  90. package/lib/blobManager/blobManager.d.ts.map +1 -1
  91. package/lib/blobManager/blobManager.js +9 -0
  92. package/lib/blobManager/blobManager.js.map +1 -1
  93. package/lib/channelCollection.d.ts +0 -14
  94. package/lib/channelCollection.d.ts.map +1 -1
  95. package/lib/channelCollection.js +2 -11
  96. package/lib/channelCollection.js.map +1 -1
  97. package/lib/containerRuntime.d.ts +34 -6
  98. package/lib/containerRuntime.d.ts.map +1 -1
  99. package/lib/containerRuntime.js +181 -90
  100. package/lib/containerRuntime.js.map +1 -1
  101. package/lib/dataStoreContext.d.ts +9 -18
  102. package/lib/dataStoreContext.d.ts.map +1 -1
  103. package/lib/dataStoreContext.js +27 -65
  104. package/lib/dataStoreContext.js.map +1 -1
  105. package/lib/gc/garbageCollection.d.ts +0 -6
  106. package/lib/gc/garbageCollection.d.ts.map +1 -1
  107. package/lib/gc/garbageCollection.js +25 -68
  108. package/lib/gc/garbageCollection.js.map +1 -1
  109. package/lib/gc/gcConfigs.d.ts.map +1 -1
  110. package/lib/gc/gcConfigs.js +12 -35
  111. package/lib/gc/gcConfigs.js.map +1 -1
  112. package/lib/gc/gcDefinitions.d.ts +9 -52
  113. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  114. package/lib/gc/gcDefinitions.js +2 -22
  115. package/lib/gc/gcDefinitions.js.map +1 -1
  116. package/lib/gc/gcHelpers.d.ts.map +1 -1
  117. package/lib/gc/gcHelpers.js +2 -6
  118. package/lib/gc/gcHelpers.js.map +1 -1
  119. package/lib/gc/gcSummaryStateTracker.d.ts +1 -1
  120. package/lib/gc/gcSummaryStateTracker.d.ts.map +1 -1
  121. package/lib/gc/gcSummaryStateTracker.js +4 -8
  122. package/lib/gc/gcSummaryStateTracker.js.map +1 -1
  123. package/lib/gc/gcTelemetry.d.ts +1 -9
  124. package/lib/gc/gcTelemetry.d.ts.map +1 -1
  125. package/lib/gc/gcTelemetry.js +3 -24
  126. package/lib/gc/gcTelemetry.js.map +1 -1
  127. package/lib/gc/index.d.ts +2 -2
  128. package/lib/gc/index.d.ts.map +1 -1
  129. package/lib/gc/index.js +2 -2
  130. package/lib/gc/index.js.map +1 -1
  131. package/lib/index.d.ts +1 -1
  132. package/lib/index.d.ts.map +1 -1
  133. package/lib/index.js +1 -1
  134. package/lib/index.js.map +1 -1
  135. package/lib/messageTypes.d.ts +6 -5
  136. package/lib/messageTypes.d.ts.map +1 -1
  137. package/lib/messageTypes.js.map +1 -1
  138. package/lib/metadata.d.ts +9 -1
  139. package/lib/metadata.d.ts.map +1 -1
  140. package/lib/metadata.js +4 -0
  141. package/lib/metadata.js.map +1 -1
  142. package/lib/opLifecycle/index.d.ts +1 -1
  143. package/lib/opLifecycle/index.d.ts.map +1 -1
  144. package/lib/opLifecycle/index.js.map +1 -1
  145. package/lib/opLifecycle/opGroupingManager.d.ts +8 -0
  146. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  147. package/lib/opLifecycle/opGroupingManager.js +34 -2
  148. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  149. package/lib/opLifecycle/outbox.d.ts +1 -0
  150. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  151. package/lib/opLifecycle/outbox.js +20 -0
  152. package/lib/opLifecycle/outbox.js.map +1 -1
  153. package/lib/opLifecycle/remoteMessageProcessor.d.ts +38 -19
  154. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  155. package/lib/opLifecycle/remoteMessageProcessor.js +67 -43
  156. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  157. package/lib/packageVersion.d.ts +1 -1
  158. package/lib/packageVersion.js +1 -1
  159. package/lib/packageVersion.js.map +1 -1
  160. package/lib/pendingStateManager.d.ts +33 -22
  161. package/lib/pendingStateManager.d.ts.map +1 -1
  162. package/lib/pendingStateManager.js +149 -106
  163. package/lib/pendingStateManager.js.map +1 -1
  164. package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  165. package/lib/summary/summarizerNode/summarizerNode.js +5 -1
  166. package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
  167. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts +3 -4
  168. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  169. package/lib/summary/summarizerNode/summarizerNodeWithGc.js +16 -15
  170. package/lib/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  171. package/package.json +21 -21
  172. package/src/batchTracker.ts +4 -2
  173. package/src/blobManager/blobManager.ts +9 -0
  174. package/src/channelCollection.ts +2 -11
  175. package/src/containerRuntime.ts +216 -121
  176. package/src/dataStoreContext.ts +29 -93
  177. package/src/gc/garbageCollection.ts +26 -79
  178. package/src/gc/gcConfigs.ts +12 -45
  179. package/src/gc/gcDefinitions.ts +10 -55
  180. package/src/gc/gcHelpers.ts +10 -8
  181. package/src/gc/gcSummaryStateTracker.ts +6 -9
  182. package/src/gc/gcTelemetry.ts +3 -38
  183. package/src/gc/index.ts +2 -6
  184. package/src/index.ts +0 -1
  185. package/src/messageTypes.ts +12 -11
  186. package/src/metadata.ts +16 -2
  187. package/src/opLifecycle/index.ts +1 -0
  188. package/src/opLifecycle/opGroupingManager.ts +42 -3
  189. package/src/opLifecycle/outbox.ts +30 -0
  190. package/src/opLifecycle/remoteMessageProcessor.ts +110 -56
  191. package/src/packageVersion.ts +1 -1
  192. package/src/pendingStateManager.ts +209 -168
  193. package/src/summary/README.md +31 -28
  194. package/src/summary/summarizerNode/summarizerNode.ts +6 -1
  195. package/src/summary/summarizerNode/summarizerNodeWithGc.ts +20 -43
  196. package/src/summary/summaryFormats.md +25 -22
@@ -3,10 +3,8 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { ICriticalContainerError } from "@fluidframework/container-definitions";
7
6
  import { IDisposable } from "@fluidframework/core-interfaces";
8
7
  import { assert, Lazy } from "@fluidframework/core-utils/internal";
9
- import { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
10
8
  import {
11
9
  ITelemetryLoggerExt,
12
10
  DataProcessingError,
@@ -16,10 +14,13 @@ import {
16
14
  import Deque from "double-ended-queue";
17
15
  import { v4 as uuid } from "uuid";
18
16
 
19
- import { InboundSequencedContainerRuntimeMessage } from "./messageTypes.js";
20
- import { asBatchMetadata, IBatchMetadata } from "./metadata.js";
21
- import { BatchId, BatchMessage, generateBatchId } from "./opLifecycle/index.js";
22
- import { pkgVersion } from "./packageVersion.js";
17
+ import {
18
+ type InboundContainerRuntimeMessage,
19
+ type InboundSequencedContainerRuntimeMessage,
20
+ type LocalContainerRuntimeMessage,
21
+ } from "./messageTypes.js";
22
+ import { asBatchMetadata, asEmptyBatchLocalOpMetadata } from "./metadata.js";
23
+ import { BatchId, BatchMessage, generateBatchId, InboundBatch } from "./opLifecycle/index.js";
23
24
 
24
25
  /**
25
26
  * This represents a message that has been submitted and is added to the pending queue when `submit` is called on the
@@ -34,8 +35,8 @@ export interface IPendingMessage {
34
35
  localOpMetadata: unknown;
35
36
  opMetadata: Record<string, unknown> | undefined;
36
37
  sequenceNumber?: number;
37
- /** Info needed to compute the batchId on reconnect */
38
- batchIdContext: {
38
+ /** Info about the batch this pending message belongs to, for validation and for computing the batchId on reconnect */
39
+ batchInfo: {
39
40
  /** The Batch's original clientId, from when it was first flushed to be submitted */
40
41
  clientId: string;
41
42
  /**
@@ -43,13 +44,15 @@ export interface IPendingMessage {
43
44
  * @remarks A negative value means it was not yet submitted when queued here (e.g. disconnected right before flush fired)
44
45
  */
45
46
  batchStartCsn: number;
47
+ /** length of the batch (how many runtime messages here) */
48
+ length: number;
46
49
  };
47
50
  }
48
51
 
49
52
  type Patch<T, U> = U & Omit<T, keyof U>;
50
53
 
51
- /** First version of the type (pre-dates batchIdContext) */
52
- type IPendingMessageV0 = Patch<IPendingMessage, { batchIdContext?: undefined }>;
54
+ /** First version of the type (pre-dates batchInfo) */
55
+ type IPendingMessageV0 = Patch<IPendingMessage, { batchInfo?: undefined }>;
53
56
 
54
57
  /**
55
58
  * Union of all supported schemas for when applying stashed ops
@@ -74,33 +77,47 @@ export type PendingMessageResubmitData = Pick<
74
77
  export interface IRuntimeStateHandler {
75
78
  connected(): boolean;
76
79
  clientId(): string | undefined;
77
- close(error?: ICriticalContainerError): void;
78
80
  applyStashedOp(content: string): Promise<unknown>;
79
81
  reSubmitBatch(batch: PendingMessageResubmitData[], batchId: BatchId): void;
80
82
  isActiveConnection: () => boolean;
81
83
  isAttached: () => boolean;
82
84
  }
83
85
 
84
- /** Union of keys of T */
85
- type KeysOfUnion<T extends object> = T extends T ? keyof T : never;
86
- /** *Partial* type all possible combinations of properties and values of union T.
87
- * This loosens typing allowing access to all possible properties without
88
- * narrowing.
89
- */
90
- type AnyComboFromUnion<T extends object> = { [P in KeysOfUnion<T>]?: T[P] };
86
+ function isEmptyBatchPendingMessage(message: IPendingMessageFromStash): boolean {
87
+ const content = JSON.parse(message.content);
88
+ return content.type === "groupedBatch" && content.contents?.length === 0;
89
+ }
91
90
 
92
- function buildPendingMessageContent(
93
- // AnyComboFromUnion is needed need to gain access to compatDetails that
94
- // is only defined for some cases.
95
- message: AnyComboFromUnion<InboundSequencedContainerRuntimeMessage>,
96
- ): string {
91
+ function buildPendingMessageContent(message: InboundSequencedContainerRuntimeMessage): string {
97
92
  // IMPORTANT: Order matters here, this must match the order of the properties used
98
93
  // when submitting the message.
99
- const { type, contents, compatDetails } = message;
94
+ const { type, contents, compatDetails }: InboundContainerRuntimeMessage = message;
100
95
  // Any properties that are not defined, won't be emitted by stringify.
101
96
  return JSON.stringify({ type, contents, compatDetails });
102
97
  }
103
98
 
99
+ function typesOfKeys<T extends object>(obj: T): Record<keyof T, string> {
100
+ return Object.keys(obj).reduce((acc, key) => {
101
+ acc[key] = typeof obj[key];
102
+ return acc;
103
+ }, {}) as Record<keyof T, string>;
104
+ }
105
+
106
+ function scrubAndStringify(
107
+ message: InboundContainerRuntimeMessage | LocalContainerRuntimeMessage,
108
+ ): string {
109
+ // Scrub the whole object in case there are unexpected keys
110
+ const scrubbed: Record<string, unknown> = typesOfKeys(message);
111
+
112
+ // For these known/expected keys, we can either drill in (for contents)
113
+ // or just use the value as-is (since it's not personal info)
114
+ scrubbed.contents = message.contents && typesOfKeys(message.contents);
115
+ scrubbed.compatDetails = message.compatDetails;
116
+ scrubbed.type = message.type;
117
+
118
+ return JSON.stringify(scrubbed);
119
+ }
120
+
104
121
  function withoutLocalOpMetadata(message: IPendingMessage): IPendingMessage {
105
122
  return {
106
123
  ...message,
@@ -108,6 +125,20 @@ function withoutLocalOpMetadata(message: IPendingMessage): IPendingMessage {
108
125
  };
109
126
  }
110
127
 
128
+ /**
129
+ * Get the effective batch ID for a pending message.
130
+ * If the batch ID is already present in the message's op metadata, return it.
131
+ * Otherwise, generate a new batch ID using the client ID and batch start CSN.
132
+ * @param pendingMessage - The pending message
133
+ * @returns The effective batch ID
134
+ */
135
+ function getEffectiveBatchId(pendingMessage: IPendingMessage): string {
136
+ return (
137
+ asBatchMetadata(pendingMessage.opMetadata)?.batchId ??
138
+ generateBatchId(pendingMessage.batchInfo.clientId, pendingMessage.batchInfo.batchStartCsn)
139
+ );
140
+ }
141
+
111
142
  /**
112
143
  * PendingStateManager is responsible for maintaining the messages that have not been sent or have not yet been
113
144
  * acknowledged by the server. It also maintains the batch information for both automatically and manually flushed
@@ -136,13 +167,6 @@ export class PendingStateManager implements IDisposable {
136
167
  this.pendingMessages.clear();
137
168
  });
138
169
 
139
- // Indicates whether we are processing a batch.
140
- private isProcessingBatch: boolean = false;
141
-
142
- // This stores the first message in the batch that we are processing. This is used to verify that we get
143
- // the correct batch metadata.
144
- private pendingBatchBeginMessage: ISequencedDocumentMessage | undefined;
145
-
146
170
  /** Used to ensure we don't replay ops on the same connection twice */
147
171
  private clientIdFromLastReplay: string | undefined;
148
172
 
@@ -248,8 +272,8 @@ export class PendingStateManager implements IDisposable {
248
272
  content,
249
273
  localOpMetadata,
250
274
  opMetadata,
251
- // Note: We only need this on the first message.
252
- batchIdContext: { clientId, batchStartCsn },
275
+ // Note: We only will read this off the first message, but put it on all for simplicity
276
+ batchInfo: { clientId, batchStartCsn, length: batch.length },
253
277
  };
254
278
  this.pendingMessages.push(pendingMessage);
255
279
  }
@@ -274,7 +298,15 @@ export class PendingStateManager implements IDisposable {
274
298
  }
275
299
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276
300
  const nextMessage = this.initialMessages.shift()!;
301
+ // Nothing to apply if the message is an empty batch.
302
+ // We still need to track it for resubmission.
277
303
  try {
304
+ if (isEmptyBatchPendingMessage(nextMessage)) {
305
+ nextMessage.localOpMetadata = { emptyBatch: true }; // equivalent to applyStashedOp for empty batch
306
+ patchbatchInfo(nextMessage); // Back compat
307
+ this.pendingMessages.push(nextMessage);
308
+ continue;
309
+ }
278
310
  // applyStashedOp will cause the DDS to behave as if it has sent the op but not actually send it
279
311
  const localOpMetadata = await this.stateHandler.applyStashedOp(nextMessage.content);
280
312
  if (!this.stateHandler.isAttached()) {
@@ -284,7 +316,7 @@ export class PendingStateManager implements IDisposable {
284
316
  } else {
285
317
  nextMessage.localOpMetadata = localOpMetadata;
286
318
  // then we push onto pendingMessages which will cause PendingStateManager to resubmit when we connect
287
- patchBatchIdContext(nextMessage); // Back compat
319
+ patchbatchInfo(nextMessage); // Back compat
288
320
  this.pendingMessages.push(nextMessage);
289
321
  }
290
322
  } catch (error) {
@@ -294,153 +326,164 @@ export class PendingStateManager implements IDisposable {
294
326
  }
295
327
 
296
328
  /**
297
- * Processes a local message once its ack'd by the server. It verifies that there was no data corruption and that
298
- * the batch information was preserved for batch messages.
299
- * @param message - The message that got ack'd and needs to be processed.
300
- * @param batchStartCsn - The clientSequenceNumber of the start of this message's batch (assigned during submit)
301
- * (not to be confused with message.clientSequenceNumber - the overwritten value in case of grouped batching)
329
+ * Processes an inbound batch of messages - May be local or remote.
330
+ *
331
+ * @param batch - The inbound batch of messages to process. Could be local or remote.
332
+ * @param local - true if we submitted this batch and expect corresponding pending messages
333
+ * @returns The inbound batch's messages with localOpMetadata "zipped" in.
334
+ *
335
+ * @remarks Closes the container if:
336
+ * - The batchStartCsn doesn't match for local batches
337
+ */
338
+ public processInboundBatch(
339
+ batch: InboundBatch,
340
+ local: boolean,
341
+ ): {
342
+ message: InboundSequencedContainerRuntimeMessage;
343
+ localOpMetadata?: unknown;
344
+ }[] {
345
+ if (local) {
346
+ return this.processPendingLocalBatch(batch);
347
+ }
348
+
349
+ // No localOpMetadata for remote messages
350
+ return batch.messages.map((message) => ({ message }));
351
+ }
352
+
353
+ /**
354
+ * Processes the incoming batch from the server that was submitted by this client.
355
+ * It verifies that messages are received in the right order and that the batch information is correct.
356
+ * @param batch - The inbound batch (originating from this client) to correlate with the pending local state
357
+ * @returns The inbound batch's messages with localOpMetadata "zipped" in.
302
358
  */
303
- public processPendingLocalMessage(
304
- message: InboundSequencedContainerRuntimeMessage,
305
- batchStartCsn: number,
359
+ private processPendingLocalBatch(batch: InboundBatch): {
360
+ message: InboundSequencedContainerRuntimeMessage;
361
+ localOpMetadata: unknown;
362
+ }[] {
363
+ this.onLocalBatchBegin(batch);
364
+
365
+ // Empty batch
366
+ if (batch.messages.length === 0) {
367
+ assert(
368
+ batch.emptyBatchSequenceNumber !== undefined,
369
+ 0x9fb /* Expected sequence number for empty batch */,
370
+ );
371
+ const localOpMetadata = this.processNextPendingMessage(batch.emptyBatchSequenceNumber);
372
+ assert(
373
+ asEmptyBatchLocalOpMetadata(localOpMetadata)?.emptyBatch === true,
374
+ 0xa20 /* Expected empty batch marker */,
375
+ );
376
+ return [];
377
+ }
378
+
379
+ return batch.messages.map((message) => ({
380
+ message,
381
+ localOpMetadata: this.processNextPendingMessage(message.sequenceNumber, message),
382
+ }));
383
+ }
384
+
385
+ /**
386
+ * Processes the pending local copy of message that's been ack'd by the server.
387
+ * @param sequenceNumber - The sequenceNumber from the server corresponding to the next pending message.
388
+ * @param message - [optional] The entire incoming message, for comparing contents with the pending message for extra validation.
389
+ * @throws DataProcessingError if the pending message content doesn't match the incoming message content.
390
+ * @returns - The localOpMetadata of the next pending message, to be sent to whoever submitted the original message.
391
+ */
392
+ private processNextPendingMessage(
393
+ sequenceNumber: number,
394
+ message?: InboundSequencedContainerRuntimeMessage,
306
395
  ): unknown {
307
- // Get the next message from the pending queue. Verify a message exists.
308
396
  const pendingMessage = this.pendingMessages.peekFront();
309
397
  assert(
310
398
  pendingMessage !== undefined,
311
399
  0x169 /* "No pending message found for this remote message" */,
312
400
  );
313
401
 
314
- // This may be the start of a batch.
315
- this.maybeProcessBatchBegin(message, batchStartCsn, pendingMessage);
316
-
317
- pendingMessage.sequenceNumber = message.sequenceNumber;
402
+ pendingMessage.sequenceNumber = sequenceNumber;
318
403
  this.savedOps.push(withoutLocalOpMetadata(pendingMessage));
319
404
 
320
405
  this.pendingMessages.shift();
321
406
 
322
- const messageContent = buildPendingMessageContent(message);
407
+ // message is undefined in the Empty Batch case,
408
+ // because we don't have an incoming message to compare and pendingMessage is just a placeholder anyway.
409
+ if (message !== undefined) {
410
+ const messageContent = buildPendingMessageContent(message);
411
+
412
+ // Stringified content should match
413
+ if (pendingMessage.content !== messageContent) {
414
+ const pendingContentObj = JSON.parse(
415
+ pendingMessage.content,
416
+ ) as LocalContainerRuntimeMessage;
417
+ const incomingContentObj = JSON.parse(
418
+ messageContent,
419
+ ) as InboundContainerRuntimeMessage;
420
+
421
+ const contentsMatch =
422
+ pendingContentObj.contents === incomingContentObj.contents ||
423
+ (pendingContentObj.contents !== undefined &&
424
+ incomingContentObj.contents !== undefined &&
425
+ JSON.stringify(pendingContentObj.contents) ===
426
+ JSON.stringify(incomingContentObj.contents));
427
+
428
+ this.logger.sendErrorEvent({
429
+ eventName: "unexpectedAckReceived",
430
+ details: {
431
+ pendingContentScrubbed: scrubAndStringify(pendingContentObj),
432
+ incomingContentScrubbed: scrubAndStringify(incomingContentObj),
433
+ contentsMatch,
434
+ },
435
+ });
323
436
 
324
- // Stringified content should match
325
- if (pendingMessage.content !== messageContent) {
326
- this.stateHandler.close(
327
- DataProcessingError.create(
437
+ throw DataProcessingError.create(
328
438
  "pending local message content mismatch",
329
439
  "unexpectedAckReceived",
330
440
  message,
331
- {
332
- expectedMessageType: JSON.parse(pendingMessage.content).type,
333
- },
334
- ),
335
- );
336
- return;
337
- }
338
-
339
- // Post-processing part - If we are processing a batch then this could be the last message in the batch.
340
- this.maybeProcessBatchEnd(message);
341
-
342
- return pendingMessage.localOpMetadata;
343
- }
344
-
345
- /**
346
- * This message could be the first message in batch. If so, set batch state marking the beginning of a batch.
347
- * @param message - The message that is being processed.
348
- * @param batchStartCsn - The clientSequenceNumber of the start of this message's batch (assigned during submit)
349
- * @param pendingMessage - The corresponding pendingMessage.
350
- */
351
- private maybeProcessBatchBegin(
352
- message: ISequencedDocumentMessage,
353
- batchStartCsn: number,
354
- pendingMessage: IPendingMessage,
355
- ) {
356
- if (!this.isProcessingBatch) {
357
- // Expecting the start of a batch (maybe single-message).
358
- if (pendingMessage.batchIdContext.batchStartCsn !== batchStartCsn) {
359
- this.logger?.sendErrorEvent({
360
- eventName: "BatchClientSequenceNumberMismatch",
361
- details: {
362
- processingBatch: !!this.pendingBatchBeginMessage,
363
- pendingBatchCsn: pendingMessage.batchIdContext.batchStartCsn,
364
- batchStartCsn,
365
- messageBatchMetadata: (message.metadata as any)?.batch,
366
- pendingMessageBatchMetadata: (pendingMessage.opMetadata as any)?.batch,
367
- },
368
- messageDetails: extractSafePropertiesFromMessage(message),
369
- });
441
+ );
370
442
  }
371
443
  }
372
444
 
373
- // This message is the first in a batch if the "batch" property on the metadata is set to true
374
- if ((message.metadata as IBatchMetadata | undefined)?.batch) {
375
- // We should not already be processing a batch and there should be no pending batch begin message.
376
- assert(
377
- !this.isProcessingBatch && this.pendingBatchBeginMessage === undefined,
378
- 0x16b /* "The pending batch state indicates we are already processing a batch" */,
379
- );
380
-
381
- // Set the pending batch state indicating we have started processing a batch.
382
- this.pendingBatchBeginMessage = message;
383
- this.isProcessingBatch = true;
384
- }
445
+ return pendingMessage.localOpMetadata;
385
446
  }
386
447
 
387
448
  /**
388
- * This message could be the last message in batch. If so, clear batch state since the batch is complete.
389
- * @param message - The message that is being processed.
449
+ * Check if the incoming batch matches the batch info for the next pending message.
390
450
  */
391
- private maybeProcessBatchEnd(message: ISequencedDocumentMessage) {
392
- if (!this.isProcessingBatch) {
393
- return;
394
- }
395
-
396
- // There should be a pending batch begin message.
451
+ private onLocalBatchBegin(batch: InboundBatch) {
452
+ // Get the next message from the pending queue. Verify a message exists.
453
+ const pendingMessage = this.pendingMessages.peekFront();
397
454
  assert(
398
- this.pendingBatchBeginMessage !== undefined,
399
- 0x16d /* "There is no pending batch begin message" */,
455
+ pendingMessage !== undefined,
456
+ 0xa21 /* No pending message found as we start processing this remote batch */,
400
457
  );
401
458
 
402
- const batchEndMetadata = (message.metadata as IBatchMetadata | undefined)?.batch;
403
- if (this.pendingMessages.isEmpty() || batchEndMetadata === false) {
404
- // Get the batch begin metadata from the first message in the batch.
405
- const batchBeginMetadata = (
406
- this.pendingBatchBeginMessage.metadata as IBatchMetadata | undefined
407
- )?.batch;
408
-
409
- // There could be just a single message in the batch. If so, it should not have any batch metadata. If there
410
- // are multiple messages in the batch, verify that we got the correct batch begin and end metadata.
411
- if (this.pendingBatchBeginMessage === message) {
412
- assert(
413
- batchBeginMetadata === undefined,
414
- 0x16e /* "Batch with single message should not have batch metadata" */,
415
- );
416
- } else {
417
- if (batchBeginMetadata !== true || batchEndMetadata !== false) {
418
- this.stateHandler.close(
419
- DataProcessingError.create(
420
- "Pending batch inconsistency", // Formerly known as asserts 0x16f and 0x170
421
- "processPendingLocalMessage",
422
- message,
423
- {
424
- runtimeVersion: pkgVersion,
425
- batchClientId:
426
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
427
- this.pendingBatchBeginMessage.clientId === null
428
- ? "null"
429
- : this.pendingBatchBeginMessage.clientId,
430
- clientId: this.stateHandler.clientId(),
431
- hasBatchStart: batchBeginMetadata === true,
432
- hasBatchEnd: batchEndMetadata === false,
433
- messageType: message.type,
434
- pendingMessagesCount: this.pendingMessagesCount,
435
- },
436
- ),
437
- );
438
- }
439
- }
440
-
441
- // Clear the pending batch state now that we have processed the entire batch.
442
- this.pendingBatchBeginMessage = undefined;
443
- this.isProcessingBatch = false;
459
+ // Note: This could be undefined if this batch became empty on resubmit.
460
+ // In this case the next pending message is an empty batch marker.
461
+ // Empty batches became empty on Resubmit, and submit them and track them in case
462
+ // a different fork of this container also submitted the same batch (and it may not be empty for that fork).
463
+ const firstMessage = batch.messages.length > 0 ? batch.messages[0] : undefined;
464
+ const expectedPendingBatchLength = batch.messages.length === 0 ? 1 : batch.messages.length;
465
+
466
+ // We expect the incoming batch to be of the same length, starting at the same clientSequenceNumber,
467
+ // as the batch we originally submitted.
468
+ // We have another later check to compare the message contents, which we'd expect to fail if this check does,
469
+ // so we don't throw here, merely log. In a later release this check may replace that one.
470
+ if (
471
+ pendingMessage.batchInfo.batchStartCsn !== batch.batchStartCsn ||
472
+ (pendingMessage.batchInfo.length >= 0 && // -1 length is back compat and isn't suitable for this check
473
+ pendingMessage.batchInfo.length !== expectedPendingBatchLength)
474
+ ) {
475
+ this.logger?.sendErrorEvent({
476
+ eventName: "BatchInfoMismatch",
477
+ details: {
478
+ pendingBatchCsn: pendingMessage.batchInfo.batchStartCsn,
479
+ batchStartCsn: batch.batchStartCsn,
480
+ pendingBatchLength: pendingMessage.batchInfo.length,
481
+ batchLength: batch.messages.length,
482
+ pendingMessageBatchMetadata: asBatchMetadata(pendingMessage.opMetadata)?.batch,
483
+ messageBatchMetadata: asBatchMetadata(firstMessage?.metadata)?.batch,
484
+ },
485
+ messageDetails: firstMessage && extractSafePropertiesFromMessage(firstMessage),
486
+ });
444
487
  }
445
488
  }
446
489
 
@@ -482,14 +525,12 @@ export class PendingStateManager implements IDisposable {
482
525
  assert(batchMetadataFlag !== false, 0x41b /* We cannot process batches in chunks */);
483
526
 
484
527
  // The next message starts a batch (possibly single-message), and we'll need its batchId.
485
- // We'll find batchId on this message if it was previously generated.
486
- // Otherwise, generate it now - this is the first time resubmitting this batch.
487
- const batchId =
488
- asBatchMetadata(pendingMessage.opMetadata)?.batchId ??
489
- generateBatchId(
490
- pendingMessage.batchIdContext.clientId,
491
- pendingMessage.batchIdContext.batchStartCsn,
492
- );
528
+ const batchId = getEffectiveBatchId(pendingMessage);
529
+ // Resubmit no messages, with the batchId. Will result in another empty batch marker.
530
+ if (asEmptyBatchLocalOpMetadata(pendingMessage.localOpMetadata)?.emptyBatch === true) {
531
+ this.stateHandler.reSubmitBatch([], batchId);
532
+ continue;
533
+ }
493
534
 
494
535
  /**
495
536
  * We must preserve the distinct batches on resubmit.
@@ -561,13 +602,13 @@ export class PendingStateManager implements IDisposable {
561
602
  }
562
603
  }
563
604
 
564
- /** For back-compat if trying to apply stashed ops that pre-date batchIdContext */
565
- function patchBatchIdContext(
605
+ /** For back-compat if trying to apply stashed ops that pre-date batchInfo */
606
+ function patchbatchInfo(
566
607
  message: IPendingMessageFromStash,
567
608
  ): asserts message is IPendingMessage {
568
- const batchIdContext: IPendingMessageFromStash["batchIdContext"] = message.batchIdContext;
569
- if (batchIdContext === undefined) {
609
+ const batchInfo: IPendingMessageFromStash["batchInfo"] = message.batchInfo;
610
+ if (batchInfo === undefined) {
570
611
  // Using uuid guarantees uniqueness, retaining existing behavior
571
- message.batchIdContext = { clientId: uuid(), batchStartCsn: -1 };
612
+ message.batchInfo = { clientId: uuid(), batchStartCsn: -1, length: -1 };
572
613
  }
573
614
  }
@@ -9,12 +9,12 @@
9
9
  - [Summary Lifecycle](#summary-lifecycle)
10
10
  - [Single-commit vs two-commit summaries](#single-commit-vs-two-commit-summaries)
11
11
  - [Incremental summaries](#incremental-summaries)
12
- - [Resiliency](#resiliency)
12
+ - [Resiliency](#resiliency)
13
13
  - [What does a summary look like?](#what-does-a-summary-look-like)
14
14
 
15
15
  ## Introduction
16
16
 
17
- This document provides a conceptual overview of summarization without going into a lot of technical or implementation details. It describes what summaries are, how / when they are generated and what do they look like. The goal is for this to be an entry point into summarization for users and developers alike.
17
+ This document provides a conceptual overview of summarization. It describes what summaries are, how / when they are generated, and what they look like. The goal is for this to be an entry point into summarization for users and developers alike.
18
18
 
19
19
  ### Summary vs snapshot
20
20
 
@@ -22,23 +22,22 @@ The terms summary and snapshot are sometimes used interchangeably. Both represen
22
22
 
23
23
  ## Why do we need summaries?
24
24
 
25
- A 'summary' captures the state of a container at a point in time. Without it, a client would have to apply every operation in the op log, even if those operations no longer affected the current state (e.g. op 1 inserts h and op 2 deletes h). For very large op logs, this would be very expensive for clients to both process and download from the service.
26
- Instead, when a client opens a collaborative document, it can download a snapshot of the container, and simply process new operations from that point forward.
25
+ A 'summary' captures the state of a container at a point in time so that future clients can start from this point. Without it, a client would have to apply every operation in the op log, even if those operations (hereafter ops) no longer affected the current state (e.g. op 1 inserts 'h' and op 2 deletes 'h'). For large op logs, this would be very expensive for clients to both download from the service and to process them.
26
+ Instead, when a client opens a collaborative document, it downloads the latest snapshot of the container, and simply process new operations from that point forward.
27
27
 
28
28
  ## Who generates summaries?
29
29
 
30
- Summaries can be generated by any client connected in "write" mode. In the current implementation, summaries are generated by a separate non-interactive client. Using a separate client is an optimization - this client doesn't have to take local changes into account which can make the summary process more complicated. A summarizer client has the following characteristics:
30
+ Summaries can be generated by any client connected in "write" mode. They are generated by a separate non-interactive client called the summarizer client. Using a separate client is an optimization - this client doesn't have to take local changes into account which can make the summary process more complicated.
31
+ A summarizer client is like any other client connected to the document except that users cannot interact with this client, and it only works on the state it receives from other clients. It has a brand-new container with its own connection to services.
32
+ All the clients connected to the document participate in a process called "summary client election" to elect a "parent summarizer" client. Typically, it's the oldest "write" client connected to the document. The parent summarizer client spawns a "summarizer" client which is responsible for summarization.
31
33
 
32
- - All the clients connected to the document participate in a process called "summary client election" to elect a "parent summarizer" client. Typically, it's the oldest "write" client connected to the document. The parent summarizer client spawns a "summarizer" client which is responsible for summarization.
33
- - A summarizer client is like any other client connected to the document except that users cannot interact with this client, and it only works on the state it receives from other clients. It has a brand-new container with its own connection to services.
34
-
35
- > Note that if the summarizer client closes, the "summary client election" process will choose a new one, if applicable. The default "summary client election" algorithm is to select the oldest "write" client as the parent summarizer client which in turn will create the summarizer client.
34
+ Note: If the summarizer client closes, the "summary client election" process will choose a new one, if there are eligible clients.
36
35
 
37
36
  ## When are summaries generated?
38
37
 
39
- The summarizer client periodically generates summary based on heuristics calculated based on configurations such as the number of user or system ops received, the amount of time a client has been idle (hasn't received any ops), the maximum time since last summary, maximum number of ops since last summary, etc. The heuristic configurations are defined by an _ISummaryConfigurationHeuristics_ interface defined in [this file](../../src/containerRuntime.ts).
38
+ The summarizer client periodically generates summary based on heuristics calculated based on configurations such as the number of user or system operations received, the amount of time a client has been idle (hasn't received any ops), the maximum time since last summary, maximum number of ops since last summary, etc. The heuristic configurations are defined by an `ISummaryConfigurationHeuristics` interface defined in [containerRuntime.ts in the container-runtime package][container-runtime].
40
39
 
41
- The summarizer client uses a default set of configurations defined by _DefaultSummaryConfiguration_ in [this file](../../src/containerRuntime.ts). These can be overridden by providing a new set of configurations as part of container runtime options during creation.
40
+ The summarizer client uses a default set of configurations defined by `DefaultSummaryConfiguration` in [containerRuntime.ts in the container-runtime package][container-runtime]. These can be overridden by providing a new set of configurations as part of container runtime options during creation.
42
41
 
43
42
  ## How are summaries generated?
44
43
 
@@ -47,35 +46,39 @@ When summarization process is triggered, every object in the container's object
47
46
  ### Summary Lifecycle
48
47
 
49
48
  The lifecycle of a summary starts when a "parent summarizer" client is elected.
49
+
50
50
  - The parent summarizer spawns a non-interactive summarizer client.
51
51
  - The summarizer client periodically starts a summary as per heuristics. A summary happens at a particular sequence number called the "summary sequence number" or reference sequence number for the summary.
52
- - The container runtime (or simply runtime) generates a summary tree (described in the ["What does a summary look like?"](#what-does-a-summary-look-like) section below).
53
- - The runtime uploads the summary tree to the Fluid storage service which returns a handle (unique id) to the summary if the upload is successful. Otherwise, it returns a failure.
52
+ - The container runtime (hereafter runtime) generates a summary tree (described in the ["What does a summary look like?"](#what-does-a-summary-look-like) section below).
53
+ - The runtime uploads the summary tree to the Fluid storage service which returns a handle (unique id) to the summary if the upload is successful. Otherwise, it returns a failure. The runtime also includes the handle of the last successful summary. If this information is incorrect, the service will reject this summary. This is done to ensure that [incremental summaries](#incremental-summaries) are correct.
54
54
  - The runtime submits a "summarize" op to the Fluid ordering service containing the uploaded summary handle and the summary sequence number.
55
- - The ordering service stamps it with a sequence number (like any other op) and broadcasts the summarize op.
56
- - Another service on the server responds to the summarize op.
57
- - If the summary is accepted, it sends a "summary ack" with the summary sequence number and a summary handle. This handle may or may not be the same as the one in summary op depending on whether this is a single-commit or two-commit summary. More details on this below.
58
- - If the summary is rejected, it sends a "summary nack" with the details of the summary op
59
- - The runtime completes the summary process on receiving the summary ack / nack. The runtime has a timeout called "maxAckWaitTime" and if the summary op, ack or nack is not received within this time, it will fail this summary.
60
-
61
- ### Single-commit vs two-commit summaries
62
-
63
- By default, Fluid uses "two-commit summaries" mode where the two commits refer to the storage committing the summary twice and returning two different handles for it - One when the summary is uploaded and second, on responding to the summary op via a summary ack. In this mode, when the server receives the summary op, it augments the corresponding summary with a "protocol" blob hence generating a new commit and new handle for this summary which it returns in the summary ack.
64
-
65
- Fluid is switching to "single-commit summary" mode where the client adds the "protocol" blob when uploading the summary. Thus, the server doesn't need to augment the summary and the summary ack is no longer required. As soon as the summary is uploaded (first commit), the summary process is complete. The "summarize" op then is just a way to indicate that a summary happened, and it has details of the summary
55
+ - The ordering service stamps it with a sequence number (like any other op) and broadcasts the summarize op. This creates a record in the op log that a summary was submitted and it lets other clients know about it. Non-summarizer clients don't do anything with the summary op. The summarizer client that submitted it processes it and waits for a summary ack / nack. Future summarizer clients also process them and validates that a corresponding summary ack / nack is received.
56
+ - The ordering service then responds to the summarize op:
57
+ - If the summary is accepted, it sends a "summary ack" with the summary sequence number and a summary handle.
58
+ - If the summary is rejected, it sends a "summary nack" with the details of the summary op.
59
+ - The runtime processes the summary ack or nack completes the summary process as success or failure accordingly.
60
+ - If the summary is successful, the handle in the ack becomes the last successful summary's handle which is used when upload summaries as described earlier.
61
+ - If the summary failed, the summarizer client closes and the summary election process starts to elect a new one.
62
+ - The runtime has a timeout called "maxAckWaitTime" and if the summary op, ack or nack is not received within this time, it will fail this summary.
66
63
 
67
64
  ### Incremental summaries
68
65
 
69
- Summaries are incremental, i.e., if an object (or node) did not change since the last summary, it doesn't have to re-summarize its entire contents. Fluid supports the concept of a summary handle defined in [this file](../../../../../common/lib/protocol-definitions/src/summary.ts). A handle is a path to a subtree in a snapshot and it allows objects to reference a subtree in the previous snapshot, which is essentially an instruction to storage to find that subtree and populate into new summary.
66
+ Summaries are incremental, i.e., if an object (or node) did not change since the last summary, it doesn't have to re-summarize its entire contents. Fluid supports the concept of a summary handle defined in [summary.ts in the protocol-definitions package][summary-protocol]. A handle is a path to a subtree in a snapshot and it allows objects to reference a subtree in the previous snapshot, which is essentially an instruction to storage to find that subtree and populate into new summary.
67
+
68
+ So, say that a data store or DDS did not change since the last summary, it doesn't have to go through the whole summary process described above. It can instead return an ISummaryHandle with path to its subtree in the last successful summary. The same applies to other types of content like a single content blob within an object's summary tree.
70
69
 
71
- Say that a data store or DDS did not change since the last summary, it doesn't have to go through the whole summary process described above. It can instead return an ISummaryHandle with path to its subtree in the previous snapshot. The same applies to other types of content like a single content blob within an object's summary tree.
70
+ For incremental summary, objects diff their content against the last summary to determine whether to send a summary handle. So, it's crucial that the last summary information be correct or else the summary will be incorrect. So, during upload, the last summary's handle is also sent and the service will validate that it's correct.
72
71
 
73
72
  ### Resiliency
74
73
 
75
- The summarization process is designed to be resilient - Basically, a document will eventually summarize and make progress even if there are intermittent failures or disruptions. Some examples of steps taken to achieve this:
74
+ The summarization process is designed to be resilient - A document will eventually summarize and make progress even if there are intermittent failures or disruptions. Some examples of steps taken to achieve this:
75
+
76
76
  - Last summary - Usually, if the "parent summarizer" client disconnects or shuts down, the "summarizer" client also shuts down and the summarizer election process begins. However, if there a certain number of un-summarized ops, the summarizer client will perform a "last summary" even if the parent shuts down. This is done to make progress in scenarios where new summarizer clients are closed quickly because the parent summarizer keeps disconnecting repeatedly.
77
77
  - Retries - The summarizer has a retry mechanism which can identify certain types of intermittent failures either in the client or in the server. It will retry the summary attempt for these failures a certain number of times. This helps in cases where there are intermittent failures such as throttling errors from the server which goes away after waiting for a while.
78
78
 
79
79
  ## What does a summary look like?
80
80
 
81
- The format of a summary is described in [summary formats](./summaryFormats.md).
81
+ The format of summaries (and snapshots) is described in [summary and snapshot formats](./summaryFormats.md).
82
+
83
+ [container-runtime]: ../../src/containerRuntime.ts
84
+ [summary-protocol]: /common/lib/protocol-definitions/src/summary.ts