@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.
Files changed (152) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +69 -0
  3. package/dist/blobManager.d.ts +6 -14
  4. package/dist/blobManager.d.ts.map +1 -1
  5. package/dist/blobManager.js +50 -37
  6. package/dist/blobManager.js.map +1 -1
  7. package/dist/containerRuntime.d.ts +14 -1
  8. package/dist/containerRuntime.d.ts.map +1 -1
  9. package/dist/containerRuntime.js +37 -12
  10. package/dist/containerRuntime.js.map +1 -1
  11. package/dist/gc/gcHelpers.d.ts.map +1 -1
  12. package/dist/gc/gcHelpers.js +6 -6
  13. package/dist/gc/gcHelpers.js.map +1 -1
  14. package/dist/opLifecycle/index.d.ts +1 -0
  15. package/dist/opLifecycle/index.d.ts.map +1 -1
  16. package/dist/opLifecycle/index.js +3 -1
  17. package/dist/opLifecycle/index.js.map +1 -1
  18. package/dist/opLifecycle/opDecompressor.d.ts.map +1 -1
  19. package/dist/opLifecycle/opDecompressor.js +2 -1
  20. package/dist/opLifecycle/opDecompressor.js.map +1 -1
  21. package/dist/opLifecycle/opGroupingManager.d.ts +14 -0
  22. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -0
  23. package/dist/opLifecycle/opGroupingManager.js +56 -0
  24. package/dist/opLifecycle/opGroupingManager.js.map +1 -0
  25. package/dist/opLifecycle/opSplitter.d.ts +1 -1
  26. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  27. package/dist/opLifecycle/opSplitter.js +5 -6
  28. package/dist/opLifecycle/opSplitter.js.map +1 -1
  29. package/dist/opLifecycle/outbox.d.ts +2 -0
  30. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  31. package/dist/opLifecycle/outbox.js +3 -3
  32. package/dist/opLifecycle/outbox.js.map +1 -1
  33. package/dist/opLifecycle/remoteMessageProcessor.d.ts +4 -2
  34. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  35. package/dist/opLifecycle/remoteMessageProcessor.js +30 -20
  36. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  37. package/dist/packageVersion.d.ts +1 -1
  38. package/dist/packageVersion.js +1 -1
  39. package/dist/packageVersion.js.map +1 -1
  40. package/dist/summary/index.d.ts +1 -1
  41. package/dist/summary/index.d.ts.map +1 -1
  42. package/dist/summary/index.js +3 -1
  43. package/dist/summary/index.js.map +1 -1
  44. package/dist/summary/runningSummarizer.d.ts +5 -3
  45. package/dist/summary/runningSummarizer.d.ts.map +1 -1
  46. package/dist/summary/runningSummarizer.js +82 -67
  47. package/dist/summary/runningSummarizer.js.map +1 -1
  48. package/dist/summary/summarizer.d.ts.map +1 -1
  49. package/dist/summary/summarizer.js +1 -5
  50. package/dist/summary/summarizer.js.map +1 -1
  51. package/dist/summary/summarizerHeuristics.d.ts +1 -0
  52. package/dist/summary/summarizerHeuristics.d.ts.map +1 -1
  53. package/dist/summary/summarizerHeuristics.js +3 -0
  54. package/dist/summary/summarizerHeuristics.js.map +1 -1
  55. package/dist/summary/summarizerNode/summarizerNode.js +1 -1
  56. package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
  57. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts +128 -2
  58. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  59. package/dist/summary/summarizerNode/summarizerNodeWithGc.js +4 -3
  60. package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  61. package/dist/summary/summarizerTypes.d.ts +14 -2
  62. package/dist/summary/summarizerTypes.d.ts.map +1 -1
  63. package/dist/summary/summarizerTypes.js.map +1 -1
  64. package/dist/summary/summaryGenerator.d.ts +28 -2
  65. package/dist/summary/summaryGenerator.d.ts.map +1 -1
  66. package/dist/summary/summaryGenerator.js +19 -16
  67. package/dist/summary/summaryGenerator.js.map +1 -1
  68. package/lib/blobManager.d.ts +6 -14
  69. package/lib/blobManager.d.ts.map +1 -1
  70. package/lib/blobManager.js +50 -37
  71. package/lib/blobManager.js.map +1 -1
  72. package/lib/containerRuntime.d.ts +14 -1
  73. package/lib/containerRuntime.d.ts.map +1 -1
  74. package/lib/containerRuntime.js +38 -13
  75. package/lib/containerRuntime.js.map +1 -1
  76. package/lib/gc/gcHelpers.d.ts.map +1 -1
  77. package/lib/gc/gcHelpers.js +6 -6
  78. package/lib/gc/gcHelpers.js.map +1 -1
  79. package/lib/opLifecycle/index.d.ts +1 -0
  80. package/lib/opLifecycle/index.d.ts.map +1 -1
  81. package/lib/opLifecycle/index.js +1 -0
  82. package/lib/opLifecycle/index.js.map +1 -1
  83. package/lib/opLifecycle/opDecompressor.d.ts.map +1 -1
  84. package/lib/opLifecycle/opDecompressor.js +2 -1
  85. package/lib/opLifecycle/opDecompressor.js.map +1 -1
  86. package/lib/opLifecycle/opGroupingManager.d.ts +14 -0
  87. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -0
  88. package/lib/opLifecycle/opGroupingManager.js +52 -0
  89. package/lib/opLifecycle/opGroupingManager.js.map +1 -0
  90. package/lib/opLifecycle/opSplitter.d.ts +1 -1
  91. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  92. package/lib/opLifecycle/opSplitter.js +5 -6
  93. package/lib/opLifecycle/opSplitter.js.map +1 -1
  94. package/lib/opLifecycle/outbox.d.ts +2 -0
  95. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  96. package/lib/opLifecycle/outbox.js +3 -3
  97. package/lib/opLifecycle/outbox.js.map +1 -1
  98. package/lib/opLifecycle/remoteMessageProcessor.d.ts +4 -2
  99. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  100. package/lib/opLifecycle/remoteMessageProcessor.js +30 -20
  101. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  102. package/lib/packageVersion.d.ts +1 -1
  103. package/lib/packageVersion.js +1 -1
  104. package/lib/packageVersion.js.map +1 -1
  105. package/lib/summary/index.d.ts +1 -1
  106. package/lib/summary/index.d.ts.map +1 -1
  107. package/lib/summary/index.js +1 -0
  108. package/lib/summary/index.js.map +1 -1
  109. package/lib/summary/runningSummarizer.d.ts +5 -3
  110. package/lib/summary/runningSummarizer.d.ts.map +1 -1
  111. package/lib/summary/runningSummarizer.js +82 -67
  112. package/lib/summary/runningSummarizer.js.map +1 -1
  113. package/lib/summary/summarizer.d.ts.map +1 -1
  114. package/lib/summary/summarizer.js +1 -5
  115. package/lib/summary/summarizer.js.map +1 -1
  116. package/lib/summary/summarizerHeuristics.d.ts +1 -0
  117. package/lib/summary/summarizerHeuristics.d.ts.map +1 -1
  118. package/lib/summary/summarizerHeuristics.js +3 -0
  119. package/lib/summary/summarizerHeuristics.js.map +1 -1
  120. package/lib/summary/summarizerNode/summarizerNode.js +1 -1
  121. package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
  122. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts +128 -2
  123. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  124. package/lib/summary/summarizerNode/summarizerNodeWithGc.js +3 -3
  125. package/lib/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  126. package/lib/summary/summarizerTypes.d.ts +14 -2
  127. package/lib/summary/summarizerTypes.d.ts.map +1 -1
  128. package/lib/summary/summarizerTypes.js.map +1 -1
  129. package/lib/summary/summaryGenerator.d.ts +28 -2
  130. package/lib/summary/summaryGenerator.d.ts.map +1 -1
  131. package/lib/summary/summaryGenerator.js +17 -15
  132. package/lib/summary/summaryGenerator.js.map +1 -1
  133. package/package.json +19 -16
  134. package/src/blobManager.ts +64 -41
  135. package/src/containerRuntime.ts +70 -9
  136. package/src/gc/gcHelpers.ts +9 -6
  137. package/src/opLifecycle/README.md +106 -0
  138. package/src/opLifecycle/index.ts +1 -0
  139. package/src/opLifecycle/opDecompressor.ts +1 -0
  140. package/src/opLifecycle/opGroupingManager.ts +78 -0
  141. package/src/opLifecycle/opSplitter.ts +1 -5
  142. package/src/opLifecycle/outbox.ts +7 -3
  143. package/src/opLifecycle/remoteMessageProcessor.ts +38 -22
  144. package/src/packageVersion.ts +1 -1
  145. package/src/summary/index.ts +1 -1
  146. package/src/summary/runningSummarizer.ts +102 -80
  147. package/src/summary/summarizer.ts +0 -8
  148. package/src/summary/summarizerHeuristics.ts +4 -0
  149. package/src/summary/summarizerNode/summarizerNode.ts +1 -1
  150. package/src/summary/summarizerNode/summarizerNodeWithGc.ts +3 -3
  151. package/src/summary/summarizerTypes.ts +20 -3
  152. package/src/summary/summaryGenerator.ts +22 -16
@@ -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: Promise<ICreateBlobResponse>;
136
- localUploadTime?: number;
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
- private readonly sendBlobAttachOp: (localId: string, storageId?: string) => void,
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 getCurrentReferenceTimestampMs: () => number | undefined,
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.localUploadTime = Date.now();
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
- if (pendingEntry) {
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] = { blob: bufferToString(entry.blob, "base64") };
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
  }
@@ -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 10 * 60 * 1000) and maxAckWaitTime.
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: 10 * 60 * 1000, // 10 mins.
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
- this.submit(ContainerMessageType.BlobAttach, undefined, undefined, {
1197
- localId,
1198
- blobId,
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.getCurrentReferenceTimestampMs(),
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 = this.remoteMessageProcessor.process(messageCopy);
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
- // For back-compat, notify only about runtime messages for now.
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
  /**
@@ -169,7 +169,7 @@ export function concatGarbageCollectionStates(
169
169
  ) {
170
170
  assert(
171
171
  nodeData.unreferencedTimestampMs === combineNodeData.unreferencedTimestampMs,
172
- "Two entries for the same GC node with different unreferenced timestamp",
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, "GC blob missing from snapshot");
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("/"), "node id should always be an absolute route");
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(childGCDetails.gcData !== undefined, "Child GC data should have been initialized");
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("/"), "Used route should always be an absolute route");
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
- "This should have be initialized when generate GC nodes above",
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
+ ```
@@ -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";
@@ -133,6 +133,7 @@ export class OpDecompressor {
133
133
  */
134
134
  try {
135
135
  if (
136
+ message.contents !== null &&
136
137
  typeof message.contents === "object" &&
137
138
  Object.keys(message.contents).length === 1 &&
138
139
  message.contents?.packedContents !== undefined &&
@@ -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
+ }