@fluidframework/container-runtime 2.0.0-dev.4.1.0.148229 → 2.0.0-dev.4.2.0.153917
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.
- package/CHANGELOG.md +58 -0
- package/README.md +69 -0
- package/dist/blobManager.d.ts +6 -14
- package/dist/blobManager.d.ts.map +1 -1
- package/dist/blobManager.js +50 -37
- package/dist/blobManager.js.map +1 -1
- package/dist/containerRuntime.d.ts +14 -1
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +37 -12
- package/dist/containerRuntime.js.map +1 -1
- package/dist/gc/gcHelpers.d.ts.map +1 -1
- package/dist/gc/gcHelpers.js +6 -6
- package/dist/gc/gcHelpers.js.map +1 -1
- package/dist/opLifecycle/index.d.ts +1 -0
- package/dist/opLifecycle/index.d.ts.map +1 -1
- package/dist/opLifecycle/index.js +3 -1
- package/dist/opLifecycle/index.js.map +1 -1
- package/dist/opLifecycle/opDecompressor.d.ts.map +1 -1
- package/dist/opLifecycle/opDecompressor.js +2 -1
- package/dist/opLifecycle/opDecompressor.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.d.ts +14 -0
- package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -0
- package/dist/opLifecycle/opGroupingManager.js +56 -0
- package/dist/opLifecycle/opGroupingManager.js.map +1 -0
- package/dist/opLifecycle/opSplitter.d.ts +1 -1
- package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
- package/dist/opLifecycle/opSplitter.js +5 -6
- package/dist/opLifecycle/opSplitter.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts +2 -0
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- package/dist/opLifecycle/outbox.js +3 -3
- package/dist/opLifecycle/outbox.js.map +1 -1
- package/dist/opLifecycle/remoteMessageProcessor.d.ts +4 -2
- package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
- package/dist/opLifecycle/remoteMessageProcessor.js +30 -20
- package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/summary/index.d.ts +1 -1
- package/dist/summary/index.d.ts.map +1 -1
- package/dist/summary/index.js +3 -1
- package/dist/summary/index.js.map +1 -1
- package/dist/summary/runningSummarizer.d.ts +5 -3
- package/dist/summary/runningSummarizer.d.ts.map +1 -1
- package/dist/summary/runningSummarizer.js +82 -67
- package/dist/summary/runningSummarizer.js.map +1 -1
- package/dist/summary/summarizer.d.ts.map +1 -1
- package/dist/summary/summarizer.js +1 -5
- package/dist/summary/summarizer.js.map +1 -1
- package/dist/summary/summarizerHeuristics.d.ts +1 -0
- package/dist/summary/summarizerHeuristics.d.ts.map +1 -1
- package/dist/summary/summarizerHeuristics.js +3 -0
- package/dist/summary/summarizerHeuristics.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNode.js +1 -1
- package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts +128 -2
- package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeWithGc.js +4 -3
- package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
- package/dist/summary/summarizerTypes.d.ts +14 -2
- package/dist/summary/summarizerTypes.d.ts.map +1 -1
- package/dist/summary/summarizerTypes.js.map +1 -1
- package/dist/summary/summaryGenerator.d.ts +28 -2
- package/dist/summary/summaryGenerator.d.ts.map +1 -1
- package/dist/summary/summaryGenerator.js +19 -16
- package/dist/summary/summaryGenerator.js.map +1 -1
- package/lib/blobManager.d.ts +6 -14
- package/lib/blobManager.d.ts.map +1 -1
- package/lib/blobManager.js +50 -37
- package/lib/blobManager.js.map +1 -1
- package/lib/containerRuntime.d.ts +14 -1
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +38 -13
- package/lib/containerRuntime.js.map +1 -1
- package/lib/gc/gcHelpers.d.ts.map +1 -1
- package/lib/gc/gcHelpers.js +6 -6
- package/lib/gc/gcHelpers.js.map +1 -1
- package/lib/opLifecycle/index.d.ts +1 -0
- package/lib/opLifecycle/index.d.ts.map +1 -1
- package/lib/opLifecycle/index.js +1 -0
- package/lib/opLifecycle/index.js.map +1 -1
- package/lib/opLifecycle/opDecompressor.d.ts.map +1 -1
- package/lib/opLifecycle/opDecompressor.js +2 -1
- package/lib/opLifecycle/opDecompressor.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.d.ts +14 -0
- package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -0
- package/lib/opLifecycle/opGroupingManager.js +52 -0
- package/lib/opLifecycle/opGroupingManager.js.map +1 -0
- package/lib/opLifecycle/opSplitter.d.ts +1 -1
- package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
- package/lib/opLifecycle/opSplitter.js +5 -6
- package/lib/opLifecycle/opSplitter.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts +2 -0
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- package/lib/opLifecycle/outbox.js +3 -3
- package/lib/opLifecycle/outbox.js.map +1 -1
- package/lib/opLifecycle/remoteMessageProcessor.d.ts +4 -2
- package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
- package/lib/opLifecycle/remoteMessageProcessor.js +30 -20
- package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/summary/index.d.ts +1 -1
- package/lib/summary/index.d.ts.map +1 -1
- package/lib/summary/index.js +1 -0
- package/lib/summary/index.js.map +1 -1
- package/lib/summary/runningSummarizer.d.ts +5 -3
- package/lib/summary/runningSummarizer.d.ts.map +1 -1
- package/lib/summary/runningSummarizer.js +82 -67
- package/lib/summary/runningSummarizer.js.map +1 -1
- package/lib/summary/summarizer.d.ts.map +1 -1
- package/lib/summary/summarizer.js +1 -5
- package/lib/summary/summarizer.js.map +1 -1
- package/lib/summary/summarizerHeuristics.d.ts +1 -0
- package/lib/summary/summarizerHeuristics.d.ts.map +1 -1
- package/lib/summary/summarizerHeuristics.js +3 -0
- package/lib/summary/summarizerHeuristics.js.map +1 -1
- package/lib/summary/summarizerNode/summarizerNode.js +1 -1
- package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts +128 -2
- package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeWithGc.js +3 -3
- package/lib/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
- package/lib/summary/summarizerTypes.d.ts +14 -2
- package/lib/summary/summarizerTypes.d.ts.map +1 -1
- package/lib/summary/summarizerTypes.js.map +1 -1
- package/lib/summary/summaryGenerator.d.ts +28 -2
- package/lib/summary/summaryGenerator.d.ts.map +1 -1
- package/lib/summary/summaryGenerator.js +17 -15
- package/lib/summary/summaryGenerator.js.map +1 -1
- package/package.json +19 -16
- package/src/blobManager.ts +64 -41
- package/src/containerRuntime.ts +70 -9
- package/src/gc/gcHelpers.ts +9 -6
- package/src/opLifecycle/README.md +106 -0
- package/src/opLifecycle/index.ts +1 -0
- package/src/opLifecycle/opDecompressor.ts +1 -0
- package/src/opLifecycle/opGroupingManager.ts +78 -0
- package/src/opLifecycle/opSplitter.ts +1 -5
- package/src/opLifecycle/outbox.ts +7 -3
- package/src/opLifecycle/remoteMessageProcessor.ts +38 -22
- package/src/packageVersion.ts +1 -1
- package/src/summary/index.ts +1 -1
- package/src/summary/runningSummarizer.ts +102 -80
- package/src/summary/summarizer.ts +0 -8
- package/src/summary/summarizerHeuristics.ts +4 -0
- package/src/summary/summarizerNode/summarizerNode.ts +1 -1
- package/src/summary/summarizerNode/summarizerNodeWithGc.ts +3 -3
- package/src/summary/summarizerTypes.ts +20 -3
- package/src/summary/summaryGenerator.ts +22 -16
package/src/blobManager.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
IContainerRuntime,
|
|
29
29
|
IContainerRuntimeEvents,
|
|
30
30
|
} from "@fluidframework/container-runtime-definitions";
|
|
31
|
-
import { AttachState } from "@fluidframework/container-definitions";
|
|
31
|
+
import { AttachState, ICriticalContainerError } from "@fluidframework/container-definitions";
|
|
32
32
|
import {
|
|
33
33
|
ChildLogger,
|
|
34
34
|
loggerToMonitoringContext,
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
ISummaryTreeWithStats,
|
|
41
41
|
ITelemetryContext,
|
|
42
42
|
} from "@fluidframework/runtime-definitions";
|
|
43
|
+
import { GenericError } from "@fluidframework/container-utils";
|
|
43
44
|
import { ContainerRuntime, TombstoneResponseHeaderKey } from "./containerRuntime";
|
|
44
45
|
import { sendGCUnexpectedUsageEvent, sweepAttachmentBlobsKey, throwOnTombstoneLoadKey } from "./gc";
|
|
45
46
|
import { Throttler, formExponentialFn, IThrottler } from "./throttler";
|
|
@@ -132,14 +133,13 @@ interface PendingBlob {
|
|
|
132
133
|
status: PendingBlobStatus;
|
|
133
134
|
storageId?: string;
|
|
134
135
|
handleP: Deferred<IFluidHandle<ArrayBufferLike>>;
|
|
135
|
-
uploadP
|
|
136
|
-
|
|
137
|
-
serverUploadTime?: number;
|
|
136
|
+
uploadP?: Promise<ICreateBlobResponse>;
|
|
137
|
+
uploadTime?: number;
|
|
138
138
|
minTTLInSeconds?: number;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
export interface IPendingBlobs {
|
|
142
|
-
[id: string]: { blob: string };
|
|
142
|
+
[id: string]: { blob: string; uploadTime?: number; minTTLInSeconds?: number };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
export interface IBlobManagerEvents {
|
|
@@ -191,6 +191,8 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
191
191
|
*/
|
|
192
192
|
private readonly tombstonedBlobs: Set<string> = new Set();
|
|
193
193
|
|
|
194
|
+
private readonly sendBlobAttachOp: (localId: string, storageId?: string) => void;
|
|
195
|
+
|
|
194
196
|
constructor(
|
|
195
197
|
private readonly routeContext: IFluidHandleContext,
|
|
196
198
|
snapshot: IBlobManagerLoadInfo,
|
|
@@ -205,7 +207,7 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
205
207
|
* knowledge of which they cannot request the blob from storage. It's important that this op is sequenced
|
|
206
208
|
* before any ops that reference the local ID, otherwise, an invalid handle could be added to the document.
|
|
207
209
|
*/
|
|
208
|
-
|
|
210
|
+
sendBlobAttachOp: (localId: string, storageId?: string) => void,
|
|
209
211
|
// Called when a blob node is requested. blobPath is the path of the blob's node in GC's graph.
|
|
210
212
|
// blobPath's format - `/<BlobManager.basePath>/<blobId>`.
|
|
211
213
|
private readonly blobRequested: (blobPath: string) => void,
|
|
@@ -214,7 +216,7 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
214
216
|
private readonly isBlobDeleted: (blobPath: string) => boolean,
|
|
215
217
|
private readonly runtime: IBlobManagerRuntime,
|
|
216
218
|
stashedBlobs: IPendingBlobs = {},
|
|
217
|
-
private readonly
|
|
219
|
+
private readonly closeContainer: (error?: ICriticalContainerError) => void,
|
|
218
220
|
) {
|
|
219
221
|
super();
|
|
220
222
|
this.mc = loggerToMonitoringContext(ChildLogger.create(this.runtime.logger, "BlobManager"));
|
|
@@ -230,6 +232,21 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
230
232
|
// Begin uploading stashed blobs from previous container instance
|
|
231
233
|
Object.entries(stashedBlobs).forEach(([localId, entry]) => {
|
|
232
234
|
const blob = stringToBuffer(entry.blob, "base64");
|
|
235
|
+
if (entry.minTTLInSeconds && entry.uploadTime) {
|
|
236
|
+
const timeLapseSinceLocalUpload = (Date.now() - entry.uploadTime) / 1000;
|
|
237
|
+
// stashed entries with more than half-life in storage will not be reuploaded
|
|
238
|
+
if (entry.minTTLInSeconds - timeLapseSinceLocalUpload > entry.minTTLInSeconds / 2) {
|
|
239
|
+
this.pendingBlobs.set(localId, {
|
|
240
|
+
blob,
|
|
241
|
+
status: PendingBlobStatus.OfflinePendingOp,
|
|
242
|
+
handleP: new Deferred(),
|
|
243
|
+
uploadP: undefined,
|
|
244
|
+
uploadTime: entry.uploadTime,
|
|
245
|
+
minTTLInSeconds: entry.minTTLInSeconds,
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
233
250
|
this.pendingBlobs.set(localId, {
|
|
234
251
|
blob,
|
|
235
252
|
status: PendingBlobStatus.OfflinePendingUpload,
|
|
@@ -237,6 +254,37 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
237
254
|
uploadP: this.uploadBlob(localId, blob),
|
|
238
255
|
});
|
|
239
256
|
});
|
|
257
|
+
|
|
258
|
+
this.sendBlobAttachOp = (localId: string, blobId?: string) => {
|
|
259
|
+
const pendingEntry = this.pendingBlobs.get(localId);
|
|
260
|
+
if (pendingEntry?.uploadTime && pendingEntry?.minTTLInSeconds) {
|
|
261
|
+
const secondsSinceUpload = (Date.now() - pendingEntry.uploadTime) / 1000;
|
|
262
|
+
const expired = pendingEntry.minTTLInSeconds - secondsSinceUpload < 0;
|
|
263
|
+
this.mc.logger.sendTelemetryEvent({
|
|
264
|
+
eventName: "sendBlobAttach",
|
|
265
|
+
entryStatus: pendingEntry.status,
|
|
266
|
+
secondsSinceUpload,
|
|
267
|
+
minTTLInSeconds: pendingEntry.minTTLInSeconds,
|
|
268
|
+
expired,
|
|
269
|
+
});
|
|
270
|
+
if (expired) {
|
|
271
|
+
// we want to avoid submitting ops with broken handles
|
|
272
|
+
this.closeContainer(
|
|
273
|
+
new GenericError(
|
|
274
|
+
"Trying to submit a BlobAttach for expired blob",
|
|
275
|
+
undefined,
|
|
276
|
+
{
|
|
277
|
+
localId,
|
|
278
|
+
blobId,
|
|
279
|
+
entryStatus: pendingEntry.status,
|
|
280
|
+
secondsSinceUpload,
|
|
281
|
+
},
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return sendBlobAttachOp(localId, blobId);
|
|
287
|
+
};
|
|
240
288
|
}
|
|
241
289
|
|
|
242
290
|
private get pendingOfflineUploads() {
|
|
@@ -428,9 +476,8 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
428
476
|
0x386 /* Must have pending blob entry for uploaded blob */,
|
|
429
477
|
);
|
|
430
478
|
entry.storageId = response.id;
|
|
431
|
-
entry.
|
|
479
|
+
entry.uploadTime = Date.now();
|
|
432
480
|
entry.minTTLInSeconds = response.minTTLInSeconds;
|
|
433
|
-
entry.serverUploadTime = this.getCurrentReferenceTimestampMs();
|
|
434
481
|
if (this.runtime.connected) {
|
|
435
482
|
if (entry.status === PendingBlobStatus.OnlinePendingUpload) {
|
|
436
483
|
// Send a blob attach op. This serves two purposes:
|
|
@@ -438,7 +485,6 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
438
485
|
// until its storage ID is added to the next summary.
|
|
439
486
|
// 2. It will create a local ID to storage ID mapping in all clients which is needed to retrieve the
|
|
440
487
|
// blob from the server via the storage ID.
|
|
441
|
-
this.logTimeInfo(entry, "sendBlobAttachResolveTTL");
|
|
442
488
|
this.sendBlobAttachOp(localId, response.id);
|
|
443
489
|
if (this.storageIds.has(response.id)) {
|
|
444
490
|
// The blob is de-duped. Set up a local ID to storage ID mapping and return the blob. Since this is
|
|
@@ -512,7 +558,6 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
512
558
|
* is called on reconnection.
|
|
513
559
|
*/
|
|
514
560
|
if (entry.status !== PendingBlobStatus.OnlinePendingOp) {
|
|
515
|
-
this.logTimeInfo(entry, "sendBlobAttachTransitionOfflineTTL");
|
|
516
561
|
this.sendBlobAttachOp(localId, entry.storageId);
|
|
517
562
|
}
|
|
518
563
|
|
|
@@ -534,9 +579,7 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
534
579
|
const { localId, blobId }: { localId?: string; blobId?: string } = metadata;
|
|
535
580
|
assert(localId !== undefined, 0x50d /* local ID not available on reSubmit */);
|
|
536
581
|
const pendingEntry = this.pendingBlobs.get(localId);
|
|
537
|
-
|
|
538
|
-
this.logTimeInfo(pendingEntry, "sendBlobAttachResubmitTTL");
|
|
539
|
-
}
|
|
582
|
+
|
|
540
583
|
if (!blobId) {
|
|
541
584
|
// We submitted this op while offline. The blob should have been uploaded by now.
|
|
542
585
|
assert(
|
|
@@ -549,32 +592,6 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
549
592
|
return this.sendBlobAttachOp(localId, blobId);
|
|
550
593
|
}
|
|
551
594
|
|
|
552
|
-
private logTimeInfo(pendingEntry: PendingBlob, eventName: string) {
|
|
553
|
-
let timeLapseSinceLocalUpload: number = 0;
|
|
554
|
-
let timeLapseSinceServerUpload: number = 0;
|
|
555
|
-
let expiredUsingLocalTime;
|
|
556
|
-
let expiredUsingServerTime;
|
|
557
|
-
if (pendingEntry.localUploadTime) {
|
|
558
|
-
timeLapseSinceLocalUpload = (Date.now() - pendingEntry.localUploadTime) / 1000;
|
|
559
|
-
expiredUsingLocalTime =
|
|
560
|
-
(pendingEntry.minTTLInSeconds ?? 0) - timeLapseSinceLocalUpload < 0 ? true : false;
|
|
561
|
-
}
|
|
562
|
-
if (pendingEntry.serverUploadTime) {
|
|
563
|
-
timeLapseSinceServerUpload = (Date.now() - pendingEntry.serverUploadTime) / 1000;
|
|
564
|
-
expiredUsingServerTime =
|
|
565
|
-
(pendingEntry.minTTLInSeconds ?? 0) - timeLapseSinceServerUpload < 0 ? true : false;
|
|
566
|
-
}
|
|
567
|
-
this.mc.logger.sendTelemetryEvent({
|
|
568
|
-
eventName,
|
|
569
|
-
entryStatus: pendingEntry.status,
|
|
570
|
-
timeLapseSinceLocalUpload,
|
|
571
|
-
timeLapseSinceServerUpload,
|
|
572
|
-
minTTLInSeconds: pendingEntry.minTTLInSeconds,
|
|
573
|
-
expiredUsingLocalTime,
|
|
574
|
-
expiredUsingServerTime,
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
|
|
578
595
|
public processBlobAttachOp(message: ISequencedDocumentMessage, local: boolean) {
|
|
579
596
|
const localId = message.metadata?.localId;
|
|
580
597
|
const blobId = message.metadata?.blobId;
|
|
@@ -887,7 +904,13 @@ export class BlobManager extends TypedEventEmitter<IBlobManagerEvents> {
|
|
|
887
904
|
public getPendingBlobs(): IPendingBlobs {
|
|
888
905
|
const blobs = {};
|
|
889
906
|
for (const [key, entry] of this.pendingBlobs) {
|
|
890
|
-
blobs[key] =
|
|
907
|
+
blobs[key] = entry.minTTLInSeconds
|
|
908
|
+
? {
|
|
909
|
+
blob: bufferToString(entry.blob, "base64"),
|
|
910
|
+
uploadTime: entry.uploadTime,
|
|
911
|
+
minTTLInSeconds: entry.minTTLInSeconds,
|
|
912
|
+
}
|
|
913
|
+
: { blob: bufferToString(entry.blob, "base64") };
|
|
891
914
|
}
|
|
892
915
|
return blobs;
|
|
893
916
|
}
|
package/src/containerRuntime.ts
CHANGED
|
@@ -168,6 +168,7 @@ import {
|
|
|
168
168
|
Outbox,
|
|
169
169
|
OpSplitter,
|
|
170
170
|
RemoteMessageProcessor,
|
|
171
|
+
OpGroupingManager,
|
|
171
172
|
} from "./opLifecycle";
|
|
172
173
|
import { DeltaManagerSummarizerProxy } from "./deltaManagerSummarizerProxy";
|
|
173
174
|
|
|
@@ -205,7 +206,7 @@ export interface ISummaryBaseConfiguration {
|
|
|
205
206
|
/**
|
|
206
207
|
* Defines the maximum allowed time to wait for a pending summary ack.
|
|
207
208
|
* The maximum amount of time client will wait for a summarize is the minimum of
|
|
208
|
-
* maxSummarizeAckWaitTime (currently
|
|
209
|
+
* maxSummarizeAckWaitTime (currently 3 * 60 * 1000) and maxAckWaitTime.
|
|
209
210
|
*/
|
|
210
211
|
maxAckWaitTime: number;
|
|
211
212
|
/**
|
|
@@ -301,7 +302,7 @@ export const DefaultSummaryConfiguration: ISummaryConfiguration = {
|
|
|
301
302
|
|
|
302
303
|
minOpsForLastSummaryAttempt: 10,
|
|
303
304
|
|
|
304
|
-
maxAckWaitTime:
|
|
305
|
+
maxAckWaitTime: 3 * 60 * 1000, // 3 mins.
|
|
305
306
|
|
|
306
307
|
maxOpsSinceLastSummary: 7000,
|
|
307
308
|
|
|
@@ -402,6 +403,17 @@ export interface IContainerRuntimeOptions {
|
|
|
402
403
|
* can be used to disable it at runtime.
|
|
403
404
|
*/
|
|
404
405
|
readonly enableOpReentryCheck?: boolean;
|
|
406
|
+
/**
|
|
407
|
+
* If enabled, the runtime will group messages within a batch into a single
|
|
408
|
+
* message to be sent to the service.
|
|
409
|
+
* The grouping an ungrouping of such messages is handled by the "OpGroupingManager".
|
|
410
|
+
*
|
|
411
|
+
* By default, the feature is disabled. If enabled from options, the `Fluid.ContainerRuntime.DisableGroupedBatching`
|
|
412
|
+
* flag can be used to disable it at runtime.
|
|
413
|
+
*
|
|
414
|
+
* @experimental Not ready for use.
|
|
415
|
+
*/
|
|
416
|
+
readonly enableGroupedBatching?: boolean;
|
|
405
417
|
}
|
|
406
418
|
|
|
407
419
|
/**
|
|
@@ -643,6 +655,7 @@ export class ContainerRuntime
|
|
|
643
655
|
maxBatchSizeInBytes = defaultMaxBatchSizeInBytes,
|
|
644
656
|
chunkSizeInBytes = defaultChunkSizeInBytes,
|
|
645
657
|
enableOpReentryCheck = false,
|
|
658
|
+
enableGroupedBatching = false,
|
|
646
659
|
} = runtimeOptions;
|
|
647
660
|
|
|
648
661
|
const registry = new FluidDataStoreRegistry(registryEntries);
|
|
@@ -726,6 +739,7 @@ export class ContainerRuntime
|
|
|
726
739
|
maxBatchSizeInBytes,
|
|
727
740
|
chunkSizeInBytes,
|
|
728
741
|
enableOpReentryCheck,
|
|
742
|
+
enableGroupedBatching,
|
|
729
743
|
},
|
|
730
744
|
containerScope,
|
|
731
745
|
logger,
|
|
@@ -1056,6 +1070,9 @@ export class ContainerRuntime
|
|
|
1056
1070
|
const disableChunking = this.mc.config.getBoolean(
|
|
1057
1071
|
"Fluid.ContainerRuntime.CompressionChunkingDisabled",
|
|
1058
1072
|
);
|
|
1073
|
+
|
|
1074
|
+
const opGroupingManager = new OpGroupingManager(this.groupedBatchingEnabled);
|
|
1075
|
+
|
|
1059
1076
|
const opSplitter = new OpSplitter(
|
|
1060
1077
|
chunks,
|
|
1061
1078
|
this.context.submitBatchFn,
|
|
@@ -1063,9 +1080,11 @@ export class ContainerRuntime
|
|
|
1063
1080
|
runtimeOptions.maxBatchSizeInBytes,
|
|
1064
1081
|
this.mc.logger,
|
|
1065
1082
|
);
|
|
1083
|
+
|
|
1066
1084
|
this.remoteMessageProcessor = new RemoteMessageProcessor(
|
|
1067
1085
|
opSplitter,
|
|
1068
1086
|
new OpDecompressor(this.mc.logger),
|
|
1087
|
+
opGroupingManager,
|
|
1069
1088
|
);
|
|
1070
1089
|
|
|
1071
1090
|
this.handleContext = new ContainerFluidHandleContext("", this);
|
|
@@ -1193,9 +1212,15 @@ export class ContainerRuntime
|
|
|
1193
1212
|
() => this.storage,
|
|
1194
1213
|
(localId: string, blobId?: string) => {
|
|
1195
1214
|
if (!this.disposed) {
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1215
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1216
|
+
Promise.resolve().then(() => {
|
|
1217
|
+
// Blob attaches need to be in their own batch (grouped batching would hide metadata)
|
|
1218
|
+
this.flush();
|
|
1219
|
+
this.submit(ContainerMessageType.BlobAttach, undefined, undefined, {
|
|
1220
|
+
localId,
|
|
1221
|
+
blobId,
|
|
1222
|
+
});
|
|
1223
|
+
this.flush();
|
|
1199
1224
|
});
|
|
1200
1225
|
}
|
|
1201
1226
|
},
|
|
@@ -1203,7 +1228,7 @@ export class ContainerRuntime
|
|
|
1203
1228
|
(blobPath: string) => this.garbageCollector.isNodeDeleted(blobPath),
|
|
1204
1229
|
this,
|
|
1205
1230
|
pendingRuntimeState?.pendingAttachmentBlobs,
|
|
1206
|
-
() => this.
|
|
1231
|
+
(error?: ICriticalContainerError) => this.closeFn(error),
|
|
1207
1232
|
);
|
|
1208
1233
|
|
|
1209
1234
|
this.scheduleManager = new ScheduleManager(
|
|
@@ -1252,6 +1277,7 @@ export class ContainerRuntime
|
|
|
1252
1277
|
disablePartialFlush: disablePartialFlush === true,
|
|
1253
1278
|
},
|
|
1254
1279
|
logger: this.mc.logger,
|
|
1280
|
+
groupingManager: opGroupingManager,
|
|
1255
1281
|
});
|
|
1256
1282
|
|
|
1257
1283
|
this.context.quorum.on("removeMember", (clientId: string) => {
|
|
@@ -1400,6 +1426,7 @@ export class ContainerRuntime
|
|
|
1400
1426
|
disablePartialFlush,
|
|
1401
1427
|
}),
|
|
1402
1428
|
telemetryDocumentId: this.telemetryDocumentId,
|
|
1429
|
+
groupedBatchingEnabled: this.groupedBatchingEnabled,
|
|
1403
1430
|
});
|
|
1404
1431
|
|
|
1405
1432
|
ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
|
|
@@ -1814,8 +1841,16 @@ export class ContainerRuntime
|
|
|
1814
1841
|
|
|
1815
1842
|
// Do shallow copy of message, as the processing flow will modify it.
|
|
1816
1843
|
const messageCopy = { ...messageArg };
|
|
1817
|
-
const message
|
|
1844
|
+
for (const message of this.remoteMessageProcessor.process(messageCopy)) {
|
|
1845
|
+
this.processCore(message, local, runtimeMessage);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1818
1848
|
|
|
1849
|
+
private processCore(
|
|
1850
|
+
message: ISequencedDocumentMessage,
|
|
1851
|
+
local: boolean,
|
|
1852
|
+
runtimeMessage: boolean,
|
|
1853
|
+
) {
|
|
1819
1854
|
// Surround the actual processing of the operation with messages to the schedule manager indicating
|
|
1820
1855
|
// the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
|
|
1821
1856
|
// messages once a batch has been fully processed.
|
|
@@ -1870,8 +1905,7 @@ export class ContainerRuntime
|
|
|
1870
1905
|
}
|
|
1871
1906
|
}
|
|
1872
1907
|
|
|
1873
|
-
|
|
1874
|
-
if (runtimeMessage) {
|
|
1908
|
+
if (runtimeMessage || this.groupedBatchingEnabled) {
|
|
1875
1909
|
this.emit("op", message, runtimeMessage);
|
|
1876
1910
|
}
|
|
1877
1911
|
|
|
@@ -3177,6 +3211,26 @@ export class ContainerRuntime
|
|
|
3177
3211
|
readAndParseBlob: ReadAndParseBlob,
|
|
3178
3212
|
versionId: string | null,
|
|
3179
3213
|
): Promise<{ snapshotTree: ISnapshotTree; versionId: string; latestSnapshotRefSeq: number }> {
|
|
3214
|
+
const recoveryMethod = this.mc.config.getString(
|
|
3215
|
+
"Fluid.ContainerRuntime.Test.SummarizationRecoveryMethod",
|
|
3216
|
+
);
|
|
3217
|
+
if (recoveryMethod === "restart") {
|
|
3218
|
+
const error = new GenericError("Restarting summarizer instead of refreshing");
|
|
3219
|
+
this.mc.logger.sendTelemetryEvent(
|
|
3220
|
+
{
|
|
3221
|
+
...event,
|
|
3222
|
+
eventName: "ClosingSummarizerOnSummaryStale",
|
|
3223
|
+
codePath: event.eventName,
|
|
3224
|
+
message: "Stopping fetch from storage",
|
|
3225
|
+
versionId: versionId != null ? versionId : undefined,
|
|
3226
|
+
},
|
|
3227
|
+
error,
|
|
3228
|
+
);
|
|
3229
|
+
this._summarizer?.stop("latestSummaryStateStale");
|
|
3230
|
+
this.closeFn();
|
|
3231
|
+
throw error;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3180
3234
|
return PerformanceEvent.timedExecAsync(
|
|
3181
3235
|
logger,
|
|
3182
3236
|
event,
|
|
@@ -3316,6 +3370,13 @@ export class ContainerRuntime
|
|
|
3316
3370
|
);
|
|
3317
3371
|
}
|
|
3318
3372
|
}
|
|
3373
|
+
|
|
3374
|
+
private get groupedBatchingEnabled(): boolean {
|
|
3375
|
+
const killSwitch = this.mc.config.getBoolean(
|
|
3376
|
+
"Fluid.ContainerRuntime.DisableGroupedBatching",
|
|
3377
|
+
);
|
|
3378
|
+
return killSwitch !== true && this.runtimeOptions.enableGroupedBatching;
|
|
3379
|
+
}
|
|
3319
3380
|
}
|
|
3320
3381
|
|
|
3321
3382
|
/**
|
package/src/gc/gcHelpers.ts
CHANGED
|
@@ -169,7 +169,7 @@ export function concatGarbageCollectionStates(
|
|
|
169
169
|
) {
|
|
170
170
|
assert(
|
|
171
171
|
nodeData.unreferencedTimestampMs === combineNodeData.unreferencedTimestampMs,
|
|
172
|
-
|
|
172
|
+
0x5d7 /* Two entries for the same GC node with different unreferenced timestamp */,
|
|
173
173
|
);
|
|
174
174
|
}
|
|
175
175
|
combineNodeData = {
|
|
@@ -253,7 +253,7 @@ export async function getGCDataFromSnapshot(
|
|
|
253
253
|
continue;
|
|
254
254
|
}
|
|
255
255
|
const gcState = await readAndParseBlob<IGarbageCollectionState>(blobId);
|
|
256
|
-
assert(gcState !== undefined,
|
|
256
|
+
assert(gcState !== undefined, 0x5d8 /* GC blob missing from snapshot */);
|
|
257
257
|
// Merge the GC state of this blob into the root GC state.
|
|
258
258
|
rootGCState = concatGarbageCollectionStates(rootGCState, gcState);
|
|
259
259
|
}
|
|
@@ -280,7 +280,7 @@ export function unpackChildNodesGCDetails(gcDetails: IGarbageCollectionDetailsBa
|
|
|
280
280
|
continue;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
assert(id.startsWith("/"),
|
|
283
|
+
assert(id.startsWith("/"), 0x5d9 /* node id should always be an absolute route */);
|
|
284
284
|
const childId = id.split("/")[1];
|
|
285
285
|
let childGCNodeId = id.slice(childId.length + 1);
|
|
286
286
|
// GC node id always begins with "/". Handle the special case where a child's id in the parent's GC nodes is
|
|
@@ -294,7 +294,10 @@ export function unpackChildNodesGCDetails(gcDetails: IGarbageCollectionDetailsBa
|
|
|
294
294
|
childGCDetails = { gcData: { gcNodes: {} }, usedRoutes: [] };
|
|
295
295
|
}
|
|
296
296
|
// gcData should not undefined as its always at least initialized as empty above.
|
|
297
|
-
assert(
|
|
297
|
+
assert(
|
|
298
|
+
childGCDetails.gcData !== undefined,
|
|
299
|
+
0x5da /* Child GC data should have been initialized */,
|
|
300
|
+
);
|
|
298
301
|
childGCDetails.gcData.gcNodes[childGCNodeId] = [...new Set(outboundRoutes)];
|
|
299
302
|
childGCDetailsMap.set(childId, childGCDetails);
|
|
300
303
|
}
|
|
@@ -306,14 +309,14 @@ export function unpackChildNodesGCDetails(gcDetails: IGarbageCollectionDetailsBa
|
|
|
306
309
|
// Remove the node's self used route, if any, and generate the children used routes.
|
|
307
310
|
const usedRoutes = gcDetails.usedRoutes.filter((route) => route !== "" && route !== "/");
|
|
308
311
|
for (const route of usedRoutes) {
|
|
309
|
-
assert(route.startsWith("/"),
|
|
312
|
+
assert(route.startsWith("/"), 0x5db /* Used route should always be an absolute route */);
|
|
310
313
|
const childId = route.split("/")[1];
|
|
311
314
|
const childUsedRoute = route.slice(childId.length + 1);
|
|
312
315
|
|
|
313
316
|
const childGCDetails = childGCDetailsMap.get(childId);
|
|
314
317
|
assert(
|
|
315
318
|
childGCDetails?.usedRoutes !== undefined,
|
|
316
|
-
|
|
319
|
+
0x5dc /* This should have be initialized when generate GC nodes above */,
|
|
317
320
|
);
|
|
318
321
|
|
|
319
322
|
childGCDetails.usedRoutes.push(childUsedRoute);
|
|
@@ -12,10 +12,12 @@ By default, the runtime is configured with a max batch size of `716800` bytes, w
|
|
|
12
12
|
|
|
13
13
|
- [Introduction](#introduction)
|
|
14
14
|
- [Compression](#compression)
|
|
15
|
+
- [Grouped batching](#grouped-batching)
|
|
15
16
|
- [Chunking for compression](#chunking-for-compression)
|
|
16
17
|
- [Disabling in case of emergency](#disabling-in-case-of-emergency)
|
|
17
18
|
- [Example configs](#example-configs)
|
|
18
19
|
- [How it works](#how-it-works)
|
|
20
|
+
- [How grouped batching works](#how-grouped-batching-works)
|
|
19
21
|
|
|
20
22
|
## Compression
|
|
21
23
|
|
|
@@ -26,6 +28,16 @@ By default, the runtime is configured with a max batch size of `716800` bytes, w
|
|
|
26
28
|
- `minimumBatchSizeInBytes` – the minimum size of the batch for which compression should kick in. If the payload is too small, compression may not yield too many benefits. To target the original 1MB issue, a good value here would be to match the default maxBatchSizeInBytes (972800), however, experimentally, a good lower value could be at around 614400 bytes. Setting this value to `Number.POSITIVE_INFINITY` will disable compression.
|
|
27
29
|
- `compressionAlgorithm` – currently, only `lz4` is supported.
|
|
28
30
|
|
|
31
|
+
## Grouped batching
|
|
32
|
+
|
|
33
|
+
**Note: This feature is currently considered experimental and is not ready for production usage.**
|
|
34
|
+
|
|
35
|
+
The `IContainerRuntimeOptions.enableGroupedBatching` option has been added to the container runtime layer and is **off by default**. This option will group all batch messages under a new "grouped" message to be sent to the service. Upon receiving this new "grouped" message, the batch messages will be extracted and given the sequence number of the parent "grouped" message.
|
|
36
|
+
|
|
37
|
+
The purpose for enabling grouped batching on top of compression is that regular compression won't include the empty messages in the chunks. Thus, if we have batches with many messages (i.e. more than 4k), we will go over the batch size limit just on empty op envelopes alone.
|
|
38
|
+
|
|
39
|
+
See [below](#how-grouped-batching-works) for an example.
|
|
40
|
+
|
|
29
41
|
## Chunking for compression
|
|
30
42
|
|
|
31
43
|
**Op chunking for compression targets payloads which exceed the max batch size after compression.** So, only payloads which are already compressed. By default, the feature is enabled.
|
|
@@ -39,6 +51,7 @@ This config would govern chunking compressed batches only. We will not be enabli
|
|
|
39
51
|
If the features are enabled using the configs, they can be disabled at runtime via feature gates as following:
|
|
40
52
|
|
|
41
53
|
- `Fluid.ContainerRuntime.CompressionDisabled` - if set to true, will disable compression (this has a side effect of also disabling chunking, as chunking is invoked only for compressed payloads).
|
|
54
|
+
- `Fluid.ContainerRuntime.DisableGroupedBatching` - if set to true, will disable grouped batching.
|
|
42
55
|
- `Fluid.ContainerRuntime.CompressionChunkingDisabled` - if set to true, will disable chunking for compression.
|
|
43
56
|
|
|
44
57
|
## Example configs
|
|
@@ -75,6 +88,14 @@ To disable compression (will also disable chunking, as chunking works only for c
|
|
|
75
88
|
}
|
|
76
89
|
```
|
|
77
90
|
|
|
91
|
+
To enable grouped batching:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
const runtimeOptions: IContainerRuntimeOptions = {
|
|
95
|
+
enableGroupedBatching: true,
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
78
99
|
## How it works
|
|
79
100
|
|
|
80
101
|
Compression currently works as a runtime layer over the regular op sending/receiving pipeline.
|
|
@@ -155,3 +176,88 @@ Notice that the sequence numbers don’t matter here, as all ops will be based o
|
|
|
155
176
|
Additionally, as compression preserves the original uncompressed batch layout in terms of the number of ops by using empty ops to reserve the sequence numbers, this ensures that the clients will always receive the exact count of ops to rebuild the uncompressed batch sequentially.
|
|
156
177
|
|
|
157
178
|
On the receiving end, the client will accumulate chunks 1 and 2 and keep them in memory. When chunk 3 is received, the original large, decompressed op will be rebuilt, and the runtime will then process the batch as if it is a compressed batch.
|
|
179
|
+
|
|
180
|
+
## How grouped batching works
|
|
181
|
+
|
|
182
|
+
**Note: There are plans to replace empty ops with something more efficient when doing grouped batching AB#4092**
|
|
183
|
+
|
|
184
|
+
Given the following baseline batch:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
+---------------+---------------+---------------+---------------+---------------+
|
|
188
|
+
| Op 1 | Op 2 | Op 3 | Op 4 | Op 5 |
|
|
189
|
+
| Contents: "a" | Contents: "b" | Contents: "c" | Contents: "d" | Contents: "e" |
|
|
190
|
+
+---------------+---------------+---------------+---------------+---------------+
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Compressed batch:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
+--------------------+-----------------+-----------------+-----------------+-----------------+
|
|
197
|
+
| Op 1 | Op 2 | Op 3 | Op 4 | Op 5 |
|
|
198
|
+
| Contents: "abcde" | Contents: empty | Contents: empty | Contents: empty | Contents: empty |
|
|
199
|
+
| Compression: 'lz4' | | | | |
|
|
200
|
+
+--------------------+-----------------+-----------------+-----------------+-----------------+
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Grouped batch:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
+---------------------------------------------------------------------------------------------------------------------------------+
|
|
207
|
+
| Op 1 Contents: +--------------------+-----------------+-----------------+-----------------+-----------------+ |
|
|
208
|
+
| SeqNum: 1 | Op 1 | Op 2 | Op 3 | Op 4 | Op 5 | |
|
|
209
|
+
| Type: "groupedBatch" | Contents: "abcde" | Contents: empty | Contents: empty | Contents: empty | Contents: empty | |
|
|
210
|
+
| | Compression: 'lz4' | | | | | |
|
|
211
|
+
| +--------------------+-----------------+-----------------+-----------------+-----------------+ |
|
|
212
|
+
+---------------------------------------------------------------------------------------------------------------------------------+
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Can produce the following chunks:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
+-------------------------------------------------+
|
|
219
|
+
| Chunk 1/2 Contents: +----------------------+ |
|
|
220
|
+
| SeqNum: 1 | +-----------------+ | |
|
|
221
|
+
| | | Contents: "abc" | | |
|
|
222
|
+
| | +-----------------+ | |
|
|
223
|
+
| +----------------------+ |
|
|
224
|
+
+-------------------------------------------------+
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
+--------------------------------------------------------------------------------------------------------------------------+
|
|
229
|
+
| Chunk 2/2 Contents: +---------------------------------------------------------------------------------------------+ | |
|
|
230
|
+
| SeqNum: 2 | +----------------+-----------------+-----------------+-----------------+-----------------+ | | |
|
|
231
|
+
| | | Contents: "de" | Contents: empty | Contents: empty | Contents: empty | Contents: empty | | | |
|
|
232
|
+
| | +----------------+-----------------+-----------------+-----------------+-----------------+ | | |
|
|
233
|
+
| +---------------------------------------------------------------------------------------------+ | |
|
|
234
|
+
+--------------------------------------------------------------------------------------------------------------------------+
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
- Send to service
|
|
238
|
+
- Service acks ops sent
|
|
239
|
+
- Receive chunks from service
|
|
240
|
+
- Recompile to the grouped batch step
|
|
241
|
+
|
|
242
|
+
Ungrouped batch:
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
+--------------------+-----------------+-----------------+-----------------+-----------------+
|
|
246
|
+
| Op 1 | Op 2 | Op 3 | Op 4 | Op 5 |
|
|
247
|
+
| Contents: "abcde" | Contents: empty | Contents: empty | Contents: empty | Contents: empty |
|
|
248
|
+
| SeqNum: 2 | SeqNum: 2 | SeqNum: 2 | SeqNum: 2 | SeqNum: 2 |
|
|
249
|
+
| ClientSeqNum: 1 | ClientSeqNum: 2 | ClientSeqNum: 3 | ClientSeqNum: 4 | ClientSeqNum: 5 |
|
|
250
|
+
| Compression: 'lz4' | | | | |
|
|
251
|
+
+--------------------+-----------------+-----------------+-----------------+-----------------+
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Uncompressed batch:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
+-----------------+-----------------+-----------------+-----------------+-----------------+
|
|
258
|
+
| Op 1 | Op 2 | Op 3 | Op 4 | Op 5 |
|
|
259
|
+
| Contents: "a" | Contents: "b" | Contents: "c" | Contents: "d" | Contents: "e" |
|
|
260
|
+
| SeqNum: 2 | SeqNum: 2 | SeqNum: 2 | SeqNum: 2 | SeqNum: 2 |
|
|
261
|
+
| ClientSeqNum: 1 | ClientSeqNum: 2 | ClientSeqNum: 3 | ClientSeqNum: 4 | ClientSeqNum: 5 |
|
|
262
|
+
+-----------------+-----------------+-----------------+-----------------+-----------------+
|
|
263
|
+
```
|
package/src/opLifecycle/index.ts
CHANGED
|
@@ -16,3 +16,4 @@ export { OpCompressor } from "./opCompressor";
|
|
|
16
16
|
export { OpDecompressor } from "./opDecompressor";
|
|
17
17
|
export { OpSplitter, splitOp } from "./opSplitter";
|
|
18
18
|
export { RemoteMessageProcessor, unpackRuntimeMessage } from "./remoteMessageProcessor";
|
|
19
|
+
export { OpGroupingManager } from "./opGroupingManager";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { assert } from "@fluidframework/common-utils";
|
|
7
|
+
import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
|
|
8
|
+
import { ContainerRuntimeMessage } from "..";
|
|
9
|
+
import { IBatch } from "./definitions";
|
|
10
|
+
|
|
11
|
+
interface IGroupedMessage {
|
|
12
|
+
contents?: unknown;
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
compression?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class OpGroupingManager {
|
|
18
|
+
static groupedBatchOp = "groupedBatch";
|
|
19
|
+
|
|
20
|
+
constructor(private readonly groupedBatchingEnabled: boolean) {}
|
|
21
|
+
|
|
22
|
+
public groupBatch(batch: IBatch): IBatch {
|
|
23
|
+
if (batch.content.length < 2 || !this.groupedBatchingEnabled) {
|
|
24
|
+
return batch;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const message of batch.content) {
|
|
28
|
+
if (message.metadata) {
|
|
29
|
+
const keys = Object.keys(message.metadata);
|
|
30
|
+
assert(keys.length < 2, 0x5dd /* cannot group ops with metadata */);
|
|
31
|
+
assert(
|
|
32
|
+
keys.length === 0 || keys[0] === "batch",
|
|
33
|
+
0x5de /* unexpected op metadata */,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Need deserializedContent for back-compat
|
|
39
|
+
const deserializedContent = {
|
|
40
|
+
type: OpGroupingManager.groupedBatchOp,
|
|
41
|
+
contents: batch.content.map<IGroupedMessage>((message) => ({
|
|
42
|
+
contents: message.contents === undefined ? undefined : JSON.parse(message.contents),
|
|
43
|
+
metadata: message.metadata,
|
|
44
|
+
compression: message.compression,
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const groupedBatch: IBatch = {
|
|
49
|
+
...batch,
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
localOpMetadata: undefined,
|
|
53
|
+
metadata: undefined,
|
|
54
|
+
referenceSequenceNumber: batch.content[0].referenceSequenceNumber,
|
|
55
|
+
deserializedContent: deserializedContent as ContainerRuntimeMessage,
|
|
56
|
+
contents: JSON.stringify(deserializedContent),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
return groupedBatch;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public ungroupOp(op: ISequencedDocumentMessage): ISequencedDocumentMessage[] {
|
|
64
|
+
if (op.contents?.type !== OpGroupingManager.groupedBatchOp) {
|
|
65
|
+
return [op];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const messages = op.contents.contents as IGroupedMessage[];
|
|
69
|
+
let fakeCsn = 1;
|
|
70
|
+
return messages.map((subMessage) => ({
|
|
71
|
+
...op,
|
|
72
|
+
clientSequenceNumber: fakeCsn++,
|
|
73
|
+
contents: subMessage.contents,
|
|
74
|
+
metadata: subMessage.metadata,
|
|
75
|
+
compression: subMessage.compression,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
}
|