@fluidframework/container-runtime 2.0.0-internal.2.2.1 → 2.0.0-internal.2.3.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 (195) hide show
  1. package/.eslintrc.js +19 -8
  2. package/dist/batchTracker.d.ts +1 -2
  3. package/dist/batchTracker.d.ts.map +1 -1
  4. package/dist/batchTracker.js.map +1 -1
  5. package/dist/blobManager.d.ts +45 -34
  6. package/dist/blobManager.d.ts.map +1 -1
  7. package/dist/blobManager.js +135 -102
  8. package/dist/blobManager.js.map +1 -1
  9. package/dist/containerRuntime.d.ts +54 -8
  10. package/dist/containerRuntime.d.ts.map +1 -1
  11. package/dist/containerRuntime.js +143 -72
  12. package/dist/containerRuntime.js.map +1 -1
  13. package/dist/dataStoreContext.d.ts +1 -1
  14. package/dist/dataStoreContext.d.ts.map +1 -1
  15. package/dist/dataStoreContext.js +6 -8
  16. package/dist/dataStoreContext.js.map +1 -1
  17. package/dist/dataStores.d.ts +12 -9
  18. package/dist/dataStores.d.ts.map +1 -1
  19. package/dist/dataStores.js +41 -35
  20. package/dist/dataStores.js.map +1 -1
  21. package/dist/garbageCollection.d.ts +41 -20
  22. package/dist/garbageCollection.d.ts.map +1 -1
  23. package/dist/garbageCollection.js +205 -150
  24. package/dist/garbageCollection.js.map +1 -1
  25. package/dist/garbageCollectionConstants.d.ts +7 -3
  26. package/dist/garbageCollectionConstants.d.ts.map +1 -1
  27. package/dist/garbageCollectionConstants.js +10 -8
  28. package/dist/garbageCollectionConstants.js.map +1 -1
  29. package/dist/garbageCollectionTombstoneUtils.d.ts +14 -0
  30. package/dist/garbageCollectionTombstoneUtils.d.ts.map +1 -0
  31. package/dist/garbageCollectionTombstoneUtils.js +23 -0
  32. package/dist/garbageCollectionTombstoneUtils.js.map +1 -0
  33. package/dist/index.d.ts +1 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +3 -5
  36. package/dist/index.js.map +1 -1
  37. package/dist/opLifecycle/batchManager.d.ts +13 -1
  38. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  39. package/dist/opLifecycle/batchManager.js +35 -1
  40. package/dist/opLifecycle/batchManager.js.map +1 -1
  41. package/dist/opLifecycle/definitions.d.ts +25 -1
  42. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  43. package/dist/opLifecycle/definitions.js.map +1 -1
  44. package/dist/opLifecycle/index.d.ts +2 -2
  45. package/dist/opLifecycle/index.d.ts.map +1 -1
  46. package/dist/opLifecycle/index.js +2 -1
  47. package/dist/opLifecycle/index.js.map +1 -1
  48. package/dist/opLifecycle/opCompressor.d.ts +1 -1
  49. package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
  50. package/dist/opLifecycle/opCompressor.js +24 -10
  51. package/dist/opLifecycle/opCompressor.js.map +1 -1
  52. package/dist/opLifecycle/opDecompressor.d.ts +2 -1
  53. package/dist/opLifecycle/opDecompressor.d.ts.map +1 -1
  54. package/dist/opLifecycle/opDecompressor.js +30 -17
  55. package/dist/opLifecycle/opDecompressor.js.map +1 -1
  56. package/dist/opLifecycle/opSplitter.d.ts +34 -2
  57. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  58. package/dist/opLifecycle/opSplitter.js +114 -5
  59. package/dist/opLifecycle/opSplitter.js.map +1 -1
  60. package/dist/opLifecycle/outbox.d.ts +5 -0
  61. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  62. package/dist/opLifecycle/outbox.js +24 -14
  63. package/dist/opLifecycle/outbox.js.map +1 -1
  64. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  65. package/dist/opLifecycle/remoteMessageProcessor.js +17 -2
  66. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  67. package/dist/packageVersion.d.ts +1 -1
  68. package/dist/packageVersion.js +1 -1
  69. package/dist/packageVersion.js.map +1 -1
  70. package/dist/runningSummarizer.d.ts.map +1 -1
  71. package/dist/runningSummarizer.js +0 -1
  72. package/dist/runningSummarizer.js.map +1 -1
  73. package/dist/scheduleManager.d.ts +0 -1
  74. package/dist/scheduleManager.d.ts.map +1 -1
  75. package/dist/scheduleManager.js +9 -20
  76. package/dist/scheduleManager.js.map +1 -1
  77. package/dist/summarizer.d.ts +0 -1
  78. package/dist/summarizer.d.ts.map +1 -1
  79. package/dist/summarizer.js +2 -1
  80. package/dist/summarizer.js.map +1 -1
  81. package/dist/summarizerTypes.d.ts +1 -0
  82. package/dist/summarizerTypes.d.ts.map +1 -1
  83. package/dist/summarizerTypes.js.map +1 -1
  84. package/dist/summaryFormat.d.ts.map +1 -1
  85. package/dist/summaryFormat.js +1 -2
  86. package/dist/summaryFormat.js.map +1 -1
  87. package/lib/batchTracker.d.ts +1 -2
  88. package/lib/batchTracker.d.ts.map +1 -1
  89. package/lib/batchTracker.js.map +1 -1
  90. package/lib/blobManager.d.ts +45 -34
  91. package/lib/blobManager.d.ts.map +1 -1
  92. package/lib/blobManager.js +137 -104
  93. package/lib/blobManager.js.map +1 -1
  94. package/lib/containerRuntime.d.ts +54 -8
  95. package/lib/containerRuntime.d.ts.map +1 -1
  96. package/lib/containerRuntime.js +140 -69
  97. package/lib/containerRuntime.js.map +1 -1
  98. package/lib/dataStoreContext.d.ts +1 -1
  99. package/lib/dataStoreContext.d.ts.map +1 -1
  100. package/lib/dataStoreContext.js +7 -9
  101. package/lib/dataStoreContext.js.map +1 -1
  102. package/lib/dataStores.d.ts +12 -9
  103. package/lib/dataStores.d.ts.map +1 -1
  104. package/lib/dataStores.js +44 -38
  105. package/lib/dataStores.js.map +1 -1
  106. package/lib/garbageCollection.d.ts +41 -20
  107. package/lib/garbageCollection.d.ts.map +1 -1
  108. package/lib/garbageCollection.js +201 -146
  109. package/lib/garbageCollection.js.map +1 -1
  110. package/lib/garbageCollectionConstants.d.ts +7 -3
  111. package/lib/garbageCollectionConstants.d.ts.map +1 -1
  112. package/lib/garbageCollectionConstants.js +9 -7
  113. package/lib/garbageCollectionConstants.js.map +1 -1
  114. package/lib/garbageCollectionTombstoneUtils.d.ts +14 -0
  115. package/lib/garbageCollectionTombstoneUtils.d.ts.map +1 -0
  116. package/lib/garbageCollectionTombstoneUtils.js +19 -0
  117. package/lib/garbageCollectionTombstoneUtils.js.map +1 -0
  118. package/lib/index.d.ts +1 -2
  119. package/lib/index.d.ts.map +1 -1
  120. package/lib/index.js +1 -2
  121. package/lib/index.js.map +1 -1
  122. package/lib/opLifecycle/batchManager.d.ts +13 -1
  123. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  124. package/lib/opLifecycle/batchManager.js +35 -1
  125. package/lib/opLifecycle/batchManager.js.map +1 -1
  126. package/lib/opLifecycle/definitions.d.ts +25 -1
  127. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  128. package/lib/opLifecycle/definitions.js.map +1 -1
  129. package/lib/opLifecycle/index.d.ts +2 -2
  130. package/lib/opLifecycle/index.d.ts.map +1 -1
  131. package/lib/opLifecycle/index.js +1 -1
  132. package/lib/opLifecycle/index.js.map +1 -1
  133. package/lib/opLifecycle/opCompressor.d.ts +1 -1
  134. package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
  135. package/lib/opLifecycle/opCompressor.js +24 -10
  136. package/lib/opLifecycle/opCompressor.js.map +1 -1
  137. package/lib/opLifecycle/opDecompressor.d.ts +2 -1
  138. package/lib/opLifecycle/opDecompressor.d.ts.map +1 -1
  139. package/lib/opLifecycle/opDecompressor.js +30 -17
  140. package/lib/opLifecycle/opDecompressor.js.map +1 -1
  141. package/lib/opLifecycle/opSplitter.d.ts +34 -2
  142. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  143. package/lib/opLifecycle/opSplitter.js +112 -4
  144. package/lib/opLifecycle/opSplitter.js.map +1 -1
  145. package/lib/opLifecycle/outbox.d.ts +5 -0
  146. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  147. package/lib/opLifecycle/outbox.js +24 -14
  148. package/lib/opLifecycle/outbox.js.map +1 -1
  149. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  150. package/lib/opLifecycle/remoteMessageProcessor.js +17 -2
  151. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  152. package/lib/packageVersion.d.ts +1 -1
  153. package/lib/packageVersion.js +1 -1
  154. package/lib/packageVersion.js.map +1 -1
  155. package/lib/runningSummarizer.d.ts.map +1 -1
  156. package/lib/runningSummarizer.js +0 -1
  157. package/lib/runningSummarizer.js.map +1 -1
  158. package/lib/scheduleManager.d.ts +0 -1
  159. package/lib/scheduleManager.d.ts.map +1 -1
  160. package/lib/scheduleManager.js +9 -20
  161. package/lib/scheduleManager.js.map +1 -1
  162. package/lib/summarizer.d.ts +0 -1
  163. package/lib/summarizer.d.ts.map +1 -1
  164. package/lib/summarizer.js +2 -1
  165. package/lib/summarizer.js.map +1 -1
  166. package/lib/summarizerTypes.d.ts +1 -0
  167. package/lib/summarizerTypes.d.ts.map +1 -1
  168. package/lib/summarizerTypes.js.map +1 -1
  169. package/lib/summaryFormat.d.ts.map +1 -1
  170. package/lib/summaryFormat.js +1 -2
  171. package/lib/summaryFormat.js.map +1 -1
  172. package/package.json +20 -19
  173. package/src/batchTracker.ts +1 -1
  174. package/src/blobManager.ts +159 -111
  175. package/src/containerRuntime.ts +202 -73
  176. package/src/dataStoreContext.ts +15 -16
  177. package/src/dataStores.ts +61 -45
  178. package/src/garbageCollection.ts +258 -183
  179. package/src/garbageCollectionConstants.ts +10 -7
  180. package/src/garbageCollectionTombstoneUtils.ts +28 -0
  181. package/src/index.ts +2 -5
  182. package/src/opLifecycle/batchManager.ts +59 -1
  183. package/src/opLifecycle/definitions.ts +27 -1
  184. package/src/opLifecycle/index.ts +2 -1
  185. package/src/opLifecycle/opCompressor.ts +29 -12
  186. package/src/opLifecycle/opDecompressor.ts +39 -18
  187. package/src/opLifecycle/opSplitter.ts +141 -7
  188. package/src/opLifecycle/outbox.ts +32 -16
  189. package/src/opLifecycle/remoteMessageProcessor.ts +19 -3
  190. package/src/packageVersion.ts +1 -1
  191. package/src/runningSummarizer.ts +0 -1
  192. package/src/scheduleManager.ts +19 -30
  193. package/src/summarizer.ts +1 -1
  194. package/src/summarizerTypes.ts +1 -0
  195. package/src/summaryFormat.ts +1 -2
@@ -3,13 +3,12 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import { GCVersion } from "./summaryFormat";
6
7
 
7
- // The key for the GC tree in summary.
8
- export const gcTreeKey = "gc";
9
- // They prefix for GC blobs in the GC tree in summary.
10
- export const gcBlobPrefix = "__gc";
11
- // The key for tombstone blob in the GC tree in summary.
12
- export const gcTombstoneBlobKey = "__tombstones";
8
+ /** The stable version of garbage collection in production. */
9
+ export const stableGCVersion: GCVersion = 1;
10
+ /** The current version of garbage collection. */
11
+ export const currentGCVersion: GCVersion = 2;
13
12
 
14
13
  // Feature gate key to turn GC on / off.
15
14
  export const runGCKey = "Fluid.GarbageCollection.RunGC";
@@ -25,8 +24,12 @@ export const trackGCStateKey = "Fluid.GarbageCollection.TrackGCState";
25
24
  export const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
26
25
  // Feature gate key to disable the tombstone feature, i.e., tombstone information is not read / written into summary.
27
26
  export const disableTombstoneKey = "Fluid.GarbageCollection.DisableTombstone";
28
- // Feature gate to enable throwing an error when tombstone object is used.
27
+ // Feature gate to enable throwing an error when tombstone object is loaded (requested).
28
+ export const throwOnTombstoneLoadKey = "Fluid.GarbageCollection.ThrowOnTombstoneLoad";
29
+ // Feature gate to enable throwing an error when tombstone object is used (e.g. outgoing or incoming ops).
29
30
  export const throwOnTombstoneUsageKey = "Fluid.GarbageCollection.ThrowOnTombstoneUsage";
31
+ // Feature gate to enable GC version upgrade.
32
+ export const gcVersionUpgradeToV2Key = "Fluid.GarbageCollection.GCVersionUpgradeToV2";
30
33
 
31
34
  // One day in milliseconds.
32
35
  export const oneDayMs = 1 * 24 * 60 * 60 * 1000;
@@ -0,0 +1,28 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { ITelemetryGenericEvent } from "@fluidframework/common-definitions";
7
+ import { packagePathToTelemetryProperty } from "@fluidframework/runtime-utils";
8
+ import { MonitoringContext } from "@fluidframework/telemetry-utils";
9
+ import { disableTombstoneKey, throwOnTombstoneLoadKey, throwOnTombstoneUsageKey } from "./garbageCollectionConstants";
10
+
11
+ /**
12
+ * Consolidates info / logic for logging when we encounter a Tombstone
13
+ */
14
+ export function sendGCTombstoneEvent(
15
+ mc: MonitoringContext,
16
+ event: ITelemetryGenericEvent & { category: "error" | "generic", isSummarizerClient: boolean },
17
+ packagePath: readonly string[] | undefined,
18
+ error?: unknown,
19
+ ) {
20
+ event.pkg = packagePathToTelemetryProperty(packagePath);
21
+ event.tombstoneFlags = JSON.stringify({
22
+ DisableTombstone: mc.config.getBoolean(disableTombstoneKey),
23
+ ThrowOnTombstoneUsage: mc.config.getBoolean(throwOnTombstoneUsageKey),
24
+ ThrowOnTombstoneLoad: mc.config.getBoolean(throwOnTombstoneLoadKey),
25
+ });
26
+
27
+ mc.logger.sendTelemetryEvent(event, error);
28
+ }
package/src/index.ts CHANGED
@@ -19,6 +19,8 @@ export {
19
19
  agentSchedulerId,
20
20
  ContainerRuntime,
21
21
  RuntimeHeaders,
22
+ AllowTombstoneRequestHeaderKey,
23
+ TombstoneResponseHeaderKey,
22
24
  ISummaryConfiguration,
23
25
  DefaultSummaryConfiguration,
24
26
  ICompressionRuntimeOptions,
@@ -28,11 +30,6 @@ export { FluidDataStoreRegistry } from "./dataStoreRegistry";
28
30
  export {
29
31
  IGCStats,
30
32
  } from "./garbageCollection";
31
- export {
32
- gcBlobPrefix,
33
- gcTombstoneBlobKey,
34
- gcTreeKey,
35
- } from "./garbageCollectionConstants";
36
33
  export {
37
34
  IPendingFlush,
38
35
  IPendingLocalState,
@@ -3,10 +3,14 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
+ import { UsageError } from "@fluidframework/driver-utils";
8
+ import { ChildLogger } from "@fluidframework/telemetry-utils";
6
9
  import { ICompressionRuntimeOptions } from "../containerRuntime";
7
10
  import { BatchMessage, IBatch, IBatchCheckpoint } from "./definitions";
8
11
 
9
12
  export interface IBatchManagerOptions {
13
+ readonly enableOpReentryCheck?: boolean;
10
14
  readonly hardLimit: number;
11
15
  readonly softLimit?: number;
12
16
  readonly compressionOptions?: ICompressionRuntimeOptions;
@@ -16,15 +20,32 @@ export interface IBatchManagerOptions {
16
20
  * Helper class that manages partial batch & rollback.
17
21
  */
18
22
  export class BatchManager {
23
+ private readonly logger;
19
24
  private pendingBatch: BatchMessage[] = [];
20
25
  private batchContentSize = 0;
26
+ /**
27
+ * Track the number of ops which were detected to have a mismatched
28
+ * reference sequence number, in order to self-throttle the telemetry events.
29
+ *
30
+ * This should be removed as part of ADO:2322
31
+ */
32
+ private readonly maxMismatchedOpsToReport = 5;
33
+ private mismatchedOpsReported = 0;
34
+
21
35
 
22
36
  public get length() { return this.pendingBatch.length; }
23
37
  public get contentSizeInBytes() { return this.batchContentSize; }
24
38
 
25
- constructor(public readonly options: IBatchManagerOptions) { }
39
+ constructor(
40
+ public readonly options: IBatchManagerOptions,
41
+ logger: ITelemetryLogger,
42
+ ) {
43
+ this.logger = ChildLogger.create(logger, "BatchManager");
44
+ }
26
45
 
27
46
  public push(message: BatchMessage): boolean {
47
+ this.checkReferenceSequenceNumber(message);
48
+
28
49
  const contentSize = this.batchContentSize + (message.contents?.length ?? 0);
29
50
  const opCount = this.pendingBatch.length;
30
51
 
@@ -87,6 +108,43 @@ export class BatchManager {
87
108
  },
88
109
  };
89
110
  }
111
+
112
+ private checkReferenceSequenceNumber(message: BatchMessage) {
113
+ if (this.pendingBatch.length === 0 || message.referenceSequenceNumber === this.pendingBatch[0].referenceSequenceNumber) {
114
+ // The reference sequence numbers are stable
115
+ return;
116
+ }
117
+
118
+ const telemetryProperties = {
119
+ referenceSequenceNumber: this.pendingBatch[0].referenceSequenceNumber,
120
+ messageReferenceSequenceNumber: message.referenceSequenceNumber,
121
+ type: message.deserializedContent.type,
122
+ length: this.pendingBatch.length,
123
+ enableOpReentryCheck: this.options.enableOpReentryCheck === true,
124
+ };
125
+ const error = new UsageError("Submission of an out of order message");
126
+ const eventName = "ReferenceSequenceNumberMismatch";
127
+
128
+ if (this.options.enableOpReentryCheck === true) {
129
+ this.logger.sendErrorEvent(
130
+ { eventName, ...telemetryProperties },
131
+ error,
132
+ );
133
+ throw error;
134
+ }
135
+
136
+ if (++this.mismatchedOpsReported <= this.maxMismatchedOpsToReport) {
137
+ this.logger.sendErrorEvent(
138
+ {
139
+ eventName,
140
+ ...telemetryProperties,
141
+ ops: this.mismatchedOpsReported,
142
+ maxOps: this.maxMismatchedOpsToReport,
143
+ },
144
+ error,
145
+ );
146
+ }
147
+ }
90
148
  }
91
149
 
92
150
  const addBatchMetadata = (batch: IBatch): IBatch => {
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IBatchMessage } from "@fluidframework/container-definitions";
7
- import { MessageType } from "@fluidframework/protocol-definitions";
7
+ import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
8
8
  import { CompressionAlgorithms, ContainerMessageType, ContainerRuntimeMessage } from "..";
9
9
 
10
10
  /**
@@ -41,4 +41,30 @@ export interface IChunkedOp {
41
41
  totalChunks: number;
42
42
  contents: string;
43
43
  originalType: MessageType | ContainerMessageType;
44
+ originalMetadata?: Record<string, unknown>;
45
+ originalCompression?: string;
46
+ }
47
+
48
+ /**
49
+ * The state of remote message processing:
50
+ * `Processed` - the message can be considered processed
51
+ * `Skipped` - the message was ignored by the processor
52
+ * `Accepted` - the message was processed partially. Eventually, a message
53
+ * will make the processor return `Processed`.
54
+ */
55
+ export type ProcessingState = "Processed" | "Skipped" | "Accepted";
56
+
57
+ /**
58
+ * Return type for functions which process remote messages
59
+ */
60
+ export interface IMessageProcessingResult {
61
+ /**
62
+ * A shallow copy of the input message if processing happened, or
63
+ * the original message otherwise
64
+ */
65
+ readonly message: ISequencedDocumentMessage;
66
+ /**
67
+ * Processing result of the input message.
68
+ */
69
+ readonly state: ProcessingState;
44
70
  }
@@ -9,9 +9,10 @@ export {
9
9
  IBatch,
10
10
  IBatchCheckpoint,
11
11
  IChunkedOp,
12
+ IMessageProcessingResult,
12
13
  } from "./definitions";
13
14
  export { Outbox } from "./outbox";
14
15
  export { OpCompressor } from "./opCompressor";
15
16
  export { OpDecompressor } from "./opDecompressor";
16
- export { OpSplitter } from "./opSplitter";
17
+ export { OpSplitter, splitOp } from "./opSplitter";
17
18
  export { RemoteMessageProcessor, unpackRuntimeMessage } from "./remoteMessageProcessor";
@@ -5,9 +5,10 @@
5
5
 
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
7
  import { IsoBuffer } from "@fluidframework/common-utils";
8
+ import { UsageError } from "@fluidframework/container-utils";
8
9
  import { ChildLogger } from "@fluidframework/telemetry-utils";
9
10
  import { compress } from "lz4js";
10
- import { CompressionAlgorithms, ContainerRuntimeMessage } from "../containerRuntime";
11
+ import { CompressionAlgorithms } from "../containerRuntime";
11
12
  import { IBatch, BatchMessage } from "./definitions";
12
13
 
13
14
  /**
@@ -17,27 +18,19 @@ import { IBatch, BatchMessage } from "./definitions";
17
18
  */
18
19
  export class OpCompressor {
19
20
  private readonly logger;
20
- private compressedBatchCount = 0;
21
21
 
22
22
  constructor(logger: ITelemetryLogger) {
23
23
  this.logger = ChildLogger.create(logger, "OpCompressor");
24
24
  }
25
25
 
26
26
  public compressBatch(batch: IBatch): IBatch {
27
- const messages: BatchMessage[] = [];
28
- this.compressedBatchCount++;
29
- const contentToCompress: ContainerRuntimeMessage[] = [];
30
- for (const message of batch.content) {
31
- contentToCompress.push(message.deserializedContent);
32
- }
33
-
34
27
  const compressionStart = Date.now();
35
- const contentsAsBuffer = new TextEncoder().encode(JSON.stringify(contentToCompress));
28
+ const contentsAsBuffer = new TextEncoder().encode(this.serializeBatch(batch));
36
29
  const compressedContents = compress(contentsAsBuffer);
37
30
  const compressedContent = IsoBuffer.from(compressedContents).toString("base64");
38
31
  const duration = Date.now() - compressionStart;
39
32
 
40
- if (batch.contentSizeInBytes > 200000 || this.compressedBatchCount % 25) {
33
+ if (batch.contentSizeInBytes > 200000) {
41
34
  this.logger.sendPerformanceEvent({
42
35
  eventName: "CompressedBatch",
43
36
  duration,
@@ -46,9 +39,10 @@ export class OpCompressor {
46
39
  });
47
40
  }
48
41
 
42
+ const messages: BatchMessage[] = [];
49
43
  messages.push({
50
44
  ...batch.content[0], contents: JSON.stringify({ packedContents: compressedContent }),
51
- metadata: { ...batch.content[0].metadata, compressed: true },
45
+ metadata: batch.content[0].metadata,
52
46
  compression: CompressionAlgorithms.lz4,
53
47
  });
54
48
 
@@ -61,4 +55,27 @@ export class OpCompressor {
61
55
  content: messages,
62
56
  };
63
57
  }
58
+
59
+ private serializeBatch(batch: IBatch): string {
60
+ try {
61
+ return JSON.stringify(batch.content.map((message) => message.deserializedContent))
62
+ } catch (e: any) {
63
+ if (e.message === "Invalid string length") {
64
+ // This is how JSON.stringify signals that
65
+ // the content size exceeds its capacity
66
+ const error = new UsageError("Payload too large");
67
+ this.logger.sendErrorEvent(
68
+ {
69
+ eventName: "BatchTooLarge",
70
+ size: batch.contentSizeInBytes,
71
+ length: batch.content.length,
72
+ },
73
+ error,
74
+ );
75
+ throw error;
76
+ }
77
+
78
+ throw e;
79
+ }
80
+ }
64
81
  }
@@ -7,6 +7,7 @@ import { decompress } from "lz4js";
7
7
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
8
8
  import { assert, IsoBuffer, Uint8ArrayToString } from "@fluidframework/common-utils";
9
9
  import { CompressionAlgorithms } from "../containerRuntime";
10
+ import { IMessageProcessingResult } from "./definitions";
10
11
 
11
12
  /**
12
13
  * State machine that "unrolls" contents of compressed batches of ops after decompressing them.
@@ -21,13 +22,12 @@ export class OpDecompressor {
21
22
  private rootMessageContents: any | undefined;
22
23
  private processedCount = 0;
23
24
 
24
- public processMessage(message: ISequencedDocumentMessage): ISequencedDocumentMessage {
25
- // We're checking for compression = true or top level compression property so
26
- // that we can enable compression without waiting on all ordering services
27
- // to pick up protocol change. Eventually only the top level property should
28
- // be used.
29
- if (message.metadata?.batch === true
30
- && (message.metadata?.compressed || message.compression !== undefined)) {
25
+ public processMessage(message: ISequencedDocumentMessage): IMessageProcessingResult {
26
+ assert(
27
+ message.compression === undefined || message.compression === CompressionAlgorithms.lz4,
28
+ 0x511 /* Only lz4 compression is supported */);
29
+
30
+ if (message.metadata?.batch === true && message.compression === CompressionAlgorithms.lz4) {
31
31
  // Beginning of a compressed batch
32
32
  assert(this.activeBatch === false, 0x4b8 /* shouldn't have multiple active batches */);
33
33
  if (message.compression) {
@@ -44,30 +44,37 @@ export class OpDecompressor {
44
44
  const asObj = JSON.parse(intoString);
45
45
  this.rootMessageContents = asObj;
46
46
 
47
- return { ...message, contents: this.rootMessageContents[this.processedCount++] };
47
+ return {
48
+ message: newMessage(message, this.rootMessageContents[this.processedCount++]),
49
+ state: "Accepted",
50
+ };
48
51
  }
49
52
 
50
53
  if (this.rootMessageContents !== undefined && message.metadata?.batch === undefined && this.activeBatch) {
54
+ assert(message.contents === undefined, 0x512 /* Expecting empty message */);
55
+
51
56
  // Continuation of compressed batch
52
- return { ...message, contents: this.rootMessageContents[this.processedCount++] };
57
+ return {
58
+ message: newMessage(message, this.rootMessageContents[this.processedCount++]),
59
+ state: "Accepted",
60
+ };
53
61
  }
54
62
 
55
63
  if (this.rootMessageContents !== undefined && message.metadata?.batch === false) {
56
64
  // End of compressed batch
57
- const returnMessage = {
58
- ...message,
59
- contents: this.rootMessageContents[this.processedCount++]
60
- };
65
+ const returnMessage = newMessage(message, this.rootMessageContents[this.processedCount++]);
61
66
 
62
67
  this.activeBatch = false;
63
68
  this.rootMessageContents = undefined;
64
69
  this.processedCount = 0;
65
70
 
66
- return returnMessage;
71
+ return {
72
+ message: returnMessage,
73
+ state: "Processed",
74
+ };
67
75
  }
68
76
 
69
- if (message.metadata?.batch === undefined &&
70
- (message.metadata?.compressed || message.compression === CompressionAlgorithms.lz4)) {
77
+ if (message.metadata?.batch === undefined && message.compression === CompressionAlgorithms.lz4) {
71
78
  // Single compressed message
72
79
  assert(this.activeBatch === false, 0x4ba /* shouldn't receive compressed message in middle of a batch */);
73
80
 
@@ -76,9 +83,23 @@ export class OpDecompressor {
76
83
  const intoString = new TextDecoder().decode(decompressedMessage);
77
84
  const asObj = JSON.parse(intoString);
78
85
 
79
- return { ...message, contents: asObj[0] };
86
+ return {
87
+ message: newMessage(message, asObj[0]),
88
+ state: "Processed",
89
+ };
80
90
  }
81
91
 
82
- return message;
92
+ return {
93
+ message,
94
+ state: "Skipped",
95
+ };
83
96
  }
84
97
  }
98
+
99
+ // We should not be mutating the input message nor its metadata
100
+ const newMessage = (originalMessage: ISequencedDocumentMessage, contents: any): ISequencedDocumentMessage => ({
101
+ ...originalMessage,
102
+ contents,
103
+ compression: undefined,
104
+ metadata: { ...originalMessage.metadata },
105
+ });
@@ -3,10 +3,14 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
+ import { assert } from "@fluidframework/common-utils";
8
+ import { IBatchMessage } from "@fluidframework/container-definitions";
6
9
  import { DataCorruptionError, extractSafePropertiesFromMessage } from "@fluidframework/container-utils";
7
10
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
8
- import { ContainerMessageType } from "../containerRuntime";
9
- import { IChunkedOp } from "./definitions";
11
+ import { ChildLogger } from "@fluidframework/telemetry-utils";
12
+ import { ContainerMessageType, ContainerRuntimeMessage } from "../containerRuntime";
13
+ import { BatchMessage, IBatch, IChunkedOp, IMessageProcessingResult } from "./definitions";
10
14
 
11
15
  /**
12
16
  * Responsible for creating and reconstructing chunked messages.
@@ -14,18 +18,33 @@ import { IChunkedOp } from "./definitions";
14
18
  export class OpSplitter {
15
19
  // Local copy of incomplete received chunks.
16
20
  private readonly chunkMap: Map<string, string[]>;
21
+ private readonly logger;
17
22
 
18
- constructor(chunks: [string, string[]][]) {
23
+ constructor(
24
+ chunks: [string, string[]][],
25
+ private readonly submitBatchFn: ((batch: IBatchMessage[]) => number) | undefined,
26
+ private readonly chunkSizeInBytes: number,
27
+ private readonly maxBatchSizeInBytes: number,
28
+ logger: ITelemetryLogger,
29
+ ) {
19
30
  this.chunkMap = new Map<string, string[]>(chunks);
31
+ this.logger = ChildLogger.create(logger, "OpSplitter");
32
+ }
33
+
34
+ public get isBatchChunkingEnabled(): boolean {
35
+ return this.chunkSizeInBytes < Number.POSITIVE_INFINITY && this.submitBatchFn !== undefined;
20
36
  }
21
37
 
22
38
  public get chunks(): ReadonlyMap<string, string[]> {
23
39
  return this.chunkMap;
24
40
  }
25
41
 
26
- public processRemoteMessage(message: ISequencedDocumentMessage): ISequencedDocumentMessage {
42
+ public processRemoteMessage(message: ISequencedDocumentMessage): IMessageProcessingResult {
27
43
  if (message.type !== ContainerMessageType.ChunkedOp) {
28
- return message;
44
+ return {
45
+ message,
46
+ state: "Skipped",
47
+ };
29
48
  }
30
49
 
31
50
  const clientId = message.clientId;
@@ -35,7 +54,10 @@ export class OpSplitter {
35
54
  if (chunkedContent.chunkId < chunkedContent.totalChunks) {
36
55
  // We are processing the op in chunks but haven't reached
37
56
  // the last chunk yet in order to reconstruct the original op
38
- return message;
57
+ return {
58
+ message,
59
+ state: "Accepted",
60
+ };
39
61
  }
40
62
 
41
63
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -45,7 +67,12 @@ export class OpSplitter {
45
67
  const newMessage = { ...message };
46
68
  newMessage.contents = serializedContent === "" ? undefined : JSON.parse(serializedContent);
47
69
  newMessage.type = chunkedContent.originalType;
48
- return newMessage;
70
+ newMessage.metadata = chunkedContent.originalMetadata;
71
+ newMessage.compression = chunkedContent.originalCompression;
72
+ return {
73
+ message: newMessage,
74
+ state: "Processed",
75
+ };
49
76
  }
50
77
 
51
78
  public clearPartialChunks(clientId: string) {
@@ -75,4 +102,111 @@ export class OpSplitter {
75
102
 
76
103
  map.push(chunkedContent.contents);
77
104
  }
105
+
106
+ /**
107
+ * Splits the first op of a compressed batch in chunks, sends the chunks separately and
108
+ * returns a new batch composed of the last chunk and the rest of the ops in the original batch.
109
+ *
110
+ * A compressed batch is formed by one large op at the first position, followed by a series of placeholder ops
111
+ * which are used in order to reserve the sequence numbers for when the first op gets unrolled into the original
112
+ * uncompressed ops at ingestion in the runtime.
113
+ *
114
+ * If the first op is too large, it can be chunked (split into smaller op) which can be sent individually over the wire
115
+ * and accumulate at ingestion, until the last op in the chunk is processed, when the original op is unrolled.
116
+ *
117
+ * This method will send the first N - 1 chunks separately and use the last chunk as the first message in the result batch
118
+ * and then appends the original placeholder ops. This will ensure that the batch semantics of the original (non-compressed) batch
119
+ * are preserved, as the original chunked op will be unrolled by the runtime when the first message in the batch is processed
120
+ * (as it is the last chunk).
121
+ *
122
+ * To illustrate, if the input is `[largeOp, emptyOp, emptyOp]`, `largeOp` will be split into `[chunk1, chunk2, chunk3, chunk4]`.
123
+ * `chunk1`, `chunk2` and `chunk3` will be sent individually and `[chunk4, emptyOp, emptyOp]` will be returned.
124
+ *
125
+ * @param batch - the compressed batch which needs to be processed
126
+ * @returns A new adjusted batch which can be sent over the wire
127
+ */
128
+ public splitCompressedBatch(batch: IBatch): IBatch {
129
+ assert(this.isBatchChunkingEnabled, 0x513 /* Chunking needs to be enabled */);
130
+ assert(batch.contentSizeInBytes > 0 && batch.content.length > 0, 0x514 /* Batch needs to be non-empty */);
131
+ assert(this.chunkSizeInBytes !== 0, 0x515 /* Chunk size needs to be non-zero */);
132
+ assert(this.chunkSizeInBytes < this.maxBatchSizeInBytes, 0x516 /* Chunk size needs to be smaller than the max batch size */);
133
+
134
+ const firstMessage = batch.content[0]; // we expect this to be the large compressed op, which needs to be split
135
+ assert(firstMessage.metadata?.compressed === true || firstMessage.compression !== undefined, 0x517 /* Batch needs to be compressed */);
136
+ assert((firstMessage.contents?.length ?? 0) >= this.chunkSizeInBytes, 0x518 /* First message in the batch needs to be chunkable */);
137
+
138
+ const restOfMessages = batch.content.slice(1); // we expect these to be empty ops, created to reserve sequence numbers
139
+ const chunks = splitOp(firstMessage, this.chunkSizeInBytes);
140
+
141
+ assert(this.submitBatchFn !== undefined, 0x519 /* We don't support old loaders */);
142
+ // Send the first N-1 chunks immediately
143
+ for (const chunk of chunks.slice(0, -1)) {
144
+ this.submitBatchFn([chunkToBatchMessage(chunk, firstMessage.referenceSequenceNumber)]);
145
+ }
146
+
147
+ // The last chunk will be part of the new batch and needs to
148
+ // preserve the batch metadata of the original batch
149
+ const lastChunk = chunkToBatchMessage(
150
+ chunks[chunks.length - 1],
151
+ firstMessage.referenceSequenceNumber,
152
+ { batch: firstMessage.metadata?.batch });
153
+
154
+ this.logger.sendPerformanceEvent({
155
+ eventName: "Chunked compressed batch",
156
+ length: batch.content.length,
157
+ sizeInBytes: batch.contentSizeInBytes,
158
+ chunks: chunks.length,
159
+ chunkSizeInBytes: this.chunkSizeInBytes,
160
+ });
161
+
162
+ return {
163
+ content: [lastChunk, ...restOfMessages],
164
+ contentSizeInBytes: lastChunk.contents?.length ?? 0,
165
+ };
166
+ }
78
167
  }
168
+
169
+ const chunkToBatchMessage = (
170
+ chunk: IChunkedOp,
171
+ referenceSequenceNumber: number,
172
+ metadata: Record<string, unknown> | undefined = undefined,
173
+ ): BatchMessage => {
174
+ const payload: ContainerRuntimeMessage = { type: ContainerMessageType.ChunkedOp, contents: chunk };
175
+ return {
176
+ contents: JSON.stringify(payload),
177
+ deserializedContent: payload,
178
+ metadata,
179
+ localOpMetadata: undefined,
180
+ referenceSequenceNumber,
181
+ };
182
+ }
183
+
184
+ export const splitOp = (op: BatchMessage, chunkSizeInBytes: number): IChunkedOp[] => {
185
+ const chunks: IChunkedOp[] = [];
186
+ assert(op.contents !== undefined && op.contents !== null, 0x51a /* We should have something to chunk */);
187
+
188
+ const contentLength = op.contents.length;
189
+ const chunkN = Math.floor((contentLength - 1) / chunkSizeInBytes) + 1;
190
+ let offset = 0;
191
+ for (let i = 1; i <= chunkN; i++) {
192
+ const chunk: IChunkedOp = {
193
+ chunkId: i,
194
+ contents: op.contents.substr(offset, chunkSizeInBytes),
195
+ originalType: op.deserializedContent.type,
196
+ totalChunks: chunkN,
197
+ }
198
+
199
+ if (i === chunkN) {
200
+ // We don't need to port these to all the chunks,
201
+ // as we rebuild the original op when we process the
202
+ // last chunk, therefore it is the only one that needs it.
203
+ chunk.originalMetadata = op.metadata;
204
+ chunk.originalCompression = op.compression;
205
+ }
206
+
207
+ chunks.push(chunk);
208
+ offset += chunkSizeInBytes;
209
+ }
210
+
211
+ return chunks;
212
+ };