@fluidframework/runtime-utils 1.2.3-83900 → 2.0.0-internal.1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,9 +24,6 @@ import { assert, unreachableCase } from "@fluidframework/common-utils";
24
24
  import { mergeStats, convertToSummaryTree, calculateStats } from "../summaryUtils";
25
25
  import { ReadAndParseBlob } from "../utils";
26
26
  import {
27
- decodeSummary,
28
- encodeSummary,
29
- EncodeSummaryParam,
30
27
  EscapedPath,
31
28
  ICreateChildDetails,
32
29
  IInitialSummary,
@@ -63,7 +60,6 @@ export class SummarizerNode implements IRootSummarizerNode {
63
60
 
64
61
  protected readonly children = new Map<string, SummarizerNode>();
65
62
  protected readonly pendingSummaries = new Map<string, SummaryNode>();
66
- private readonly outstandingOps: ISequencedDocumentMessage[] = [];
67
63
  private wipReferenceSequenceNumber: number | undefined;
68
64
  private wipLocalPaths: { localPath: EscapedPath; additionalPath?: EscapedPath; } | undefined;
69
65
  private wipSkipRecursion = false;
@@ -112,52 +108,12 @@ export class SummarizerNode implements IRootSummarizerNode {
112
108
  }
113
109
  }
114
110
 
115
- try {
116
- const result = await this.summarizeInternalFn(fullTree, true, telemetryContext);
117
- this.wipLocalPaths = { localPath: EscapedPath.create(result.id) };
118
- if (result.pathPartsForChildren !== undefined) {
119
- this.wipLocalPaths.additionalPath = EscapedPath.createAndConcat(result.pathPartsForChildren);
120
- }
121
- return { summary: result.summary, stats: result.stats };
122
- } catch (error) {
123
- if (this.throwOnError || this.trackingSequenceNumber < this._changeSequenceNumber) {
124
- throw error;
125
- }
126
- const latestSummary = this._latestSummary;
127
- const initialSummary = this.initialSummary;
128
-
129
- let encodeParam: EncodeSummaryParam;
130
- let localPath: EscapedPath;
131
- if (latestSummary !== undefined) {
132
- // Create using handle of latest acked summary
133
- encodeParam = {
134
- fromSummary: true,
135
- summaryNode: latestSummary,
136
- };
137
- localPath = latestSummary.localPath;
138
- } else if (initialSummary?.summary !== undefined) {
139
- // Create using initial summary from attach op
140
- encodeParam = {
141
- fromSummary: false,
142
- initialSummary: initialSummary.summary,
143
- };
144
- localPath = EscapedPath.create(initialSummary.id);
145
- } else {
146
- // No base summary to reference
147
- throw error;
148
- }
149
- this.wipSummaryLogger.sendErrorEvent({
150
- eventName: "SummarizingWithBasePlusOps",
151
- },
152
- error);
153
- const summary = encodeSummary(encodeParam, this.outstandingOps);
154
- this.wipLocalPaths = {
155
- localPath,
156
- additionalPath: summary.additionalPath,
157
- };
158
- this.wipSkipRecursion = true;
159
- return { summary: summary.summary, stats: summary.stats };
111
+ const result = await this.summarizeInternalFn(fullTree, true, telemetryContext);
112
+ this.wipLocalPaths = { localPath: EscapedPath.create(result.id) };
113
+ if (result.pathPartsForChildren !== undefined) {
114
+ this.wipLocalPaths.additionalPath = EscapedPath.createAndConcat(result.pathPartsForChildren);
160
115
  }
116
+ return { summary: result.summary, stats: result.stats };
161
117
  }
162
118
 
163
119
  /**
@@ -286,7 +242,12 @@ export class SummarizerNode implements IRootSummarizerNode {
286
242
  );
287
243
  return { latestSummaryUpdated: true, wasSummaryTracked: false, snapshot: snapshotTree };
288
244
  }
289
-
245
+ /**
246
+ * Called when we get an ack from the server for a summary we've just sent. Updates the reference state of this node
247
+ * from the state in the pending summary queue.
248
+ * @param proposalHandle - Handle for the current proposal.
249
+ * @param referenceSequenceNumber - reference sequence number of sent summary.
250
+ */
290
251
  protected refreshLatestSummaryFromPending(
291
252
  proposalHandle: string,
292
253
  referenceSequenceNumber: number,
@@ -312,7 +273,6 @@ export class SummarizerNode implements IRootSummarizerNode {
312
273
  this.refreshLatestSummaryCore(referenceSequenceNumber);
313
274
 
314
275
  this._latestSummary = summaryNode;
315
-
316
276
  // Propagate update to all child nodes
317
277
  for (const child of this.children.values()) {
318
278
  child.refreshLatestSummaryFromPending(proposalHandle, referenceSequenceNumber);
@@ -334,15 +294,14 @@ export class SummarizerNode implements IRootSummarizerNode {
334
294
 
335
295
  this.refreshLatestSummaryCore(referenceSequenceNumber);
336
296
 
337
- const { baseSummary, pathParts } = decodeSummary(snapshotTree, correlatedSummaryLogger);
338
-
339
297
  this._latestSummary = new SummaryNode({
340
298
  referenceSequenceNumber,
341
299
  basePath,
342
300
  localPath,
343
301
  });
344
302
 
345
- const { childrenTree, childrenPathPart } = parseSummaryForSubtrees(baseSummary);
303
+ const pathParts: string[] = [];
304
+ const { childrenTree, childrenPathPart } = parseSummaryForSubtrees(snapshotTree);
346
305
  if (childrenPathPart !== undefined) {
347
306
  pathParts.push(childrenPathPart);
348
307
  }
@@ -376,14 +335,6 @@ export class SummarizerNode implements IRootSummarizerNode {
376
335
  this.pendingSummaries.delete(key);
377
336
  }
378
337
  }
379
-
380
- // Clear earlier outstanding ops
381
- while (
382
- this.outstandingOps.length > 0
383
- && this.outstandingOps[0].sequenceNumber <= referenceSequenceNumber
384
- ) {
385
- this.outstandingOps.shift();
386
- }
387
338
  }
388
339
 
389
340
  public loadBaseSummaryWithoutDifferential(snapshot: ISnapshotTree) {
@@ -398,46 +349,22 @@ export class SummarizerNode implements IRootSummarizerNode {
398
349
  public async loadBaseSummary(
399
350
  snapshot: ISnapshotTree,
400
351
  readAndParseBlob: ReadAndParseBlob,
401
- ): Promise<{ baseSummary: ISnapshotTree; outstandingOps: ISequencedDocumentMessage[]; }> {
402
- const decodedSummary = decodeSummary(snapshot, this.defaultLogger);
403
- const outstandingOps = await decodedSummary.getOutstandingOps(readAndParseBlob);
404
-
405
- const { childrenPathPart } = parseSummaryForSubtrees(decodedSummary.baseSummary);
352
+ ): Promise<ISnapshotTree> {
353
+ const pathParts: string[] = [];
354
+ const { childrenPathPart } = parseSummaryForSubtrees(snapshot);
406
355
  if (childrenPathPart !== undefined) {
407
- decodedSummary.pathParts.push(childrenPathPart);
408
- }
409
-
410
- if (decodedSummary.pathParts.length > 0 && this._latestSummary !== undefined) {
411
- this._latestSummary.additionalPath = EscapedPath.createAndConcat(decodedSummary.pathParts);
356
+ pathParts.push(childrenPathPart);
412
357
  }
413
358
 
414
- // Defensive assertion: tracking number should already exceed this number.
415
- // This is probably a little excessive; can remove when stable.
416
- if (outstandingOps.length > 0) {
417
- const newOpsLatestSeq = outstandingOps[outstandingOps.length - 1].sequenceNumber;
418
- assert(
419
- newOpsLatestSeq <= this.trackingSequenceNumber,
420
- 0x1a9 /* "When loading base summary, expected outstanding ops <= tracking sequence number" */,
421
- );
359
+ if (pathParts.length > 0 && this._latestSummary !== undefined) {
360
+ this._latestSummary.additionalPath = EscapedPath.createAndConcat(pathParts);
422
361
  }
423
362
 
424
- return {
425
- baseSummary: decodedSummary.baseSummary,
426
- outstandingOps,
427
- };
363
+ return snapshot;
428
364
  }
429
365
 
430
366
  public recordChange(op: ISequencedDocumentMessage): void {
431
- const lastOp = this.outstandingOps[this.outstandingOps.length - 1];
432
- if (lastOp !== undefined) {
433
- assert(
434
- lastOp.sequenceNumber < op.sequenceNumber,
435
- 0x1aa /* Out of order change recorded */,
436
- );
437
- }
438
367
  this.invalidate(op.sequenceNumber);
439
- this.trackingSequenceNumber = op.sequenceNumber;
440
- this.outstandingOps.push(op);
441
368
  }
442
369
 
443
370
  public invalidate(sequenceNumber: number): void {
@@ -460,13 +387,6 @@ export class SummarizerNode implements IRootSummarizerNode {
460
387
  }
461
388
 
462
389
  private readonly canReuseHandle: boolean;
463
- private readonly throwOnError: boolean;
464
- /**
465
- * Sequence number of latest tracked op. This updates during recordChange,
466
- * but not for invalidate since we don't have the op. If this drifts from
467
- * changeSequenceNumber and we try to create a differential summary we assert.
468
- */
469
- private trackingSequenceNumber: number;
470
390
 
471
391
  /**
472
392
  * Do not call constructor directly.
@@ -483,11 +403,6 @@ export class SummarizerNode implements IRootSummarizerNode {
483
403
  protected wipSummaryLogger?: ITelemetryLogger,
484
404
  ) {
485
405
  this.canReuseHandle = config.canReuseHandle ?? true;
486
- // BUGBUG: Seeing issues with differential summaries.
487
- // this will disable them, and throw instead
488
- // while we continue to investigate
489
- this.throwOnError = true; // config.throwOnFailure ?? false;
490
- this.trackingSequenceNumber = this._changeSequenceNumber;
491
406
  }
492
407
 
493
408
  public createChild(
@@ -517,7 +432,8 @@ export class SummarizerNode implements IRootSummarizerNode {
517
432
  );
518
433
 
519
434
  // There may be additional state that has to be updated in this child. For example, if a summary is being
520
- // tracked, the child's summary tracking state needs to be updated too.
435
+ // tracked, the child's summary tracking state needs to be updated too. Same goes for pendingSummaries we might
436
+ // have outstanding on the parent in case we realize nodes in between Summary Op and Summary Ack.
521
437
  this.maybeUpdateChildState(child);
522
438
 
523
439
  this.children.set(id, child);
@@ -620,6 +536,8 @@ export class SummarizerNode implements IRootSummarizerNode {
620
536
  /**
621
537
  * Updates the state of the child if required. For example, if a summary is currently being tracked, the child's
622
538
  * summary tracking state needs to be updated too.
539
+ * Also, in case a child node gets realized in between Summary Op and Summary Ack, let's initialize the child's
540
+ * pending summary as well.
623
541
  * @param child - The child node whose state is to be updated.
624
542
  */
625
543
  protected maybeUpdateChildState(child: SummarizerNode) {
@@ -628,8 +546,23 @@ export class SummarizerNode implements IRootSummarizerNode {
628
546
  if (this.isTrackingInProgress()) {
629
547
  child.wipReferenceSequenceNumber = this.wipReferenceSequenceNumber;
630
548
  }
549
+ // In case we have pending summaries on the parent, let's initialize it on the child.
550
+ if (child._latestSummary !== undefined) {
551
+ for (const [key, value] of this.pendingSummaries.entries()) {
552
+ const newLatestSummaryNode = new SummaryNode({
553
+ referenceSequenceNumber: value.referenceSequenceNumber,
554
+ basePath: child._latestSummary.basePath,
555
+ localPath: child._latestSummary.localPath,
556
+ });
557
+
558
+ child.addPendingSummary(key, newLatestSummaryNode);
559
+ }
560
+ }
631
561
  }
632
562
 
563
+ protected addPendingSummary(key: string, summary: SummaryNode) {
564
+ this.pendingSummaries.set(key, summary);
565
+ }
633
566
  /**
634
567
  * Tells whether summary tracking is in progress. True if "startSummary" API is called before summarize.
635
568
  */
@@ -4,22 +4,14 @@
4
4
  */
5
5
 
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
- import { assert } from "@fluidframework/common-utils";
8
7
  import {
9
8
  ISnapshotTree,
10
- ISequencedDocumentMessage,
11
- SummaryType,
12
9
  ISummaryTree,
13
10
  SummaryObject,
14
11
  } from "@fluidframework/protocol-definitions";
15
12
  import { channelsTreeName, ISummaryTreeWithStats } from "@fluidframework/runtime-definitions";
16
- import { SummaryTreeBuilder } from "../summaryUtils";
17
13
  import { ReadAndParseBlob } from "../utils";
18
14
 
19
- const baseSummaryTreeKey = "_baseSummary";
20
- const outstandingOpsBlobKey = "_outstandingOps";
21
- const maxDecodeDepth = 100;
22
-
23
15
  /**
24
16
  * Return value of refreshSummaryAck function. There can be three different scenarios based on the passed params:
25
17
  * 1. The latest summary was not udpated.
@@ -137,88 +129,6 @@ export class SummaryNode {
137
129
  }
138
130
  }
139
131
 
140
- /** Result from decoding summary which may have been a differential summary. */
141
- interface IDecodedSummary {
142
- /** The innermost base summary which is not itself a differential summary */
143
- readonly baseSummary: ISnapshotTree;
144
- /** The entire path name to the innermost base summary */
145
- readonly pathParts: string[];
146
- /** Function to fetch all outstanding ops since the innermost base summary */
147
- getOutstandingOps(readAndParseBlob: ReadAndParseBlob): Promise<ISequencedDocumentMessage[]>;
148
- }
149
-
150
- /**
151
- * Checks if the snapshot is created by referencing a previous successful
152
- * summary plus outstanding ops. If so, it will recursively "decode" it until
153
- * it gets to the last successful summary (the base summary) and returns that
154
- * as well as a function for fetching the outstanding ops. Also returns the
155
- * full path to the previous base summary for child summarizer nodes to use as
156
- * their base path when necessary.
157
- * @param snapshot - snapshot tree to decode
158
- */
159
- export function decodeSummary(
160
- snapshot: ISnapshotTree,
161
- logger: Pick<ITelemetryLogger, "sendTelemetryEvent">,
162
- ): IDecodedSummary {
163
- let baseSummary = snapshot;
164
- const pathParts: string[] = [];
165
- const opsBlobs: string[] = [];
166
-
167
- for (let i = 0; ; i++) {
168
- if (i > maxDecodeDepth) {
169
- logger.sendTelemetryEvent({
170
- eventName: "DecodeSummaryMaxDepth",
171
- maxDecodeDepth,
172
- });
173
- }
174
- const outstandingOpsBlob = baseSummary.blobs[outstandingOpsBlobKey];
175
- const newBaseSummary = baseSummary.trees[baseSummaryTreeKey];
176
- if (outstandingOpsBlob === undefined && newBaseSummary === undefined) {
177
- return {
178
- baseSummary,
179
- pathParts,
180
- async getOutstandingOps(readAndParseBlob: ReadAndParseBlob) {
181
- let outstandingOps: ISequencedDocumentMessage[] = [];
182
- for (const opsBlob of opsBlobs) {
183
- const newOutstandingOps = await readAndParseBlob<ISequencedDocumentMessage[]>(opsBlob);
184
- if (outstandingOps.length > 0 && newOutstandingOps.length > 0) {
185
- const latestSeq = outstandingOps[outstandingOps.length - 1].sequenceNumber;
186
- const newEarliestSeq = newOutstandingOps[0].sequenceNumber;
187
- if (newEarliestSeq <= latestSeq) {
188
- logger.sendTelemetryEvent({
189
- eventName: "DuplicateOutstandingOps",
190
- // eslint-disable-next-line max-len
191
- message: `newEarliestSeq <= latestSeq in decodeSummary: ${newEarliestSeq} <= ${latestSeq}`,
192
- });
193
- while (newOutstandingOps.length > 0
194
- && newOutstandingOps[0].sequenceNumber <= latestSeq) {
195
- newOutstandingOps.shift();
196
- }
197
- }
198
- }
199
- outstandingOps = outstandingOps.concat(newOutstandingOps);
200
- }
201
- return outstandingOps;
202
- },
203
- };
204
- }
205
-
206
- assert(!!outstandingOpsBlob, 0x1af /* "Outstanding ops blob missing, but base summary tree exists" */);
207
- assert(newBaseSummary !== undefined, 0x1b0 /* "Base summary tree missing, but outstanding ops blob exists" */);
208
- baseSummary = newBaseSummary;
209
- pathParts.push(baseSummaryTreeKey);
210
- opsBlobs.unshift(outstandingOpsBlob);
211
- }
212
- }
213
-
214
- /**
215
- * Summary tree which is a handle of the previous successfully acked summary
216
- * and a blob of the outstanding ops since that summary.
217
- */
218
- interface IEncodedSummary extends ISummaryTreeWithStats {
219
- readonly additionalPath: EscapedPath;
220
- }
221
-
222
132
  /**
223
133
  * Parameter to help encode summary with conditional behavior.
224
134
  * When fromSummary is true, it will contain the SummaryNode of
@@ -234,42 +144,6 @@ export type EncodeSummaryParam = {
234
144
  initialSummary: ISummaryTreeWithStats;
235
145
  };
236
146
 
237
- /**
238
- * Creates a summary tree which is a handle of the previous successfully acked summary
239
- * and a blob of the outstanding ops since that summary. If there is no acked summary yet,
240
- * it will create with the tree found in the initial attach op and the blob of outstanding ops.
241
- * @param summaryParam - information about last acked summary and paths to encode if from summary,
242
- * otherwise the initial summary from the attach op.
243
- * @param outstandingOps - outstanding ops since last acked summary
244
- */
245
- export function encodeSummary(
246
- summaryParam: EncodeSummaryParam,
247
- outstandingOps: ISequencedDocumentMessage[],
248
- ): IEncodedSummary {
249
- let additionalPath = EscapedPath.create(baseSummaryTreeKey);
250
-
251
- const builder = new SummaryTreeBuilder();
252
- builder.addBlob(outstandingOpsBlobKey, JSON.stringify(outstandingOps));
253
-
254
- if (summaryParam.fromSummary) {
255
- // Create using handle of latest acked summary
256
- const summaryNode = summaryParam.summaryNode;
257
- if (summaryNode.additionalPath !== undefined) {
258
- additionalPath = additionalPath.concat(summaryNode.additionalPath);
259
- }
260
- builder.addHandle(baseSummaryTreeKey, SummaryType.Tree, summaryNode.fullPath.path);
261
- } else {
262
- // Create using initial summary from attach op
263
- builder.addWithStats(baseSummaryTreeKey, summaryParam.initialSummary);
264
- }
265
-
266
- const summary = builder.getSummaryTree();
267
- return {
268
- ...summary,
269
- additionalPath,
270
- };
271
- }
272
-
273
147
  /**
274
148
  * Information about the initial summary tree found from an attach op.
275
149
  */