@fluidframework/container-runtime 0.59.4001 → 0.59.4003

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.
@@ -61,6 +61,7 @@ import {
61
61
  IQuorumClients,
62
62
  ISequencedDocumentMessage,
63
63
  ISignalMessage,
64
+ ISnapshotTree,
64
65
  ISummaryConfiguration,
65
66
  ISummaryContent,
66
67
  ISummaryTree,
@@ -2245,21 +2246,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2245
2246
  },
2246
2247
  );
2247
2248
 
2249
+ let latestSnapshotVersionId: string | undefined;
2248
2250
  if (refreshLatestAck) {
2249
- const latestSummaryRefSeq = await this.refreshLatestSummaryAckFromServer(
2251
+ const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(
2250
2252
  ChildLogger.create(summaryNumberLogger, undefined, { all: { safeSummary: true } }));
2253
+ const latestSnapshotRefSeq = latestSnapshotInfo.latestSnapshotRefSeq;
2254
+ latestSnapshotVersionId = latestSnapshotInfo.latestSnapshotVersionId;
2251
2255
 
2252
- if (latestSummaryRefSeq > this.deltaManager.lastSequenceNumber) {
2256
+ if (latestSnapshotRefSeq > this.deltaManager.lastSequenceNumber) {
2253
2257
  // We need to catch up to the latest summary's reference sequence number before pausing.
2254
2258
  await PerformanceEvent.timedExecAsync(
2255
2259
  summaryNumberLogger,
2256
2260
  {
2257
2261
  eventName: "WaitingForSeq",
2258
2262
  lastSequenceNumber: this.deltaManager.lastSequenceNumber,
2259
- targetSequenceNumber: latestSummaryRefSeq,
2263
+ targetSequenceNumber: latestSnapshotRefSeq,
2260
2264
  lastKnownSeqNumber: this.deltaManager.lastKnownSeqNumber,
2261
2265
  },
2262
- async () => waitForSeq(this.deltaManager, latestSummaryRefSeq),
2266
+ async () => waitForSeq(this.deltaManager, latestSnapshotRefSeq),
2263
2267
  { start: true, end: true, cancel: "error" }, // definitely want start event
2264
2268
  );
2265
2269
  }
@@ -2384,19 +2388,33 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2384
2388
  return { stage: "generate", ...generateSummaryData, error: continueResult.error };
2385
2389
  }
2386
2390
 
2391
+ // It may happen that the lastAck it not correct due to missing summaryAck in case of single commit
2392
+ // summary. So if the previous summarizer closes just after submitting the summary and before
2393
+ // submitting the summaryOp then we can't rely on summaryAck. So in case we have
2394
+ // latestSnapshotVersionId from storage and it does not match with the lastAck ackHandle, then use
2395
+ // the one fetched from storage as parent as that is the latest.
2387
2396
  const lastAck = this.summaryCollection.latestAck;
2388
- const summaryContext: ISummaryContext =
2389
- lastAck === undefined
2390
- ? {
2397
+ let summaryContext: ISummaryContext;
2398
+ if (lastAck?.summaryAck.contents.handle !== latestSnapshotVersionId
2399
+ && latestSnapshotVersionId !== undefined) {
2400
+ summaryContext = {
2401
+ proposalHandle: undefined,
2402
+ ackHandle: latestSnapshotVersionId,
2403
+ referenceSequenceNumber: summaryRefSeqNum,
2404
+ };
2405
+ } else if (lastAck === undefined) {
2406
+ summaryContext = {
2391
2407
  proposalHandle: undefined,
2392
2408
  ackHandle: this.context.getLoadedFromVersion()?.id,
2393
2409
  referenceSequenceNumber: summaryRefSeqNum,
2394
- }
2395
- : {
2410
+ };
2411
+ } else {
2412
+ summaryContext = {
2396
2413
  proposalHandle: lastAck.summaryOp.contents.handle,
2397
2414
  ackHandle: lastAck.summaryAck.contents.handle,
2398
2415
  referenceSequenceNumber: summaryRefSeqNum,
2399
2416
  };
2417
+ }
2400
2418
 
2401
2419
  let handle: string;
2402
2420
  try {
@@ -2737,20 +2755,29 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2737
2755
  summaryLogger: ITelemetryLogger,
2738
2756
  ) {
2739
2757
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
2740
- const result = await this.summarizerNode.refreshLatestSummary(
2741
- proposalHandle,
2742
- summaryRefSeq,
2743
- async () => this.fetchSnapshotFromStorage(ackHandle, summaryLogger, {
2744
- eventName: "RefreshLatestSummaryGetSnapshot",
2745
- ackHandle,
2746
- summaryRefSeq,
2747
- fetchLatest: false,
2748
- }),
2749
- readAndParseBlob,
2750
- summaryLogger,
2751
- );
2752
-
2753
- // Notify the garbage collector so it can update its latest summary state.
2758
+ // The call to fetch the snapshot is very expensive and not always needed.
2759
+ // It should only be done by the summarizerNode, if required.
2760
+ const snapshotTreeFetcher = async () => {
2761
+ const fetchResult = await this.fetchSnapshotFromStorage(
2762
+ ackHandle,
2763
+ summaryLogger,
2764
+ {
2765
+ eventName: "RefreshLatestSummaryGetSnapshot",
2766
+ ackHandle,
2767
+ summaryRefSeq,
2768
+ fetchLatest: false,
2769
+ });
2770
+ return fetchResult.snapshotTree;
2771
+ };
2772
+ const result = await this.summarizerNode.refreshLatestSummary(
2773
+ proposalHandle,
2774
+ summaryRefSeq,
2775
+ snapshotTreeFetcher,
2776
+ readAndParseBlob,
2777
+ summaryLogger,
2778
+ );
2779
+
2780
+ // Notify the garbage collector so it can update its latest summary state.
2754
2781
  await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
2755
2782
  }
2756
2783
 
@@ -2760,19 +2787,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2760
2787
  * @param summaryLogger - logger to use when fetching snapshot from storage
2761
2788
  * @returns downloaded snapshot's reference sequence number
2762
2789
  */
2763
- private async refreshLatestSummaryAckFromServer(summaryLogger: ITelemetryLogger): Promise<number> {
2764
- const snapshot = await this.fetchSnapshotFromStorage(null, summaryLogger, {
2765
- eventName: "RefreshLatestSummaryGetSnapshot",
2766
- fetchLatest: true,
2767
- });
2790
+ private async refreshLatestSummaryAckFromServer(
2791
+ summaryLogger: ITelemetryLogger,
2792
+ ): Promise<{ latestSnapshotRefSeq: number; latestSnapshotVersionId: string | undefined; }> {
2793
+ const { snapshotTree, versionId } = await this.fetchSnapshotFromStorage(null, summaryLogger, {
2794
+ eventName: "RefreshLatestSummaryGetSnapshot",
2795
+ fetchLatest: true,
2796
+ },
2797
+ );
2768
2798
 
2769
2799
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
2770
- const snapshotRefSeq = await seqFromTree(snapshot, readAndParseBlob);
2800
+ const latestSnapshotRefSeq = await seqFromTree(snapshotTree, readAndParseBlob);
2771
2801
 
2772
2802
  const result = await this.summarizerNode.refreshLatestSummary(
2773
2803
  undefined,
2774
- snapshotRefSeq,
2775
- async () => snapshot,
2804
+ latestSnapshotRefSeq,
2805
+ async () => snapshotTree,
2776
2806
  readAndParseBlob,
2777
2807
  summaryLogger,
2778
2808
  );
@@ -2780,11 +2810,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2780
2810
  // Notify the garbage collector so it can update its latest summary state.
2781
2811
  await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
2782
2812
 
2783
- return snapshotRefSeq;
2813
+ return { latestSnapshotRefSeq, latestSnapshotVersionId: versionId };
2784
2814
  }
2785
2815
 
2786
2816
  private async fetchSnapshotFromStorage(
2787
- versionId: string | null, logger: ITelemetryLogger, event: ITelemetryGenericEvent) {
2817
+ versionId: string | null,
2818
+ logger: ITelemetryLogger,
2819
+ event: ITelemetryGenericEvent,
2820
+ ): Promise<{ snapshotTree: ISnapshotTree; versionId: string; }> {
2788
2821
  return PerformanceEvent.timedExecAsync(
2789
2822
  logger, event, async (perfEvent: {
2790
2823
  end: (arg0: {
@@ -2803,7 +2836,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2803
2836
  stats.getSnapshotDuration = trace.trace().duration;
2804
2837
 
2805
2838
  perfEvent.end(stats);
2806
- return maybeSnapshot;
2839
+ return { snapshotTree: maybeSnapshot, versionId: versions[0].id };
2807
2840
  });
2808
2841
  }
2809
2842
 
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "0.59.4001";
9
+ export const pkgVersion = "0.59.4003";
@@ -79,10 +79,10 @@ export class RunningSummarizer implements IDisposable {
79
79
  }
80
80
 
81
81
  public get disposed() { return this._disposed; }
82
-
83
82
  private stopping = false;
84
83
  private _disposed = false;
85
84
  private summarizingLock: Promise<void> | undefined;
85
+ private refreshSummaryAckLock: Promise<void> | undefined;
86
86
  private tryWhileSummarizing = false;
87
87
  private readonly pendingAckTimer: PromiseTimer;
88
88
  private heuristicRunner?: ISummarizeHeuristicRunner;
@@ -270,6 +270,31 @@ export class RunningSummarizer implements IDisposable {
270
270
  }
271
271
  }
272
272
 
273
+ /**
274
+ * Blocks a new summarizer from running in case RefreshSummaryAck is being processed.
275
+ * Assumes that caller checked upfront for lack of concurrent action (this.refreshSummaryAckLock)
276
+ * before calling this API. I.e. caller is responsible for either erroring out or waiting on this promise.
277
+ * Note: The refreshSummaryAckLock makes sure no summarizer gets enqueued or processed
278
+ * until the refresh has completed. One can't rely uniquely on the summarizingLock as the
279
+ * refreshLatestSummaryAck also happens during the time summarizingLock !== undefined.
280
+ * Ex. Summarizer submits a summay + op and then waits for the Summary Ack to proceed
281
+ * with the refreshLatestSummaryAck and complete the summary.
282
+ * @param action - action to perform.
283
+ * @returns - result of action.
284
+ */
285
+ public async lockedRefreshSummaryAckAction<T>(action: () => Promise<T>) {
286
+ assert(this.refreshSummaryAckLock === undefined,
287
+ "Refresh Summary Ack - Caller is responsible for checking lock");
288
+
289
+ const refreshSummaryAckLock = new Deferred<void>();
290
+ this.refreshSummaryAckLock = refreshSummaryAckLock.promise;
291
+
292
+ return action().finally(() => {
293
+ refreshSummaryAckLock.resolve();
294
+ this.refreshSummaryAckLock = undefined;
295
+ });
296
+ }
297
+
273
298
  /**
274
299
  * Runs single summary action that prevents any other concurrent actions.
275
300
  * Assumes that caller checked upfront for lack of concurrent action (this.summarizingLock)
@@ -284,6 +309,8 @@ export class RunningSummarizer implements IDisposable {
284
309
  this.summarizingLock = summarizingLock.promise;
285
310
 
286
311
  this.summarizeCount++;
312
+ // Make sure the RefreshLatestSummaryAck is not being executed.
313
+ await this.refreshSummaryAckLock;
287
314
 
288
315
  return action().finally(() => {
289
316
  summarizingLock.resolve();
@@ -383,6 +410,10 @@ export class RunningSummarizer implements IDisposable {
383
410
  });
384
411
  await delay(delaySeconds * 1000);
385
412
  }
413
+
414
+ // Make sure the refresh Summary Ack is not being executed.
415
+ await this.refreshSummaryAckLock;
416
+
386
417
  // Note: no need to account for cancellationToken.waitCancelled here, as
387
418
  // this is accounted SummaryGenerator.summarizeCore that controls receivedSummaryAckOrNack.
388
419
  const resultSummarize = this.generator.summarize(summarizeProps, options, cancellationToken);
@@ -431,6 +462,7 @@ export class RunningSummarizer implements IDisposable {
431
462
  // The heuristics are blocking concurrent summarize attempts.
432
463
  throw new UsageError("Attempted to run an already-running summarizer on demand");
433
464
  }
465
+
434
466
  const result = this.trySummarizeOnce(
435
467
  { reason: `onDemand/${reason}` },
436
468
  options,
package/src/summarizer.ts CHANGED
@@ -10,7 +10,12 @@ import { ILoader, LoaderHeader } from "@fluidframework/container-definitions";
10
10
  import { UsageError } from "@fluidframework/container-utils";
11
11
  import { DriverHeader } from "@fluidframework/driver-definitions";
12
12
  import { requestFluidObject } from "@fluidframework/runtime-utils";
13
- import { ChildLogger, IFluidErrorBase, LoggingError, wrapErrorAndLog } from "@fluidframework/telemetry-utils";
13
+ import {
14
+ ChildLogger,
15
+ IFluidErrorBase,
16
+ LoggingError,
17
+ wrapErrorAndLog,
18
+ } from "@fluidframework/telemetry-utils";
14
19
  import {
15
20
  FluidObject,
16
21
  IFluidHandleContext,
@@ -23,7 +28,7 @@ import {
23
28
  } from "@fluidframework/protocol-definitions";
24
29
  import { ICancellableSummarizerController } from "./runWhileConnectedCoordinator";
25
30
  import { summarizerClientType } from "./summarizerClientElection";
26
- import { SummaryCollection } from "./summaryCollection";
31
+ import { IAckedSummary, SummaryCollection } from "./summaryCollection";
27
32
  import { SummarizerHandle } from "./summarizerHandle";
28
33
  import { RunningSummarizer } from "./runningSummarizer";
29
34
  import {
@@ -374,22 +379,33 @@ export class Summarizer extends EventEmitter implements ISummarizer {
374
379
 
375
380
  private async handleSummaryAcks() {
376
381
  let refSequenceNumber = this.runtime.deltaManager.initialSequenceNumber;
382
+ let ack: IAckedSummary | undefined;
377
383
  while (this.runningSummarizer) {
378
384
  const summaryLogger = this.runningSummarizer.tryGetCorrelatedLogger(refSequenceNumber) ?? this.logger;
379
385
  try {
380
- const ack = await this.summaryCollection.waitSummaryAck(refSequenceNumber);
386
+ // Initialize ack with undefined if exception happens inside of waitSummaryAck on second iteration,
387
+ // we record undefined, not previous handles.
388
+ ack = undefined;
389
+ ack = await this.summaryCollection.waitSummaryAck(refSequenceNumber);
381
390
  refSequenceNumber = ack.summaryOp.referenceSequenceNumber;
382
-
383
- await this.internalsProvider.refreshLatestSummaryAck(
384
- ack.summaryOp.contents.handle,
385
- ack.summaryAck.contents.handle,
386
- refSequenceNumber,
387
- summaryLogger,
388
- );
391
+ const summaryOpHandle = ack.summaryOp.contents.handle;
392
+ const summaryAckHandle = ack.summaryAck.contents.handle;
393
+ // Make sure we block any summarizer from being executed/enqueued while
394
+ // executing the refreshLatestSummaryAck.
395
+ // https://dev.azure.com/fluidframework/internal/_workitems/edit/779
396
+ await this.runningSummarizer.lockedRefreshSummaryAckAction(async () =>
397
+ this.internalsProvider.refreshLatestSummaryAck(
398
+ summaryOpHandle,
399
+ summaryAckHandle,
400
+ refSequenceNumber,
401
+ summaryLogger,
402
+ ));
389
403
  } catch (error) {
390
404
  summaryLogger.sendErrorEvent({
391
405
  eventName: "HandleSummaryAckError",
392
406
  referenceSequenceNumber: refSequenceNumber,
407
+ handle: ack?.summaryOp?.contents?.handle,
408
+ ackHandle: ack?.summaryAck?.contents?.handle,
393
409
  }, error);
394
410
  }
395
411
  refSequenceNumber++;