@fluidframework/container-runtime 2.1.0-276985 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +71 -18
  3. package/api-extractor/api-extractor.current.json +5 -0
  4. package/api-extractor/api-extractor.legacy.json +1 -1
  5. package/api-extractor.json +1 -1
  6. package/api-report/container-runtime.legacy.public.api.md +9 -0
  7. package/container-runtime.test-files.tar +0 -0
  8. package/dist/blobManager/blobManager.d.ts +10 -0
  9. package/dist/blobManager/blobManager.d.ts.map +1 -1
  10. package/dist/blobManager/blobManager.js +19 -0
  11. package/dist/blobManager/blobManager.js.map +1 -1
  12. package/dist/channelCollection.d.ts +1 -1
  13. package/dist/channelCollection.d.ts.map +1 -1
  14. package/dist/channelCollection.js +40 -8
  15. package/dist/channelCollection.js.map +1 -1
  16. package/dist/containerRuntime.d.ts +14 -5
  17. package/dist/containerRuntime.d.ts.map +1 -1
  18. package/dist/containerRuntime.js +151 -99
  19. package/dist/containerRuntime.js.map +1 -1
  20. package/dist/dataStoreContext.d.ts +4 -0
  21. package/dist/dataStoreContext.d.ts.map +1 -1
  22. package/dist/dataStoreContext.js +9 -3
  23. package/dist/dataStoreContext.js.map +1 -1
  24. package/dist/gc/garbageCollection.d.ts +1 -1
  25. package/dist/gc/garbageCollection.d.ts.map +1 -1
  26. package/dist/gc/garbageCollection.js +14 -8
  27. package/dist/gc/garbageCollection.js.map +1 -1
  28. package/dist/gc/gcDefinitions.d.ts +4 -2
  29. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  30. package/dist/gc/gcDefinitions.js.map +1 -1
  31. package/dist/gc/gcHelpers.d.ts.map +1 -1
  32. package/dist/gc/gcHelpers.js +12 -0
  33. package/dist/gc/gcHelpers.js.map +1 -1
  34. package/dist/gc/gcTelemetry.d.ts +3 -2
  35. package/dist/gc/gcTelemetry.d.ts.map +1 -1
  36. package/dist/gc/gcTelemetry.js +6 -6
  37. package/dist/gc/gcTelemetry.js.map +1 -1
  38. package/dist/legacy.d.ts +1 -1
  39. package/dist/metadata.d.ts +7 -1
  40. package/dist/metadata.d.ts.map +1 -1
  41. package/dist/metadata.js +6 -0
  42. package/dist/metadata.js.map +1 -1
  43. package/dist/opLifecycle/batchManager.d.ts +8 -1
  44. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  45. package/dist/opLifecycle/batchManager.js +37 -16
  46. package/dist/opLifecycle/batchManager.js.map +1 -1
  47. package/dist/opLifecycle/definitions.d.ts +1 -1
  48. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  49. package/dist/opLifecycle/definitions.js.map +1 -1
  50. package/dist/opLifecycle/index.d.ts +2 -2
  51. package/dist/opLifecycle/index.d.ts.map +1 -1
  52. package/dist/opLifecycle/index.js +3 -1
  53. package/dist/opLifecycle/index.js.map +1 -1
  54. package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
  55. package/dist/opLifecycle/opCompressor.js +12 -8
  56. package/dist/opLifecycle/opCompressor.js.map +1 -1
  57. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  58. package/dist/opLifecycle/opGroupingManager.js +14 -11
  59. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  60. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  61. package/dist/opLifecycle/opSplitter.js +11 -6
  62. package/dist/opLifecycle/opSplitter.js.map +1 -1
  63. package/dist/opLifecycle/outbox.d.ts +22 -6
  64. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  65. package/dist/opLifecycle/outbox.js +43 -21
  66. package/dist/opLifecycle/outbox.js.map +1 -1
  67. package/dist/opLifecycle/remoteMessageProcessor.d.ts +10 -8
  68. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  69. package/dist/opLifecycle/remoteMessageProcessor.js +39 -15
  70. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  71. package/dist/packageVersion.d.ts +1 -1
  72. package/dist/packageVersion.d.ts.map +1 -1
  73. package/dist/packageVersion.js +1 -1
  74. package/dist/packageVersion.js.map +1 -1
  75. package/dist/pendingStateManager.d.ts +37 -13
  76. package/dist/pendingStateManager.d.ts.map +1 -1
  77. package/dist/pendingStateManager.js +95 -45
  78. package/dist/pendingStateManager.js.map +1 -1
  79. package/dist/public.d.ts +1 -1
  80. package/dist/scheduleManager.js +4 -0
  81. package/dist/scheduleManager.js.map +1 -1
  82. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  83. package/dist/summary/summarizerNode/summarizerNodeUtils.js +2 -0
  84. package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  85. package/dist/summary/summaryFormat.d.ts.map +1 -1
  86. package/dist/summary/summaryFormat.js +4 -1
  87. package/dist/summary/summaryFormat.js.map +1 -1
  88. package/internal.d.ts +1 -1
  89. package/legacy.d.ts +1 -1
  90. package/lib/blobManager/blobManager.d.ts +10 -0
  91. package/lib/blobManager/blobManager.d.ts.map +1 -1
  92. package/lib/blobManager/blobManager.js +19 -0
  93. package/lib/blobManager/blobManager.js.map +1 -1
  94. package/lib/channelCollection.d.ts +1 -1
  95. package/lib/channelCollection.d.ts.map +1 -1
  96. package/lib/channelCollection.js +40 -8
  97. package/lib/channelCollection.js.map +1 -1
  98. package/lib/containerRuntime.d.ts +14 -5
  99. package/lib/containerRuntime.d.ts.map +1 -1
  100. package/lib/containerRuntime.js +152 -100
  101. package/lib/containerRuntime.js.map +1 -1
  102. package/lib/dataStoreContext.d.ts +4 -0
  103. package/lib/dataStoreContext.d.ts.map +1 -1
  104. package/lib/dataStoreContext.js +10 -4
  105. package/lib/dataStoreContext.js.map +1 -1
  106. package/lib/gc/garbageCollection.d.ts +1 -1
  107. package/lib/gc/garbageCollection.d.ts.map +1 -1
  108. package/lib/gc/garbageCollection.js +14 -8
  109. package/lib/gc/garbageCollection.js.map +1 -1
  110. package/lib/gc/gcDefinitions.d.ts +4 -2
  111. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  112. package/lib/gc/gcDefinitions.js.map +1 -1
  113. package/lib/gc/gcHelpers.d.ts.map +1 -1
  114. package/lib/gc/gcHelpers.js +12 -0
  115. package/lib/gc/gcHelpers.js.map +1 -1
  116. package/lib/gc/gcTelemetry.d.ts +3 -2
  117. package/lib/gc/gcTelemetry.d.ts.map +1 -1
  118. package/lib/gc/gcTelemetry.js +6 -6
  119. package/lib/gc/gcTelemetry.js.map +1 -1
  120. package/lib/legacy.d.ts +1 -1
  121. package/lib/metadata.d.ts +7 -1
  122. package/lib/metadata.d.ts.map +1 -1
  123. package/lib/metadata.js +4 -1
  124. package/lib/metadata.js.map +1 -1
  125. package/lib/opLifecycle/batchManager.d.ts +8 -1
  126. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  127. package/lib/opLifecycle/batchManager.js +35 -15
  128. package/lib/opLifecycle/batchManager.js.map +1 -1
  129. package/lib/opLifecycle/definitions.d.ts +1 -1
  130. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  131. package/lib/opLifecycle/definitions.js.map +1 -1
  132. package/lib/opLifecycle/index.d.ts +2 -2
  133. package/lib/opLifecycle/index.d.ts.map +1 -1
  134. package/lib/opLifecycle/index.js +2 -2
  135. package/lib/opLifecycle/index.js.map +1 -1
  136. package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
  137. package/lib/opLifecycle/opCompressor.js +12 -8
  138. package/lib/opLifecycle/opCompressor.js.map +1 -1
  139. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  140. package/lib/opLifecycle/opGroupingManager.js +14 -11
  141. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  142. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  143. package/lib/opLifecycle/opSplitter.js +11 -6
  144. package/lib/opLifecycle/opSplitter.js.map +1 -1
  145. package/lib/opLifecycle/outbox.d.ts +22 -6
  146. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  147. package/lib/opLifecycle/outbox.js +44 -22
  148. package/lib/opLifecycle/outbox.js.map +1 -1
  149. package/lib/opLifecycle/remoteMessageProcessor.d.ts +10 -8
  150. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  151. package/lib/opLifecycle/remoteMessageProcessor.js +37 -14
  152. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  153. package/lib/packageVersion.d.ts +1 -1
  154. package/lib/packageVersion.d.ts.map +1 -1
  155. package/lib/packageVersion.js +1 -1
  156. package/lib/packageVersion.js.map +1 -1
  157. package/lib/pendingStateManager.d.ts +37 -13
  158. package/lib/pendingStateManager.d.ts.map +1 -1
  159. package/lib/pendingStateManager.js +95 -45
  160. package/lib/pendingStateManager.js.map +1 -1
  161. package/lib/public.d.ts +1 -1
  162. package/lib/scheduleManager.js +4 -0
  163. package/lib/scheduleManager.js.map +1 -1
  164. package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  165. package/lib/summary/summarizerNode/summarizerNodeUtils.js +2 -0
  166. package/lib/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  167. package/lib/summary/summaryFormat.d.ts.map +1 -1
  168. package/lib/summary/summaryFormat.js +4 -1
  169. package/lib/summary/summaryFormat.js.map +1 -1
  170. package/package.json +46 -31
  171. package/src/blobManager/blobManager.ts +19 -0
  172. package/src/channelCollection.ts +48 -11
  173. package/src/containerRuntime.ts +203 -133
  174. package/src/dataStoreContext.ts +22 -4
  175. package/src/gc/garbageCollection.ts +15 -10
  176. package/src/gc/gcDefinitions.ts +7 -2
  177. package/src/gc/gcHelpers.ts +18 -6
  178. package/src/gc/gcTelemetry.ts +20 -8
  179. package/src/metadata.ts +11 -1
  180. package/src/opLifecycle/README.md +0 -8
  181. package/src/opLifecycle/batchManager.ts +49 -16
  182. package/src/opLifecycle/definitions.ts +1 -1
  183. package/src/opLifecycle/index.ts +13 -2
  184. package/src/opLifecycle/opCompressor.ts +12 -8
  185. package/src/opLifecycle/opGroupingManager.ts +14 -11
  186. package/src/opLifecycle/opSplitter.ts +10 -6
  187. package/src/opLifecycle/outbox.ts +64 -26
  188. package/src/opLifecycle/remoteMessageProcessor.ts +56 -17
  189. package/src/packageVersion.ts +1 -1
  190. package/src/pendingStateManager.ts +173 -74
  191. package/src/scheduleManager.ts +6 -2
  192. package/src/summary/README.md +81 -0
  193. package/src/summary/summarizerNode/summarizerNodeUtils.ts +3 -1
  194. package/src/summary/summaryFormat.ts +3 -1
  195. package/src/summary/summaryFormats.md +69 -8
  196. package/tsconfig.json +0 -1
  197. package/src/summary/images/appTree.png +0 -0
  198. package/src/summary/images/protocolAndAppTree.png +0 -0
  199. package/src/summary/images/summaryTree.png +0 -0
@@ -14,15 +14,18 @@ import {
14
14
  extractSafePropertiesFromMessage,
15
15
  } from "@fluidframework/telemetry-utils/internal";
16
16
  import Deque from "double-ended-queue";
17
+ import { v4 as uuid } from "uuid";
17
18
 
18
- import { InboundSequencedContainerRuntimeMessage } from "./messageTypes.js";
19
- import { IBatchMetadata } from "./metadata.js";
20
- import type { BatchMessage } from "./opLifecycle/index.js";
19
+ import { type InboundSequencedContainerRuntimeMessage } from "./messageTypes.js";
20
+ import { asBatchMetadata, IBatchMetadata } from "./metadata.js";
21
+ import { BatchId, BatchMessage, generateBatchId } from "./opLifecycle/index.js";
21
22
  import { pkgVersion } from "./packageVersion.js";
22
23
 
23
24
  /**
24
25
  * This represents a message that has been submitted and is added to the pending queue when `submit` is called on the
25
26
  * ContainerRuntime. This message has either not been ack'd by the server or has not been submitted to the server yet.
27
+ *
28
+ * @remarks This is the current serialization format for pending local state when a Container is serialized.
26
29
  */
27
30
  export interface IPendingMessage {
28
31
  type: "message";
@@ -31,9 +34,30 @@ export interface IPendingMessage {
31
34
  localOpMetadata: unknown;
32
35
  opMetadata: Record<string, unknown> | undefined;
33
36
  sequenceNumber?: number;
34
- batchStartCsn?: number;
37
+ /** Info needed to compute the batchId on reconnect */
38
+ batchIdContext: {
39
+ /** The Batch's original clientId, from when it was first flushed to be submitted */
40
+ clientId: string;
41
+ /**
42
+ * The Batch's original clientSequenceNumber, from when it was first flushed to be submitted
43
+ * @remarks A negative value means it was not yet submitted when queued here (e.g. disconnected right before flush fired)
44
+ */
45
+ batchStartCsn: number;
46
+ };
35
47
  }
36
48
 
49
+ type Patch<T, U> = U & Omit<T, keyof U>;
50
+
51
+ /** First version of the type (pre-dates batchIdContext) */
52
+ type IPendingMessageV0 = Patch<IPendingMessage, { batchIdContext?: undefined }>;
53
+
54
+ /**
55
+ * Union of all supported schemas for when applying stashed ops
56
+ *
57
+ * @remarks When the format changes, this type should update to reflect all possible schemas.
58
+ */
59
+ type IPendingMessageFromStash = IPendingMessageV0 | IPendingMessage;
60
+
37
61
  export interface IPendingLocalState {
38
62
  /**
39
63
  * list of pending states, including ops and batch information
@@ -41,19 +65,18 @@ export interface IPendingLocalState {
41
65
  pendingStates: IPendingMessage[];
42
66
  }
43
67
 
44
- export interface IPendingBatchMessage {
45
- content: string;
46
- localOpMetadata: unknown;
47
- opMetadata: Record<string, unknown> | undefined;
48
- }
68
+ /** Info needed to replay/resubmit a pending message */
69
+ export type PendingMessageResubmitData = Pick<
70
+ IPendingMessage,
71
+ "content" | "localOpMetadata" | "opMetadata"
72
+ >;
49
73
 
50
74
  export interface IRuntimeStateHandler {
51
75
  connected(): boolean;
52
76
  clientId(): string | undefined;
53
77
  close(error?: ICriticalContainerError): void;
54
78
  applyStashedOp(content: string): Promise<unknown>;
55
- reSubmit(message: IPendingBatchMessage): void;
56
- reSubmitBatch(batch: IPendingBatchMessage[]): void;
79
+ reSubmitBatch(batch: PendingMessageResubmitData[], batchId: BatchId): void;
57
80
  isActiveConnection: () => boolean;
58
81
  isAttached: () => boolean;
59
82
  }
@@ -95,15 +118,19 @@ function withoutLocalOpMetadata(message: IPendingMessage): IPendingMessage {
95
118
  * It verifies that all the ops are acked, are received in the right order and batch information is correct.
96
119
  */
97
120
  export class PendingStateManager implements IDisposable {
121
+ /** Messages that will need to be resubmitted if not ack'd before the next reconnection */
98
122
  private readonly pendingMessages = new Deque<IPendingMessage>();
99
- // This queue represents already acked messages.
100
- private readonly initialMessages = new Deque<IPendingMessage>();
123
+ /** Messages stashed from a previous container, now being rehydrated. Need to be resubmitted. */
124
+ private readonly initialMessages = new Deque<IPendingMessageFromStash>();
101
125
 
102
126
  /**
103
127
  * Sequenced local ops that are saved when stashing since pending ops may depend on them
104
128
  */
105
129
  private savedOps: IPendingMessage[] = [];
106
130
 
131
+ /** Used to stand in for batchStartCsn for messages that weren't submitted (so no CSN) */
132
+ private negativeCounter: number = -1;
133
+
107
134
  private readonly disposeOnce = new Lazy<void>(() => {
108
135
  this.initialMessages.clear();
109
136
  this.pendingMessages.clear();
@@ -116,7 +143,8 @@ export class PendingStateManager implements IDisposable {
116
143
  // the correct batch metadata.
117
144
  private pendingBatchBeginMessage: ISequencedDocumentMessage | undefined;
118
145
 
119
- private clientId: string | undefined;
146
+ /** Used to ensure we don't replay ops on the same connection twice */
147
+ private clientIdFromLastReplay: string | undefined;
120
148
 
121
149
  /**
122
150
  * The pending messages count. Includes `pendingMessages` and `initialMessages` to keep in sync with
@@ -176,11 +204,11 @@ export class PendingStateManager implements IDisposable {
176
204
 
177
205
  constructor(
178
206
  private readonly stateHandler: IRuntimeStateHandler,
179
- initialLocalState: IPendingLocalState | undefined,
180
- private readonly logger: ITelemetryLoggerExt | undefined,
207
+ stashedLocalState: IPendingLocalState | undefined,
208
+ private readonly logger: ITelemetryLoggerExt,
181
209
  ) {
182
- if (initialLocalState?.pendingStates) {
183
- this.initialMessages.push(...initialLocalState.pendingStates);
210
+ if (stashedLocalState?.pendingStates) {
211
+ this.initialMessages.push(...stashedLocalState.pendingStates);
184
212
  }
185
213
  }
186
214
 
@@ -197,6 +225,16 @@ export class PendingStateManager implements IDisposable {
197
225
  * or undefined if the batch was not yet sent (e.g. by the time we flushed we lost the connection)
198
226
  */
199
227
  public onFlushBatch(batch: BatchMessage[], clientSequenceNumber: number | undefined) {
228
+ // If we're connected this is the client of the current connection,
229
+ // otherwise it's the clientId that just disconnected
230
+ // It's only undefined if we've NEVER connected. This is a tight corner case and we can
231
+ // simply make up a unique ID in this case.
232
+ const clientId = this.stateHandler.clientId() ?? uuid();
233
+
234
+ // If the batch was not yet sent, we need to assign a unique batchStartCsn
235
+ // Use a negative number to distinguish these from real CSNs
236
+ const batchStartCsn = clientSequenceNumber ?? this.negativeCounter--;
237
+
200
238
  for (const message of batch) {
201
239
  const {
202
240
  contents: content = "",
@@ -210,7 +248,8 @@ export class PendingStateManager implements IDisposable {
210
248
  content,
211
249
  localOpMetadata,
212
250
  opMetadata,
213
- batchStartCsn: clientSequenceNumber,
251
+ // Note: We only need this on the first message.
252
+ batchIdContext: { clientId, batchStartCsn },
214
253
  };
215
254
  this.pendingMessages.push(pendingMessage);
216
255
  }
@@ -245,6 +284,7 @@ export class PendingStateManager implements IDisposable {
245
284
  } else {
246
285
  nextMessage.localOpMetadata = localOpMetadata;
247
286
  // then we push onto pendingMessages which will cause PendingStateManager to resubmit when we connect
287
+ patchBatchIdContext(nextMessage); // Back compat
248
288
  this.pendingMessages.push(nextMessage);
249
289
  }
250
290
  } catch (error) {
@@ -253,6 +293,25 @@ export class PendingStateManager implements IDisposable {
253
293
  }
254
294
  }
255
295
 
296
+ /**
297
+ * Processes the incoming batch from the server. It verifies that messages are received in the right order and
298
+ * that the batch information is correct.
299
+ * @param batch - The batch that is being processed.
300
+ * @param batchStartCsn - The clientSequenceNumber of the start of this message's batch
301
+ */
302
+ public processPendingLocalBatch(
303
+ batch: InboundSequencedContainerRuntimeMessage[],
304
+ batchStartCsn: number,
305
+ ): {
306
+ message: InboundSequencedContainerRuntimeMessage;
307
+ localOpMetadata: unknown;
308
+ }[] {
309
+ return batch.map((message) => ({
310
+ message,
311
+ localOpMetadata: this.processPendingLocalMessage(message, batchStartCsn),
312
+ }));
313
+ }
314
+
256
315
  /**
257
316
  * Processes a local message once its ack'd by the server. It verifies that there was no data corruption and that
258
317
  * the batch information was preserved for batch messages.
@@ -260,37 +319,25 @@ export class PendingStateManager implements IDisposable {
260
319
  * @param batchStartCsn - The clientSequenceNumber of the start of this message's batch (assigned during submit)
261
320
  * (not to be confused with message.clientSequenceNumber - the overwritten value in case of grouped batching)
262
321
  */
263
- public processPendingLocalMessage(
322
+ private processPendingLocalMessage(
264
323
  message: InboundSequencedContainerRuntimeMessage,
265
324
  batchStartCsn: number,
266
325
  ): unknown {
267
- // Pre-processing part - This may be the start of a batch.
268
- this.maybeProcessBatchBegin(message);
269
326
  // Get the next message from the pending queue. Verify a message exists.
270
327
  const pendingMessage = this.pendingMessages.peekFront();
271
328
  assert(
272
329
  pendingMessage !== undefined,
273
330
  0x169 /* "No pending message found for this remote message" */,
274
331
  );
332
+
333
+ // This may be the start of a batch.
334
+ this.maybeProcessBatchBegin(message, batchStartCsn, pendingMessage);
335
+
275
336
  pendingMessage.sequenceNumber = message.sequenceNumber;
276
337
  this.savedOps.push(withoutLocalOpMetadata(pendingMessage));
277
338
 
278
339
  this.pendingMessages.shift();
279
340
 
280
- if (pendingMessage.batchStartCsn !== batchStartCsn) {
281
- this.logger?.sendErrorEvent({
282
- eventName: "BatchClientSequenceNumberMismatch",
283
- details: {
284
- processingBatch: !!this.pendingBatchBeginMessage,
285
- pendingBatchCsn: pendingMessage.batchStartCsn,
286
- batchStartCsn,
287
- messageBatchMetadata: (message.metadata as any)?.batch,
288
- pendingMessageBatchMetadata: (pendingMessage.opMetadata as any)?.batch,
289
- },
290
- messageDetails: extractSafePropertiesFromMessage(message),
291
- });
292
- }
293
-
294
341
  const messageContent = buildPendingMessageContent(message);
295
342
 
296
343
  // Stringified content should match
@@ -317,8 +364,31 @@ export class PendingStateManager implements IDisposable {
317
364
  /**
318
365
  * This message could be the first message in batch. If so, set batch state marking the beginning of a batch.
319
366
  * @param message - The message that is being processed.
367
+ * @param batchStartCsn - The clientSequenceNumber of the start of this message's batch (assigned during submit)
368
+ * @param pendingMessage - The corresponding pendingMessage.
320
369
  */
321
- private maybeProcessBatchBegin(message: ISequencedDocumentMessage) {
370
+ private maybeProcessBatchBegin(
371
+ message: ISequencedDocumentMessage,
372
+ batchStartCsn: number,
373
+ pendingMessage: IPendingMessage,
374
+ ) {
375
+ if (!this.isProcessingBatch) {
376
+ // Expecting the start of a batch (maybe single-message).
377
+ if (pendingMessage.batchIdContext.batchStartCsn !== batchStartCsn) {
378
+ this.logger?.sendErrorEvent({
379
+ eventName: "BatchClientSequenceNumberMismatch",
380
+ details: {
381
+ processingBatch: !!this.pendingBatchBeginMessage,
382
+ pendingBatchCsn: pendingMessage.batchIdContext.batchStartCsn,
383
+ batchStartCsn,
384
+ messageBatchMetadata: (message.metadata as any)?.batch,
385
+ pendingMessageBatchMetadata: (pendingMessage.opMetadata as any)?.batch,
386
+ },
387
+ messageDetails: extractSafePropertiesFromMessage(message),
388
+ });
389
+ }
390
+ }
391
+
322
392
  // This message is the first in a batch if the "batch" property on the metadata is set to true
323
393
  if ((message.metadata as IBatchMetadata | undefined)?.batch) {
324
394
  // We should not already be processing a batch and there should be no pending batch begin message.
@@ -406,10 +476,10 @@ export class PendingStateManager implements IDisposable {
406
476
 
407
477
  // This assert suggests we are about to send same ops twice, which will result in data loss.
408
478
  assert(
409
- this.clientId !== this.stateHandler.clientId(),
479
+ this.clientIdFromLastReplay !== this.stateHandler.clientId(),
410
480
  0x173 /* "replayPendingStates called twice for same clientId!" */,
411
481
  );
412
- this.clientId = this.stateHandler.clientId();
482
+ this.clientIdFromLastReplay = this.stateHandler.clientId();
413
483
 
414
484
  assert(
415
485
  this.initialMessages.isEmpty(),
@@ -426,54 +496,72 @@ export class PendingStateManager implements IDisposable {
426
496
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
427
497
  let pendingMessage = this.pendingMessages.shift()!;
428
498
  remainingPendingMessagesCount--;
429
- assert(
430
- pendingMessage.opMetadata?.batch !== false,
431
- 0x41b /* We cannot process batches in chunks */,
432
- );
499
+
500
+ const batchMetadataFlag = asBatchMetadata(pendingMessage.opMetadata)?.batch;
501
+ assert(batchMetadataFlag !== false, 0x41b /* We cannot process batches in chunks */);
502
+
503
+ // The next message starts a batch (possibly single-message), and we'll need its batchId.
504
+ // We'll find batchId on this message if it was previously generated.
505
+ // Otherwise, generate it now - this is the first time resubmitting this batch.
506
+ const batchId =
507
+ asBatchMetadata(pendingMessage.opMetadata)?.batchId ??
508
+ generateBatchId(
509
+ pendingMessage.batchIdContext.clientId,
510
+ pendingMessage.batchIdContext.batchStartCsn,
511
+ );
433
512
 
434
513
  /**
435
- * We want to ensure grouped messages get processed in a batch.
514
+ * We must preserve the distinct batches on resubmit.
436
515
  * Note: It is not possible for the PendingStateManager to receive a partially acked batch. It will
437
- * either receive the whole batch ack or nothing at all.
516
+ * either receive the whole batch ack or nothing at all. @see ScheduleManager for how this works.
438
517
  */
439
- if (pendingMessage.opMetadata?.batch) {
440
- assert(
441
- remainingPendingMessagesCount > 0,
442
- 0x554 /* Last pending message cannot be a batch begin */,
518
+ if (batchMetadataFlag === undefined) {
519
+ // Single-message batch
520
+
521
+ this.stateHandler.reSubmitBatch(
522
+ [
523
+ {
524
+ content: pendingMessage.content,
525
+ localOpMetadata: pendingMessage.localOpMetadata,
526
+ opMetadata: pendingMessage.opMetadata,
527
+ },
528
+ ],
529
+ batchId,
443
530
  );
531
+ continue;
532
+ }
533
+ // else: batchMetadataFlag === true (It's a typical multi-message batch)
444
534
 
445
- const batch: IPendingBatchMessage[] = [];
446
-
447
- // check is >= because batch end may be last pending message
448
- while (remainingPendingMessagesCount >= 0) {
449
- batch.push({
450
- content: pendingMessage.content,
451
- localOpMetadata: pendingMessage.localOpMetadata,
452
- opMetadata: pendingMessage.opMetadata,
453
- });
535
+ assert(
536
+ remainingPendingMessagesCount > 0,
537
+ 0x554 /* Last pending message cannot be a batch begin */,
538
+ );
454
539
 
455
- if (pendingMessage.opMetadata?.batch === false) {
456
- break;
457
- }
458
- assert(remainingPendingMessagesCount > 0, 0x555 /* No batch end found */);
459
-
460
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
461
- pendingMessage = this.pendingMessages.shift()!;
462
- remainingPendingMessagesCount--;
463
- assert(
464
- pendingMessage.opMetadata?.batch !== true,
465
- 0x556 /* Batch start needs a corresponding batch end */,
466
- );
467
- }
540
+ const batch: PendingMessageResubmitData[] = [];
468
541
 
469
- this.stateHandler.reSubmitBatch(batch);
470
- } else {
471
- this.stateHandler.reSubmit({
542
+ // check is >= because batch end may be last pending message
543
+ while (remainingPendingMessagesCount >= 0) {
544
+ batch.push({
472
545
  content: pendingMessage.content,
473
546
  localOpMetadata: pendingMessage.localOpMetadata,
474
547
  opMetadata: pendingMessage.opMetadata,
475
548
  });
549
+
550
+ if (pendingMessage.opMetadata?.batch === false) {
551
+ break;
552
+ }
553
+ assert(remainingPendingMessagesCount > 0, 0x555 /* No batch end found */);
554
+
555
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
556
+ pendingMessage = this.pendingMessages.shift()!;
557
+ remainingPendingMessagesCount--;
558
+ assert(
559
+ pendingMessage.opMetadata?.batch !== true,
560
+ 0x556 /* Batch start needs a corresponding batch end */,
561
+ );
476
562
  }
563
+
564
+ this.stateHandler.reSubmitBatch(batch, batchId);
477
565
  }
478
566
 
479
567
  // pending ops should no longer depend on previous sequenced local ops after resubmit
@@ -491,3 +579,14 @@ export class PendingStateManager implements IDisposable {
491
579
  }
492
580
  }
493
581
  }
582
+
583
+ /** For back-compat if trying to apply stashed ops that pre-date batchIdContext */
584
+ function patchBatchIdContext(
585
+ message: IPendingMessageFromStash,
586
+ ): asserts message is IPendingMessage {
587
+ const batchIdContext: IPendingMessageFromStash["batchIdContext"] = message.batchIdContext;
588
+ if (batchIdContext === undefined) {
589
+ // Using uuid guarantees uniqueness, retaining existing behavior
590
+ message.batchIdContext = { clientId: uuid(), batchStartCsn: -1 };
591
+ }
592
+ }
@@ -124,7 +124,9 @@ class ScheduleManagerCore {
124
124
  }
125
125
 
126
126
  // First message will have the batch flag set to true if doing a batched send
127
- const firstMessageMetadata = messages[0].metadata as IRuntimeMessageMetadata;
127
+ // Non null asserting because of the length check above
128
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129
+ const firstMessageMetadata = messages[0]!.metadata as IRuntimeMessageMetadata;
128
130
  if (!firstMessageMetadata?.batch) {
129
131
  return;
130
132
  }
@@ -136,7 +138,9 @@ class ScheduleManagerCore {
136
138
  }
137
139
 
138
140
  // Set the batch flag to false on the last message to indicate the end of the send batch
139
- const lastMessage = messages[messages.length - 1];
141
+ // Non null asserting here because of the length check at the start of the function
142
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
143
+ const lastMessage = messages[messages.length - 1]!;
140
144
  // TODO: It's not clear if this shallow clone is required, as opposed to just setting "batch" to false.
141
145
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
142
146
  lastMessage.metadata = { ...(lastMessage.metadata as any), batch: false };
@@ -0,0 +1,81 @@
1
+ ## Table of contents
2
+
3
+ - [Introduction](#introduction)
4
+ - [Summary vs snapshot](#summary-vs-snapshot)
5
+ - [Why do we need summaries?](#why-do-we-need-summaries)
6
+ - [Who generates summaries?](#who-generates-summaries)
7
+ - [When are summaries generated?](#when-are-summaries-generated)
8
+ - [How are summaries generated?](#how-are-summaries-generated)
9
+ - [Summary Lifecycle](#summary-lifecycle)
10
+ - [Single-commit vs two-commit summaries](#single-commit-vs-two-commit-summaries)
11
+ - [Incremental summaries](#incremental-summaries)
12
+ - [Resiliency](#resiliency)
13
+ - [What does a summary look like?](#what-does-a-summary-look-like)
14
+
15
+ ## Introduction
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.
18
+
19
+ ### Summary vs snapshot
20
+
21
+ The terms summary and snapshot are sometimes used interchangeably. Both represent the state of a container at a point in time. They differ in some respects which are described in [this FAQ](https://fluidframework.com/docs/faq/#summarization).
22
+
23
+ ## Why do we need summaries?
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.
27
+
28
+ ## Who generates summaries?
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:
31
+
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.
36
+
37
+ ## When are summaries generated?
38
+
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).
40
+
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.
42
+
43
+ ## How are summaries generated?
44
+
45
+ When summarization process is triggered, every object in the container's object tree that has data to be summarized is asked to generate its summary, starting at the container runtime which is at the root. There are various objects that participate in the summary process and generate its summary such as data stores, DDSes, garbage collector, blob manager, id compressor, etc. Note that the user data is in the DDSes.
46
+
47
+ ### Summary Lifecycle
48
+
49
+ The lifecycle of a summary starts when a "parent summarizer" client is elected.
50
+ - The parent summarizer spawns a non-interactive summarizer client.
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.
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
66
+
67
+ ### Incremental summaries
68
+
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.
70
+
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.
72
+
73
+ ### Resiliency
74
+
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:
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
+ - 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
+
79
+ ## What does a summary look like?
80
+
81
+ The format of a summary is described in [summary formats](./summaryFormats.md).
@@ -73,7 +73,9 @@ export class EscapedPath {
73
73
  public static createAndConcat(pathParts: string[]): EscapedPath {
74
74
  let ret = EscapedPath.create(pathParts[0] ?? "");
75
75
  for (let i = 1; i < pathParts.length; i++) {
76
- ret = ret.concat(EscapedPath.create(pathParts[i]));
76
+ // Non null asserting here since we are iterating over pathParts
77
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
78
+ ret = ret.concat(EscapedPath.create(pathParts[i]!));
77
79
  }
78
80
  return ret;
79
81
  }
@@ -281,7 +281,9 @@ export async function getFluidDataStoreAttributes(
281
281
  ): Promise<ReadFluidDataStoreAttributes> {
282
282
  const attributes = await readAndParse<ReadFluidDataStoreAttributes>(
283
283
  storage,
284
- snapshot.blobs[dataStoreAttributesBlobName],
284
+ // TODO why are we non null asserting here?
285
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
286
+ snapshot.blobs[dataStoreAttributesBlobName]!,
285
287
  );
286
288
  // Use the snapshotFormatVersion to determine how the pkg is encoded in the snapshot.
287
289
  // For snapshotFormatVersion = "0.1" (1) or above, pkg is jsonified, otherwise it is just a string.
@@ -104,13 +104,26 @@ Each node in a snapshot tree is represented by the above interface and contains
104
104
  This section shows what a typical summary or snapshot tree in a container looks like. Some key things to note:
105
105
 
106
106
  - The diagrams in this section show some examples of existing blobs / trees that are added at each node and doesn't show an exhaustive list.
107
- - The blue boxes represent tree nodes.
108
- - The green boxes represent blobs.
109
- - The purple boxes represent attachments.
110
- - The orange boxes represent other nodes - either existing nodes that are not shown or new nodes that may be added in the future. A node can be a tree, blob or attachment.
107
+ - The blue boxes represent summary tree nodes.
108
+ - The green boxes represent summary blobs.
109
+ - The purple boxes represent summary attachments.
110
+ - The pink boxes represent other nodes - either existing nodes that are not shown or new nodes that may be added in the future. A node can be a tree, blob or attachment.
111
111
 
112
112
  A typical tree uploaded to or downloaded from storage looks like the following:
113
- ![ProtocolAndAppTree](./images/protocolAndAppTree.png)
113
+
114
+ ```mermaid
115
+ flowchart TD
116
+ classDef tree fill:#4672c4,color:#fff
117
+ classDef blob fill:#538135,color:#fff
118
+ classDef others fill:#d636bb,stroke:#4672c4,stroke-width:1px,color:#fff,stroke-dasharray: 5 5
119
+ A["/"]:::tree --> B[".protocol"]:::tree
120
+ B --> C[attributes]:::blob
121
+ B --> D["quorum members"]:::blob
122
+ B --> E["quorum proposals"]:::blob
123
+ B --> F["quorum values"]:::blob
124
+ B --> G["other nodes"]:::others
125
+ A --> H[".app (described below)"]:::tree
126
+ ```
114
127
 
115
128
  `Protocol tree` - This is the tree named `.protocol` and contains protocol level information for the container. These are used by the container to initialize.
116
129
 
@@ -129,8 +142,38 @@ The contents of the protocol tree are:
129
142
  ### App tree
130
143
 
131
144
  This is what the ".app" tree looks like which is generated by the container runtime during summary upload. The same is passed to container runtime during snapshot download:
132
- ![appTree](./images/appTree.png)
133
145
 
146
+ ```mermaid
147
+ flowchart TD
148
+ classDef tree fill:#4672c4,color:#fff
149
+ classDef blob fill:#538135,color:#fff
150
+ classDef attachment fill:#904fc2,color:#fff
151
+ classDef others fill:#d636bb,stroke:#4672c4,stroke-width:1px,color:#fff,stroke-dasharray: 5 5
152
+ classDef hidden display:none;
153
+ A[".app"]:::tree --> B[.metadata]:::blob
154
+ A --> C[.aliases]:::blob
155
+ A --> D[.idCompressor]:::blob
156
+ A --> E[.channels]:::tree
157
+ E --> F["data store 1"]:::tree
158
+ E --> G["data store 2"]:::tree
159
+ E --> H["data store N"]:::tree
160
+ G --> I[.components]:::blob
161
+ G --> J[.channels]:::tree
162
+ J --> K[.channels]:::tree
163
+ J --> L[DDS2]:::tree
164
+ L --> M[.attributes]:::blob
165
+ L --> N["other nodes"]:::others
166
+ N --> END:::hidden
167
+ L --> O[.header]:::blob
168
+ J --> P[.channels]:::tree
169
+ G --> Q["other nodes"]:::others
170
+ A --> R[gc]:::tree
171
+ A --> S["other nodes"]:::others
172
+ A --> T[.blobs]:::tree
173
+ T --> U["attachment blob 1"]:::attachment
174
+ T --> V["attachment blob N"]:::attachment
175
+
176
+ ```
134
177
  - `Container`: The root represents the container or container runtime node. Its contents are described below:
135
178
 
136
179
  - `.metadata blob` - The container level metadata such as creation time, create version, etc.
@@ -152,9 +195,27 @@ This is what the ".app" tree looks like which is generated by the container runt
152
195
  - `.header blob` - Added by some DDSs and may contains its data. Note that all DDSs may not add this.
153
196
  - A DDS may add other blobs and / or trees to represent its data. Basically, a DDS can write its data in any form
154
197
 
155
- ### Summary tree distinction
198
+ ### Summary tree distinction - Incremental summaries
156
199
 
157
200
  In the visualization above, a summary tree differs from a snapshot tree in the following way:
158
201
  A summary tree supports incremental summaries via summary handles. Any node in the tree that has not changed since the previous successful summary can send a summary handle (`ISummaryHandle`) instead of sending its entire contents in a full summary. The following diagram shows this with an example where certain parts of the summary tree use a summary handle. It is a zoomed in version of the same app tree as above where nodes where summary handles are marked in red:
159
202
 
160
- ![summaryTree](./images/summaryTree.png)
203
+ ```mermaid
204
+ flowchart TD
205
+ classDef tree fill:#4672c4,color:#fff
206
+ classDef blob fill:#538135,color:#fff
207
+ classDef others fill:#d636bb,stroke:#4672c4,stroke-width:1px,color:#fff,stroke-dasharray: 5 5
208
+ classDef handle fill:#cc4343,color:#fff
209
+ A[".app"]:::tree --> B["other nodes"]:::others
210
+ A --> C[.channels]:::tree
211
+ C --> D["handle: '/data store 1'"]:::handle
212
+ C --> E["data store 2"]:::tree
213
+ E --> F[".channels"]:::tree
214
+ F --> G["handle: '/data store 2/DDS 1'"]:::handle
215
+ F --> H["DDS 2"]:::tree
216
+ H --> I["handle: '/data store 2/DDS 2/sub node'"]:::handle
217
+ F --> J["DDS N"]:::tree
218
+ E --> K["other nodes"]:::others
219
+ C --> L["data store N"]:::tree
220
+ A --> M["handle: '/gc'"]:::handle
221
+ ```
package/tsconfig.json CHANGED
@@ -5,7 +5,6 @@
5
5
  "compilerOptions": {
6
6
  "rootDir": "./src",
7
7
  "outDir": "./lib",
8
- "noUncheckedIndexedAccess": false,
9
8
  "exactOptionalPropertyTypes": false,
10
9
  },
11
10
  }
Binary file
Binary file