@fluidframework/container-runtime 1.2.3-84921 → 1.2.5

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.
@@ -2534,21 +2534,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2534
2534
  },
2535
2535
  );
2536
2536
 
2537
+ let latestSnapshotVersionId: string | undefined;
2537
2538
  if (refreshLatestAck) {
2538
- const latestSummaryRefSeq = await this.refreshLatestSummaryAckFromServer(
2539
+ const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(
2539
2540
  ChildLogger.create(summaryNumberLogger, undefined, { all: { safeSummary: true } }));
2541
+ const latestSnapshotRefSeq = latestSnapshotInfo.latestSnapshotRefSeq;
2542
+ latestSnapshotVersionId = latestSnapshotInfo.latestSnapshotVersionId;
2540
2543
 
2541
- if (latestSummaryRefSeq > this.deltaManager.lastSequenceNumber) {
2544
+ if (latestSnapshotRefSeq > this.deltaManager.lastSequenceNumber) {
2542
2545
  // We need to catch up to the latest summary's reference sequence number before pausing.
2543
2546
  await PerformanceEvent.timedExecAsync(
2544
2547
  summaryNumberLogger,
2545
2548
  {
2546
2549
  eventName: "WaitingForSeq",
2547
2550
  lastSequenceNumber: this.deltaManager.lastSequenceNumber,
2548
- targetSequenceNumber: latestSummaryRefSeq,
2551
+ targetSequenceNumber: latestSnapshotRefSeq,
2549
2552
  lastKnownSeqNumber: this.deltaManager.lastKnownSeqNumber,
2550
2553
  },
2551
- async () => waitForSeq(this.deltaManager, latestSummaryRefSeq),
2554
+ async () => waitForSeq(this.deltaManager, latestSnapshotRefSeq),
2552
2555
  { start: true, end: true, cancel: "error" }, // definitely want start event
2553
2556
  );
2554
2557
  }
@@ -2673,19 +2676,33 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2673
2676
  return { stage: "generate", ...generateSummaryData, error: continueResult.error };
2674
2677
  }
2675
2678
 
2679
+ // It may happen that the lastAck it not correct due to missing summaryAck in case of single commit
2680
+ // summary. So if the previous summarizer closes just after submitting the summary and before
2681
+ // submitting the summaryOp then we can't rely on summaryAck. So in case we have
2682
+ // latestSnapshotVersionId from storage and it does not match with the lastAck ackHandle, then use
2683
+ // the one fetched from storage as parent as that is the latest.
2676
2684
  const lastAck = this.summaryCollection.latestAck;
2677
- const summaryContext: ISummaryContext =
2678
- lastAck === undefined
2679
- ? {
2680
- proposalHandle: undefined,
2681
- ackHandle: this.context.getLoadedFromVersion()?.id,
2682
- referenceSequenceNumber: summaryRefSeqNum,
2683
- }
2684
- : {
2685
- proposalHandle: lastAck.summaryOp.contents.handle,
2686
- ackHandle: lastAck.summaryAck.contents.handle,
2687
- referenceSequenceNumber: summaryRefSeqNum,
2688
- };
2685
+ let summaryContext: ISummaryContext;
2686
+ if (lastAck?.summaryAck.contents.handle !== latestSnapshotVersionId
2687
+ && latestSnapshotVersionId !== undefined) {
2688
+ summaryContext = {
2689
+ proposalHandle: undefined,
2690
+ ackHandle: latestSnapshotVersionId,
2691
+ referenceSequenceNumber: summaryRefSeqNum,
2692
+ };
2693
+ } else if (lastAck === undefined) {
2694
+ summaryContext = {
2695
+ proposalHandle: undefined,
2696
+ ackHandle: this.context.getLoadedFromVersion()?.id,
2697
+ referenceSequenceNumber: summaryRefSeqNum,
2698
+ };
2699
+ } else {
2700
+ summaryContext = {
2701
+ proposalHandle: lastAck.summaryOp.contents.handle,
2702
+ ackHandle: lastAck.summaryAck.contents.handle,
2703
+ referenceSequenceNumber: summaryRefSeqNum,
2704
+ };
2705
+ }
2689
2706
 
2690
2707
  let handle: string;
2691
2708
  try {
@@ -3039,15 +3056,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3039
3056
  summaryLogger: ITelemetryLogger,
3040
3057
  ) {
3041
3058
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
3042
- const result = await this.summarizerNode.refreshLatestSummary(
3043
- proposalHandle,
3044
- summaryRefSeq,
3045
- async () => this.fetchSnapshotFromStorage(ackHandle, summaryLogger, {
3059
+ const { snapshotTree } = await this.fetchSnapshotFromStorage(
3060
+ ackHandle,
3061
+ summaryLogger,
3062
+ {
3046
3063
  eventName: "RefreshLatestSummaryGetSnapshot",
3047
3064
  ackHandle,
3048
3065
  summaryRefSeq,
3049
3066
  fetchLatest: false,
3050
- }),
3067
+ },
3068
+ );
3069
+ const result = await this.summarizerNode.refreshLatestSummary(
3070
+ proposalHandle,
3071
+ summaryRefSeq,
3072
+ async () => snapshotTree,
3051
3073
  readAndParseBlob,
3052
3074
  summaryLogger,
3053
3075
  );
@@ -3062,19 +3084,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3062
3084
  * @param summaryLogger - logger to use when fetching snapshot from storage
3063
3085
  * @returns downloaded snapshot's reference sequence number
3064
3086
  */
3065
- private async refreshLatestSummaryAckFromServer(summaryLogger: ITelemetryLogger): Promise<number> {
3066
- const snapshot = await this.fetchSnapshotFromStorage(null, summaryLogger, {
3067
- eventName: "RefreshLatestSummaryGetSnapshot",
3068
- fetchLatest: true,
3069
- });
3087
+ private async refreshLatestSummaryAckFromServer(
3088
+ summaryLogger: ITelemetryLogger,
3089
+ ): Promise<{ latestSnapshotRefSeq: number; latestSnapshotVersionId: string | undefined; }> {
3090
+ const { snapshotTree, versionId } = await this.fetchSnapshotFromStorage(null, summaryLogger, {
3091
+ eventName: "RefreshLatestSummaryGetSnapshot",
3092
+ fetchLatest: true,
3093
+ },
3094
+ );
3070
3095
 
3071
3096
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
3072
- const snapshotRefSeq = await seqFromTree(snapshot, readAndParseBlob);
3097
+ const latestSnapshotRefSeq = await seqFromTree(snapshotTree, readAndParseBlob);
3073
3098
 
3074
3099
  const result = await this.summarizerNode.refreshLatestSummary(
3075
3100
  undefined,
3076
- snapshotRefSeq,
3077
- async () => snapshot,
3101
+ latestSnapshotRefSeq,
3102
+ async () => snapshotTree,
3078
3103
  readAndParseBlob,
3079
3104
  summaryLogger,
3080
3105
  );
@@ -3082,11 +3107,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3082
3107
  // Notify the garbage collector so it can update its latest summary state.
3083
3108
  await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
3084
3109
 
3085
- return snapshotRefSeq;
3110
+ return { latestSnapshotRefSeq, latestSnapshotVersionId: versionId };
3086
3111
  }
3087
3112
 
3088
3113
  private async fetchSnapshotFromStorage(
3089
- versionId: string | null, logger: ITelemetryLogger, event: ITelemetryGenericEvent) {
3114
+ versionId: string | null,
3115
+ logger: ITelemetryLogger,
3116
+ event: ITelemetryGenericEvent,
3117
+ ): Promise<{ snapshotTree: ISnapshotTree; versionId: string; }> {
3090
3118
  return PerformanceEvent.timedExecAsync(
3091
3119
  logger, event, async (perfEvent: {
3092
3120
  end: (arg0: {
@@ -3106,7 +3134,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3106
3134
  stats.getSnapshotDuration = trace.trace().duration;
3107
3135
 
3108
3136
  perfEvent.end(stats);
3109
- return maybeSnapshot;
3137
+ return { snapshotTree: maybeSnapshot, versionId: versions[0].id };
3110
3138
  });
3111
3139
  }
3112
3140
 
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "1.2.3-84921";
9
+ export const pkgVersion = "1.2.5";
@@ -78,10 +78,10 @@ export class RunningSummarizer implements IDisposable {
78
78
  }
79
79
 
80
80
  public get disposed() { return this._disposed; }
81
-
82
81
  private stopping = false;
83
82
  private _disposed = false;
84
83
  private summarizingLock: Promise<void> | undefined;
84
+ private refreshSummaryAckLock: Promise<void> | undefined;
85
85
  private tryWhileSummarizing = false;
86
86
  private readonly pendingAckTimer: PromiseTimer;
87
87
  private heuristicRunner?: ISummarizeHeuristicRunner;
@@ -276,6 +276,31 @@ export class RunningSummarizer implements IDisposable {
276
276
  }
277
277
  }
278
278
 
279
+ /**
280
+ * Blocks a new summarizer from running in case RefreshSummaryAck is being processed.
281
+ * Assumes that caller checked upfront for lack of concurrent action (this.refreshSummaryAckLock)
282
+ * before calling this API. I.e. caller is responsible for either erroring out or waiting on this promise.
283
+ * Note: The refreshSummaryAckLock makes sure no summarizer gets enqueued or processed
284
+ * until the refresh has completed. One can't rely uniquely on the summarizingLock as the
285
+ * refreshLatestSummaryAck also happens during the time summarizingLock !== undefined.
286
+ * Ex. Summarizer submits a summay + op and then waits for the Summary Ack to proceed
287
+ * with the refreshLatestSummaryAck and complete the summary.
288
+ * @param action - action to perform.
289
+ * @returns - result of action.
290
+ */
291
+ public async lockedRefreshSummaryAckAction<T>(action: () => Promise<T>) {
292
+ assert(this.refreshSummaryAckLock === undefined,
293
+ "Refresh Summary Ack - Caller is responsible for checking lock");
294
+
295
+ const refreshSummaryAckLock = new Deferred<void>();
296
+ this.refreshSummaryAckLock = refreshSummaryAckLock.promise;
297
+
298
+ return action().finally(() => {
299
+ refreshSummaryAckLock.resolve();
300
+ this.refreshSummaryAckLock = undefined;
301
+ });
302
+ }
303
+
279
304
  /**
280
305
  * Runs single summary action that prevents any other concurrent actions.
281
306
  * Assumes that caller checked upfront for lack of concurrent action (this.summarizingLock)
@@ -290,6 +315,8 @@ export class RunningSummarizer implements IDisposable {
290
315
  this.summarizingLock = summarizingLock.promise;
291
316
 
292
317
  this.summarizeCount++;
318
+ // Make sure the RefreshLatestSummaryAck is not being executed.
319
+ await this.refreshSummaryAckLock;
293
320
 
294
321
  return action().finally(() => {
295
322
  summarizingLock.resolve();
@@ -393,6 +420,10 @@ export class RunningSummarizer implements IDisposable {
393
420
  });
394
421
  await delay(delaySeconds * 1000);
395
422
  }
423
+
424
+ // Make sure the refresh Summary Ack is not being executed.
425
+ await this.refreshSummaryAckLock;
426
+
396
427
  // Note: no need to account for cancellationToken.waitCancelled here, as
397
428
  // this is accounted SummaryGenerator.summarizeCore that controls receivedSummaryAckOrNack.
398
429
  const resultSummarize = this.generator.summarize(summarizeProps, options, cancellationToken);
@@ -441,6 +472,7 @@ export class RunningSummarizer implements IDisposable {
441
472
  // The heuristics are blocking concurrent summarize attempts.
442
473
  throw new UsageError("Attempted to run an already-running summarizer on demand");
443
474
  }
475
+
444
476
  const result = this.trySummarizeOnce(
445
477
  { reason: `onDemand/${reason}` },
446
478
  options,
package/src/summarizer.ts CHANGED
@@ -8,9 +8,15 @@ import { Deferred } from "@fluidframework/common-utils";
8
8
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
9
9
  import { ILoader, LoaderHeader } from "@fluidframework/container-definitions";
10
10
  import { UsageError } from "@fluidframework/container-utils";
11
- import { DriverHeader } from "@fluidframework/driver-definitions";
11
+ import { DriverErrorType, 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
+ isFluidError,
17
+ LoggingError,
18
+ wrapErrorAndLog,
19
+ } from "@fluidframework/telemetry-utils";
14
20
  import {
15
21
  FluidObject,
16
22
  IFluidHandleContext,
@@ -23,7 +29,7 @@ import {
23
29
  import { ISummaryConfiguration } from "./containerRuntime";
24
30
  import { ICancellableSummarizerController } from "./runWhileConnectedCoordinator";
25
31
  import { summarizerClientType } from "./summarizerClientElection";
26
- import { SummaryCollection } from "./summaryCollection";
32
+ import { IAckedSummary, SummaryCollection } from "./summaryCollection";
27
33
  import { SummarizerHandle } from "./summarizerHandle";
28
34
  import { RunningSummarizer } from "./runningSummarizer";
29
35
  import {
@@ -378,22 +384,52 @@ export class Summarizer extends EventEmitter implements ISummarizer {
378
384
 
379
385
  private async handleSummaryAcks() {
380
386
  let refSequenceNumber = this.runtime.deltaManager.initialSequenceNumber;
387
+ let ack: IAckedSummary | undefined;
381
388
  while (this.runningSummarizer) {
382
389
  const summaryLogger = this.runningSummarizer.tryGetCorrelatedLogger(refSequenceNumber) ?? this.logger;
383
390
  try {
384
- const ack = await this.summaryCollection.waitSummaryAck(refSequenceNumber);
391
+ // Initialize ack with undefined if exception happens inside of waitSummaryAck on second iteration,
392
+ // we record undefined, not previous handles.
393
+ ack = undefined;
394
+ ack = await this.summaryCollection.waitSummaryAck(refSequenceNumber);
385
395
  refSequenceNumber = ack.summaryOp.referenceSequenceNumber;
386
-
387
- await this.internalsProvider.refreshLatestSummaryAck(
388
- ack.summaryOp.contents.handle,
389
- ack.summaryAck.contents.handle,
390
- refSequenceNumber,
391
- summaryLogger,
396
+ const summaryOpHandle = ack.summaryOp.contents.handle;
397
+ const summaryAckHandle = ack.summaryAck.contents.handle;
398
+ // Make sure we block any summarizer from being executed/enqueued while
399
+ // executing the refreshLatestSummaryAck.
400
+ // https://dev.azure.com/fluidframework/internal/_workitems/edit/779
401
+ await this.runningSummarizer.lockedRefreshSummaryAckAction(async () =>
402
+ this.internalsProvider.refreshLatestSummaryAck(
403
+ summaryOpHandle,
404
+ summaryAckHandle,
405
+ refSequenceNumber,
406
+ summaryLogger,
407
+ ).catch(async (error) => {
408
+ // If the error is 404, so maybe the fetched version no longer exists on server. We just
409
+ // ignore this error in that case, as that means we will have another summaryAck for the
410
+ // latest version with which we will refresh the state. However in case of single commit
411
+ // summary, we might me missing a summary ack, so in that case we are still fine as the
412
+ // code in `submitSummary` function in container runtime, will refresh the latest state
413
+ // by calling `refreshLatestSummaryAckFromServer` and we will be fine.
414
+ if (isFluidError(error)
415
+ && error.errorType === DriverErrorType.fileNotFoundOrAccessDeniedError) {
416
+ summaryLogger.sendTelemetryEvent({
417
+ eventName: "HandleSummaryAckErrorIgnored",
418
+ referenceSequenceNumber: refSequenceNumber,
419
+ proposalHandle: summaryOpHandle,
420
+ ackHandle: summaryAckHandle,
421
+ }, error);
422
+ } else {
423
+ throw error;
424
+ }
425
+ }),
392
426
  );
393
427
  } catch (error) {
394
428
  summaryLogger.sendErrorEvent({
395
429
  eventName: "HandleSummaryAckError",
396
430
  referenceSequenceNumber: refSequenceNumber,
431
+ handle: ack?.summaryOp?.contents?.handle,
432
+ ackHandle: ack?.summaryAck?.contents?.handle,
397
433
  }, error);
398
434
  }
399
435
  refSequenceNumber++;