@fluidframework/container-runtime 2.74.0 → 2.80.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.
Files changed (103) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/container-runtime.test-files.tar +0 -0
  3. package/dist/blobManager/blobManager.d.ts +0 -2
  4. package/dist/blobManager/blobManager.d.ts.map +1 -1
  5. package/dist/blobManager/blobManager.js +4 -5
  6. package/dist/blobManager/blobManager.js.map +1 -1
  7. package/dist/channelCollection.d.ts +3 -3
  8. package/dist/channelCollection.d.ts.map +1 -1
  9. package/dist/channelCollection.js.map +1 -1
  10. package/dist/containerRuntime.d.ts +5 -0
  11. package/dist/containerRuntime.d.ts.map +1 -1
  12. package/dist/containerRuntime.js +13 -7
  13. package/dist/containerRuntime.js.map +1 -1
  14. package/dist/dataStoreContext.d.ts +1 -1
  15. package/dist/dataStoreContext.d.ts.map +1 -1
  16. package/dist/dataStoreContext.js +1 -1
  17. package/dist/dataStoreContext.js.map +1 -1
  18. package/dist/packageVersion.d.ts +1 -1
  19. package/dist/packageVersion.js +1 -1
  20. package/dist/packageVersion.js.map +1 -1
  21. package/dist/runtimeLayerCompatState.d.ts +9 -5
  22. package/dist/runtimeLayerCompatState.d.ts.map +1 -1
  23. package/dist/runtimeLayerCompatState.js +16 -6
  24. package/dist/runtimeLayerCompatState.js.map +1 -1
  25. package/dist/summary/summarizerClientElection.d.ts.map +1 -1
  26. package/dist/summary/summarizerClientElection.js +1 -0
  27. package/dist/summary/summarizerClientElection.js.map +1 -1
  28. package/dist/summary/summarizerTypes.d.ts +5 -0
  29. package/dist/summary/summarizerTypes.d.ts.map +1 -1
  30. package/dist/summary/summarizerTypes.js.map +1 -1
  31. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.d.ts +3 -1
  32. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
  33. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.js +39 -18
  34. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
  35. package/dist/summary/summaryDelayLoadedModule/summarizer.d.ts +1 -1
  36. package/dist/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
  37. package/dist/summary/summaryDelayLoadedModule/summarizer.js +13 -11
  38. package/dist/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
  39. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.d.ts +4 -1
  40. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.d.ts.map +1 -1
  41. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.js +30 -9
  42. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
  43. package/dist/summary/summaryManager.d.ts +1 -1
  44. package/dist/summary/summaryManager.d.ts.map +1 -1
  45. package/dist/summary/summaryManager.js +10 -9
  46. package/dist/summary/summaryManager.js.map +1 -1
  47. package/lib/blobManager/blobManager.d.ts +0 -2
  48. package/lib/blobManager/blobManager.d.ts.map +1 -1
  49. package/lib/blobManager/blobManager.js +4 -5
  50. package/lib/blobManager/blobManager.js.map +1 -1
  51. package/lib/channelCollection.d.ts +3 -3
  52. package/lib/channelCollection.d.ts.map +1 -1
  53. package/lib/channelCollection.js.map +1 -1
  54. package/lib/containerRuntime.d.ts +5 -0
  55. package/lib/containerRuntime.d.ts.map +1 -1
  56. package/lib/containerRuntime.js +13 -7
  57. package/lib/containerRuntime.js.map +1 -1
  58. package/lib/dataStoreContext.d.ts +1 -1
  59. package/lib/dataStoreContext.d.ts.map +1 -1
  60. package/lib/dataStoreContext.js +1 -1
  61. package/lib/dataStoreContext.js.map +1 -1
  62. package/lib/packageVersion.d.ts +1 -1
  63. package/lib/packageVersion.js +1 -1
  64. package/lib/packageVersion.js.map +1 -1
  65. package/lib/runtimeLayerCompatState.d.ts +9 -5
  66. package/lib/runtimeLayerCompatState.d.ts.map +1 -1
  67. package/lib/runtimeLayerCompatState.js +15 -5
  68. package/lib/runtimeLayerCompatState.js.map +1 -1
  69. package/lib/summary/summarizerClientElection.d.ts.map +1 -1
  70. package/lib/summary/summarizerClientElection.js +1 -0
  71. package/lib/summary/summarizerClientElection.js.map +1 -1
  72. package/lib/summary/summarizerTypes.d.ts +5 -0
  73. package/lib/summary/summarizerTypes.d.ts.map +1 -1
  74. package/lib/summary/summarizerTypes.js.map +1 -1
  75. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.d.ts +3 -1
  76. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
  77. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.js +39 -18
  78. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
  79. package/lib/summary/summaryDelayLoadedModule/summarizer.d.ts +1 -1
  80. package/lib/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
  81. package/lib/summary/summaryDelayLoadedModule/summarizer.js +13 -11
  82. package/lib/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
  83. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.d.ts +4 -1
  84. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.d.ts.map +1 -1
  85. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.js +26 -5
  86. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
  87. package/lib/summary/summaryManager.d.ts +1 -1
  88. package/lib/summary/summaryManager.d.ts.map +1 -1
  89. package/lib/summary/summaryManager.js +10 -9
  90. package/lib/summary/summaryManager.js.map +1 -1
  91. package/package.json +26 -26
  92. package/src/blobManager/blobManager.ts +3 -7
  93. package/src/channelCollection.ts +3 -3
  94. package/src/containerRuntime.ts +19 -6
  95. package/src/dataStoreContext.ts +2 -2
  96. package/src/packageVersion.ts +1 -1
  97. package/src/runtimeLayerCompatState.ts +25 -9
  98. package/src/summary/summarizerClientElection.ts +1 -0
  99. package/src/summary/summarizerTypes.ts +5 -0
  100. package/src/summary/summaryDelayLoadedModule/runningSummarizer.ts +54 -26
  101. package/src/summary/summaryDelayLoadedModule/summarizer.ts +13 -11
  102. package/src/summary/summaryDelayLoadedModule/summaryGenerator.ts +35 -5
  103. package/src/summary/summaryManager.ts +11 -10
@@ -270,7 +270,6 @@ export class BlobManager {
270
270
  // blobPath's format - `/<basePath>/<localId>`.
271
271
  private readonly isBlobDeleted: (blobPath: string) => boolean;
272
272
  private readonly runtime: IBlobManagerRuntime;
273
- private readonly localIdGenerator: () => string;
274
273
 
275
274
  private readonly createBlobPayloadPending: boolean;
276
275
 
@@ -298,7 +297,6 @@ export class BlobManager {
298
297
  readonly isBlobDeleted: (blobPath: string) => boolean;
299
298
  readonly runtime: IBlobManagerRuntime;
300
299
  pendingBlobs: IPendingBlobs | undefined;
301
- readonly localIdGenerator?: (() => string) | undefined;
302
300
  readonly createBlobPayloadPending: boolean;
303
301
  }) {
304
302
  const {
@@ -310,7 +308,6 @@ export class BlobManager {
310
308
  isBlobDeleted,
311
309
  runtime,
312
310
  pendingBlobs,
313
- localIdGenerator,
314
311
  createBlobPayloadPending,
315
312
  } = props;
316
313
  this.routeContext = routeContext;
@@ -319,7 +316,6 @@ export class BlobManager {
319
316
  this.blobRequested = blobRequested;
320
317
  this.isBlobDeleted = isBlobDeleted;
321
318
  this.runtime = runtime;
322
- this.localIdGenerator = localIdGenerator ?? uuid;
323
319
  this.createBlobPayloadPending = createBlobPayloadPending;
324
320
 
325
321
  this.mc = createChildMonitoringContext({
@@ -480,7 +476,7 @@ export class BlobManager {
480
476
  if (signal?.aborted === true) {
481
477
  throw createAbortError();
482
478
  }
483
- const localId = this.localIdGenerator();
479
+ const localId = uuid();
484
480
  this.localBlobCache.set(localId, { state: "uploading", blob });
485
481
  // Blobs created while the container is detached are stored in IDetachedBlobStorage.
486
482
  // The 'IContainerStorageService.createBlob()' call below will respond with a pseudo storage ID.
@@ -497,7 +493,7 @@ export class BlobManager {
497
493
  blob: ArrayBufferLike,
498
494
  signal?: AbortSignal,
499
495
  ): Promise<IFluidHandleInternalPayloadPending<ArrayBufferLike>> {
500
- const localId = this.localIdGenerator();
496
+ const localId = uuid();
501
497
  this.localBlobCache.set(localId, { state: "localOnly", blob });
502
498
  await this.uploadAndAttach(localId, signal);
503
499
  return this.getNonPayloadPendingBlobHandle(localId);
@@ -507,7 +503,7 @@ export class BlobManager {
507
503
  blob: ArrayBufferLike,
508
504
  signal?: AbortSignal,
509
505
  ): IFluidHandleInternalPayloadPending<ArrayBufferLike> {
510
- const localId = this.localIdGenerator();
506
+ const localId = uuid();
511
507
  this.localBlobCache.set(localId, { state: "localOnly", blob });
512
508
 
513
509
  const blobHandle = new BlobHandle(
@@ -793,7 +793,7 @@ export class ChannelCollection
793
793
  | OutboundContainerRuntimeAttachMessage
794
794
  | ContainerRuntimeAliasMessage,
795
795
  localOpMetadata: unknown,
796
- squash: boolean | undefined,
796
+ squash: boolean,
797
797
  ): void => {
798
798
  switch (message.type) {
799
799
  case ContainerMessageType.Attach:
@@ -813,7 +813,7 @@ export class ChannelCollection
813
813
  protected readonly resubmitDataStoreOp = (
814
814
  envelope: IEnvelope<FluidDataStoreMessage>,
815
815
  localOpMetadata: unknown,
816
- squash: boolean | undefined,
816
+ squash: boolean,
817
817
  ): void => {
818
818
  const context = this.contexts.get(envelope.address);
819
819
  // If the data store has been deleted, log an error and throw an error. If there are local changes for a
@@ -1820,7 +1820,7 @@ export class ComposableChannelCollection
1820
1820
  type: string,
1821
1821
  content: unknown,
1822
1822
  localOpMetadata: unknown,
1823
- squash?: boolean,
1823
+ squash: boolean,
1824
1824
  ): void {
1825
1825
  // If the cast is incorrect and type is not one of the three supported,
1826
1826
  // reSubmitContainerMessage will assert.
@@ -1622,10 +1622,11 @@ export class ContainerRuntime
1622
1622
 
1623
1623
  // Validate that the Loader is compatible with this Runtime.
1624
1624
  const maybeLoaderCompatDetailsForRuntime = context as FluidObject<ILayerCompatDetails>;
1625
+
1625
1626
  validateLoaderCompatibility(
1626
1627
  maybeLoaderCompatDetailsForRuntime.ILayerCompatDetails,
1627
1628
  this.disposeFn,
1628
- this.mc.logger,
1629
+ this.mc,
1629
1630
  );
1630
1631
 
1631
1632
  // If we support multiple algorithms in the future, then we would need to manage it here carefully.
@@ -1828,7 +1829,9 @@ export class ContainerRuntime
1828
1829
  validateSummaryHeuristicConfiguration(this.summaryConfiguration);
1829
1830
  }
1830
1831
 
1831
- this.summariesDisabled = isSummariesDisabled(this.summaryConfiguration);
1832
+ this.summariesDisabled =
1833
+ isSummariesDisabled(this.summaryConfiguration) ||
1834
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.Test.DisableSummaries") === true;
1832
1835
 
1833
1836
  this.maxConsecutiveReconnects =
1834
1837
  this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? defaultMaxConsecutiveReconnects;
@@ -2294,7 +2297,10 @@ export class ContainerRuntime
2294
2297
 
2295
2298
  const defaultAction = (): void => {
2296
2299
  if (summaryCollection.opsSinceLastAck > maxOpsSinceLastSummary) {
2297
- this.mc.logger.sendTelemetryEvent({ eventName: "SummaryStatus:Behind" });
2300
+ this.mc.logger.sendTelemetryEvent({
2301
+ eventName: "SummaryStatus:Behind",
2302
+ opsWithoutSummary: summaryCollection.opsSinceLastAck,
2303
+ });
2298
2304
  // unregister default to no log on every op after falling behind
2299
2305
  // and register summary ack handler to re-register this handler
2300
2306
  // after successful summary
@@ -2336,8 +2342,9 @@ export class ContainerRuntime
2336
2342
  "summarizerStop",
2337
2343
  "summarizerStart",
2338
2344
  "summarizerStartupFailed",
2339
- ]) {
2340
- this.summaryManager?.on(eventName, (...args: unknown[]) => {
2345
+ "summarizeTimeout",
2346
+ ] as const) {
2347
+ this.summaryManager.on(eventName, (...args: unknown[]) => {
2341
2348
  this.emit(eventName, ...args);
2342
2349
  });
2343
2350
  }
@@ -3930,6 +3937,10 @@ export class ContainerRuntime
3930
3937
  * True to run GC sweep phase after the mark phase
3931
3938
  */
3932
3939
  runSweep?: boolean;
3940
+ /**
3941
+ * Telemetry context to populate during summarization.
3942
+ */
3943
+ telemetryContext?: TelemetryContext;
3933
3944
  }): Promise<ISummaryTreeWithStats> {
3934
3945
  this.verifyNotClosed();
3935
3946
 
@@ -3940,9 +3951,9 @@ export class ContainerRuntime
3940
3951
  runGC = this.garbageCollector.shouldRunGC,
3941
3952
  runSweep,
3942
3953
  fullGC,
3954
+ telemetryContext = new TelemetryContext(),
3943
3955
  } = options;
3944
3956
 
3945
- const telemetryContext = new TelemetryContext();
3946
3957
  // Add the options that are used to generate this summary to the telemetry context.
3947
3958
  telemetryContext.setMultiple("fluid_Summarize", "Options", {
3948
3959
  fullTree,
@@ -4183,6 +4194,7 @@ export class ContainerRuntime
4183
4194
  finalAttempt = false,
4184
4195
  summaryLogger,
4185
4196
  latestSummaryRefSeqNum,
4197
+ telemetryContext = new TelemetryContext(),
4186
4198
  } = options;
4187
4199
  // The summary number for this summary. This will be updated during the summary process, so get it now and
4188
4200
  // use it for all events logged during this summary.
@@ -4372,6 +4384,7 @@ export class ContainerRuntime
4372
4384
  trackState: true,
4373
4385
  summaryLogger: summaryNumberLogger,
4374
4386
  runGC: this.garbageCollector.shouldRunGC,
4387
+ telemetryContext,
4375
4388
  });
4376
4389
  } catch (error) {
4377
4390
  return {
@@ -1003,7 +1003,7 @@ export abstract class FluidDataStoreContext
1003
1003
  validateDatastoreCompatibility(
1004
1004
  maybeDataStoreCompatDetails.ILayerCompatDetails,
1005
1005
  this.dispose.bind(this),
1006
- this.mc.logger,
1006
+ this.mc,
1007
1007
  );
1008
1008
 
1009
1009
  // And now mark the runtime active
@@ -1096,7 +1096,7 @@ export abstract class FluidDataStoreContext
1096
1096
  public reSubmit(
1097
1097
  message: FluidDataStoreMessage,
1098
1098
  localOpMetadata: unknown,
1099
- squash?: boolean,
1099
+ squash: boolean,
1100
1100
  ): void {
1101
1101
  assert(!!this.channel, 0x14b /* "Channel must exist when resubmitting ops" */);
1102
1102
  this.channel.reSubmit(message.type, message.content, localOpMetadata, squash);
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.74.0";
9
+ export const pkgVersion = "2.80.0";
@@ -3,9 +3,10 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import type {
7
- ILayerCompatDetails,
8
- ILayerCompatSupportRequirements,
6
+ import {
7
+ generation,
8
+ type ILayerCompatDetails,
9
+ type ILayerCompatSupportRequirements,
9
10
  } from "@fluid-internal/client-utils";
10
11
  import type { ICriticalContainerError } from "@fluidframework/container-definitions";
11
12
  import {
@@ -14,11 +15,17 @@ import {
14
15
  } from "@fluidframework/runtime-definitions/internal";
15
16
  import {
16
17
  validateLayerCompatibility,
17
- type ITelemetryLoggerExt,
18
+ type MonitoringContext,
18
19
  } from "@fluidframework/telemetry-utils/internal";
19
20
 
20
21
  import { pkgVersion } from "./packageVersion.js";
21
22
 
23
+ /**
24
+ * The config key to disable strict loader layer compatibility check.
25
+ */
26
+ export const disableStrictLoaderLayerCompatibilityCheckKey =
27
+ "Fluid.ContainerRuntime.DisableStrictLoaderLayerCompatibilityCheck";
28
+
22
29
  /**
23
30
  * The core compatibility details of the Runtime layer that is the same across all layer boundaries.
24
31
  * @internal
@@ -31,7 +38,7 @@ export const runtimeCoreCompatDetails = {
31
38
  /**
32
39
  * The current generation of the Runtime layer.
33
40
  */
34
- generation: 3,
41
+ generation,
35
42
  } as const;
36
43
 
37
44
  /**
@@ -97,8 +104,16 @@ export const dataStoreSupportRequirementsForRuntime: ILayerCompatSupportRequirem
97
104
  export function validateLoaderCompatibility(
98
105
  maybeLoaderCompatDetailsForRuntime: ILayerCompatDetails | undefined,
99
106
  disposeFn: (error?: ICriticalContainerError) => void,
100
- logger: ITelemetryLoggerExt,
107
+ mc: MonitoringContext,
101
108
  ): void {
109
+ // By default, use strictCompatibilityCheck here - If the Loader doesn't provide compatibility details,
110
+ // assume it's a very old version and should be considered incompatible,
111
+ // since Loader can drift far from the Runtime causing issues.
112
+ // Can be disabled via config `disableStrictLoaderLayerCompatibilityCheckKey`.
113
+ const disableStrictLoaderLayerCompatibilityCheck = mc.config.getBoolean(
114
+ disableStrictLoaderLayerCompatibilityCheckKey,
115
+ );
116
+
102
117
  validateLayerCompatibility(
103
118
  "runtime",
104
119
  "loader",
@@ -106,7 +121,8 @@ export function validateLoaderCompatibility(
106
121
  loaderSupportRequirementsForRuntime,
107
122
  maybeLoaderCompatDetailsForRuntime,
108
123
  disposeFn,
109
- logger,
124
+ mc,
125
+ disableStrictLoaderLayerCompatibilityCheck !== true /* strictCompatibilityCheck */,
110
126
  );
111
127
  }
112
128
 
@@ -117,7 +133,7 @@ export function validateLoaderCompatibility(
117
133
  export function validateDatastoreCompatibility(
118
134
  maybeDataStoreCompatDetailsForRuntime: ILayerCompatDetails | undefined,
119
135
  disposeFn: () => void,
120
- logger: ITelemetryLoggerExt,
136
+ mc: MonitoringContext,
121
137
  ): void {
122
138
  validateLayerCompatibility(
123
139
  "runtime",
@@ -126,6 +142,6 @@ export function validateDatastoreCompatibility(
126
142
  dataStoreSupportRequirementsForRuntime,
127
143
  maybeDataStoreCompatDetailsForRuntime,
128
144
  disposeFn,
129
- logger,
145
+ mc,
130
146
  );
131
147
  }
@@ -91,6 +91,7 @@ export class SummarizerClientElection
91
91
  lastSummaryAckSeqForClient: this.lastSummaryAckSeqForClient,
92
92
  electionSequenceNumber,
93
93
  nextElectedClientId: this.clientElection.peekNextElectedClient()?.clientId,
94
+ opsWithoutSummary,
94
95
  });
95
96
  this.lastReportedSeq = sequenceNumber;
96
97
  }
@@ -22,6 +22,7 @@ import type {
22
22
  ISequencedDocumentMessage,
23
23
  } from "@fluidframework/driver-definitions/internal";
24
24
  import type { ISummaryStats } from "@fluidframework/runtime-definitions/internal";
25
+ import type { TelemetryContext } from "@fluidframework/runtime-utils/internal";
25
26
  import type {
26
27
  ITelemetryLoggerExt,
27
28
  ITelemetryLoggerPropertyBag,
@@ -167,6 +168,10 @@ export interface ISubmitSummaryOptions extends ISummarizeOptions {
167
168
  * The sequence number of the latest summary used to validate if summary state is correct before summarizing
168
169
  */
169
170
  readonly latestSummaryRefSeqNum: number;
171
+ /**
172
+ * Shared telemetry context for the current summarize attempt.
173
+ */
174
+ telemetryContext?: TelemetryContext;
170
175
  }
171
176
 
172
177
  /**
@@ -177,11 +177,6 @@ export class RunningSummarizer
177
177
  private totalSuccessfulAttempts = 0;
178
178
  private initialized = false;
179
179
 
180
- private readonly runtimeListener: (
181
- op: ISequencedDocumentMessage,
182
- runtimeMessage?: boolean,
183
- ) => void;
184
-
185
180
  /**
186
181
  * The maximum number of summary attempts to do when submit summary fails.
187
182
  */
@@ -295,12 +290,6 @@ export class RunningSummarizer
295
290
  this.mc.logger,
296
291
  );
297
292
 
298
- // Listen to runtime for ops
299
- this.runtimeListener = (op: ISequencedDocumentMessage, runtimeMessage?: boolean) => {
300
- this.handleOp(op, runtimeMessage === true);
301
- };
302
- this.runtime.on("op", this.runtimeListener);
303
-
304
293
  // The max attempts for submit failures can be overridden via a feature flag. This allows us to
305
294
  // tweak this as per telemetry data until we arrive at a stable number.
306
295
  // If its set to a number higher than `defaultMaxAttemptsForSubmitFailures`, it will be ignored.
@@ -312,6 +301,8 @@ export class RunningSummarizer
312
301
  overrideMaxAttempts < defaultMaxAttemptsForSubmitFailures
313
302
  ? overrideMaxAttempts
314
303
  : defaultMaxAttemptsForSubmitFailures;
304
+
305
+ this.setupEventListeners();
315
306
  }
316
307
 
317
308
  private async handleSummaryAck(ack: IAckedSummary): Promise<void> {
@@ -397,7 +388,7 @@ export class RunningSummarizer
397
388
  }
398
389
 
399
390
  public dispose(): void {
400
- this.runtime.off("op", this.runtimeListener);
391
+ this.cleanupEventListeners();
401
392
  this.summaryWatcher.dispose();
402
393
  this.heuristicRunner?.dispose();
403
394
  this.heuristicRunner = undefined;
@@ -408,6 +399,33 @@ export class RunningSummarizer
408
399
  this.stopping = true;
409
400
  }
410
401
 
402
+ private readonly eventsCleanup: (() => void)[] = [];
403
+
404
+ private setupEventListeners(): void {
405
+ const runtimeListener: (op: ISequencedDocumentMessage, runtimeMessage?: boolean) => void =
406
+ (op: ISequencedDocumentMessage, runtimeMessage?: boolean) => {
407
+ this.handleOp(op, runtimeMessage === true);
408
+ };
409
+ this.runtime.on("op", runtimeListener);
410
+ this.eventsCleanup.push(() => this.runtime.off("op", runtimeListener));
411
+
412
+ // Forward events
413
+ for (const event of ["summarizeTimeout"] as const) {
414
+ const listener = (...args: unknown[]): void => {
415
+ this.emit(event, ...args);
416
+ };
417
+ this.generator.on(event, listener);
418
+ this.eventsCleanup.push(() => this.generator.off(event, listener));
419
+ }
420
+ }
421
+
422
+ private cleanupEventListeners(): void {
423
+ for (const cleanup of this.eventsCleanup) {
424
+ cleanup();
425
+ }
426
+ this.eventsCleanup.length = 0;
427
+ }
428
+
411
429
  /**
412
430
  * RunningSummarizer's logger includes the sequenced index of the current summary on each event.
413
431
  * If some other Summarizer code wants that event on their logs they can get it here,
@@ -655,11 +673,12 @@ export class RunningSummarizer
655
673
  numUnsummarizedNonRuntimeOps: this.heuristicData.numNonRuntimeOps,
656
674
  isLastSummary,
657
675
  });
658
- this.mc.logger.sendErrorEvent(
676
+ summaryLogger.sendErrorEvent(
659
677
  {
660
678
  eventName: "SummarizeFailed",
661
679
  maxAttempts: 1,
662
680
  summaryAttempts: 1,
681
+ isLastSummary,
663
682
  },
664
683
  result.error,
665
684
  );
@@ -708,7 +727,10 @@ export class RunningSummarizer
708
727
  this.afterSummaryAction();
709
728
  },
710
729
  ).catch((error) => {
711
- this.mc.logger.sendErrorEvent({ eventName: "UnexpectedSummarizeError" }, error);
730
+ this.mc.logger.sendErrorEvent(
731
+ { eventName: "UnexpectedSummarizeError", summarizeReason: reason },
732
+ error,
733
+ );
712
734
  });
713
735
  }
714
736
 
@@ -718,6 +740,7 @@ export class RunningSummarizer
718
740
  */
719
741
  private async trySummarizeWithRetries(
720
742
  reason: SummarizeReason,
743
+ summarizeOptions: ISummarizeOptions = {},
721
744
  ): Promise<ISummarizeResults | undefined> {
722
745
  // Helper to set summarize options, telemetry properties and call summarize.
723
746
  const attemptSummarize = (
@@ -727,13 +750,13 @@ export class RunningSummarizer
727
750
  summarizeProps: ISummarizeTelemetryProperties;
728
751
  summarizeResult: ISummarizeResults;
729
752
  } => {
730
- const summarizeOptions: ISummarizeOptions = {
731
- fullTree: false,
753
+ const attemptSummarizeOptions: ISummarizeOptions = {
754
+ fullTree: summarizeOptions.fullTree ?? false,
732
755
  };
733
756
  const summarizeProps: ISummarizeTelemetryProperties = {
734
757
  summarizeReason: reason,
735
758
  summaryAttempts: attemptNumber,
736
- ...summarizeOptions,
759
+ ...attemptSummarizeOptions,
737
760
  finalAttempt,
738
761
  };
739
762
  const summaryLogger = createChildLogger({
@@ -742,7 +765,7 @@ export class RunningSummarizer
742
765
  });
743
766
 
744
767
  const summaryOptions: ISubmitSummaryOptions = {
745
- ...summarizeOptions,
768
+ ...attemptSummarizeOptions,
746
769
  summaryLogger,
747
770
  cancellationToken: this.cancellationToken,
748
771
  finalAttempt,
@@ -872,6 +895,8 @@ export class RunningSummarizer
872
895
  eventName: "SummarizeFailed",
873
896
  maxAttempts,
874
897
  summaryAttempts: currentAttempt,
898
+ summarizeReason: reason,
899
+ isLastSummary: reason === "lastSummary",
875
900
  },
876
901
  error,
877
902
  );
@@ -893,8 +918,9 @@ export class RunningSummarizer
893
918
  private async summarizeOnDemandWithRetries(
894
919
  reason: SummarizeReason,
895
920
  resultsBuilder: SummarizeResultBuilder,
921
+ summarizeOptions: ISummarizeOptions = {},
896
922
  ): Promise<ISummarizeResults> {
897
- const results = await this.trySummarizeWithRetries(reason);
923
+ const results = await this.trySummarizeWithRetries(reason, summarizeOptions);
898
924
  if (results === undefined) {
899
925
  resultsBuilder.fail(
900
926
  "Summarization was canceled",
@@ -932,13 +958,15 @@ export class RunningSummarizer
932
958
  throw new UsageError("Attempted to run an already-running summarizer on demand");
933
959
  }
934
960
 
935
- const { reason, ...summarizeOptions } = options;
936
- if (options.retryOnFailure === true) {
937
- this.summarizeOnDemandWithRetries(`onDemand;${reason}`, resultsBuilder).catch(
938
- (error: IRetriableFailureError) => {
939
- resultsBuilder.fail("summarize failed", error);
940
- },
941
- );
961
+ const { reason, retryOnFailure, ...summarizeOptions } = options;
962
+ if (retryOnFailure === true) {
963
+ this.summarizeOnDemandWithRetries(
964
+ `onDemand;${reason}`,
965
+ resultsBuilder,
966
+ summarizeOptions,
967
+ ).catch((error: IRetriableFailureError) => {
968
+ resultsBuilder.fail("summarize failed", error);
969
+ });
942
970
  } else {
943
971
  this.trySummarizeOnce(
944
972
  { summarizeReason: `onDemand/${reason}` },
@@ -301,7 +301,7 @@ export class Summarizer extends TypedEventEmitter<ISummarizerEvents> implements
301
301
  this.runtime,
302
302
  );
303
303
  this.runningSummarizer = runningSummarizer;
304
- this.setupForwardedEvents();
304
+ this.setupForwardedEvents(runningSummarizer);
305
305
  this.starting = false;
306
306
  return runningSummarizer;
307
307
  }
@@ -399,24 +399,26 @@ export class Summarizer extends TypedEventEmitter<ISummarizerEvents> implements
399
399
  this._heuristicData?.recordAttempt(summaryRefSeqNum);
400
400
  }
401
401
 
402
- private readonly forwardedEvents = new Map<string, () => void>();
402
+ private readonly forwardedEventsCleanup: (() => void)[] = [];
403
403
 
404
- private setupForwardedEvents(): void {
405
- for (const event of ["summarize", "summarizeAllAttemptsFailed"]) {
404
+ private setupForwardedEvents(runningSummarizer: RunningSummarizer): void {
405
+ for (const event of [
406
+ "summarize",
407
+ "summarizeAllAttemptsFailed",
408
+ "summarizeTimeout",
409
+ ] as const) {
406
410
  const listener = (...args: unknown[]): void => {
407
411
  this.emit(event, ...args);
408
412
  };
409
- // TODO: better typing here
410
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
411
- this.runningSummarizer?.on(event as any, listener);
412
- this.forwardedEvents.set(event, listener);
413
+ runningSummarizer.on(event, listener);
414
+ this.forwardedEventsCleanup.push(() => runningSummarizer.off(event, listener));
413
415
  }
414
416
  }
415
417
 
416
418
  private cleanupForwardedEvents(): void {
417
- for (const [event, listener] of this.forwardedEvents.entries()) {
418
- this.runningSummarizer?.off(event, listener);
419
+ for (const cleanup of this.forwardedEventsCleanup) {
420
+ cleanup();
419
421
  }
420
- this.forwardedEvents.clear();
422
+ this.forwardedEventsCleanup.length = 0;
421
423
  }
422
424
  }
@@ -3,9 +3,12 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import { TypedEventEmitter } from "@fluid-internal/client-utils";
7
+ import type { ISummarizerEvents } from "@fluidframework/container-runtime-definitions/internal";
6
8
  import { assert, type IPromiseTimer, Timer } from "@fluidframework/core-utils/internal";
7
9
  import { DriverErrorTypes, MessageType } from "@fluidframework/driver-definitions/internal";
8
10
  import { getRetryDelaySecondsFromError } from "@fluidframework/driver-utils/internal";
11
+ import { TelemetryContext } from "@fluidframework/runtime-utils/internal";
9
12
  import {
10
13
  isFluidError,
11
14
  type ITelemetryLoggerExt,
@@ -40,8 +43,9 @@ const maxSummarizeTimeoutCount = 5; // Double and resend 5 times
40
43
  /**
41
44
  * This class generates and tracks a summary attempt.
42
45
  */
43
- export class SummaryGenerator {
46
+ export class SummaryGenerator extends TypedEventEmitter<ISummarizerEvents> {
44
47
  private readonly summarizeTimer: Timer;
48
+ private activeTelemetryContext?: TelemetryContext;
45
49
  constructor(
46
50
  private readonly pendingAckTimer: IPromiseTimer,
47
51
  private readonly heuristicData: ISummarizeHeuristicData,
@@ -55,6 +59,7 @@ export class SummaryGenerator {
55
59
  private readonly summaryWatcher: Pick<IClientSummaryWatcher, "watchSummary">,
56
60
  private readonly logger: ITelemetryLoggerExt,
57
61
  ) {
62
+ super();
58
63
  this.summarizeTimer = new Timer(maxSummarizeTimeoutTime, () =>
59
64
  this.summarizeTimerHandler(maxSummarizeTimeoutTime, 1),
60
65
  );
@@ -85,7 +90,16 @@ export class SummaryGenerator {
85
90
  submitSummaryOptions: ISubmitSummaryOptions,
86
91
  resultsBuilder: SummarizeResultBuilder,
87
92
  ): Promise<void> {
88
- const { summaryLogger, cancellationToken, ...summarizeOptions } = submitSummaryOptions;
93
+ const {
94
+ summaryLogger,
95
+ cancellationToken,
96
+ telemetryContext = new TelemetryContext(),
97
+ ...summarizeOptions
98
+ } = submitSummaryOptions;
99
+
100
+ telemetryContext.setCurrentSummarizeStep("submitSummary");
101
+ const submitOptions = { ...submitSummaryOptions, telemetryContext };
102
+ this.activeTelemetryContext = telemetryContext;
89
103
 
90
104
  // Note: timeSinceLastAttempt and timeSinceLastSummary for the
91
105
  // first summary are basically the time since the summarizer was loaded.
@@ -97,6 +111,8 @@ export class SummaryGenerator {
97
111
  fullTree: summarizeOptions.fullTree ?? false,
98
112
  timeSinceLastAttempt,
99
113
  timeSinceLastSummary,
114
+ nonRuntimeOpsSinceLastSummary: this.heuristicData.numNonRuntimeOps,
115
+ runtimeOpsSinceLastSummary: this.heuristicData.numRuntimeOps,
100
116
  };
101
117
 
102
118
  const summarizeEvent = PerformanceEvent.start(
@@ -148,10 +164,12 @@ export class SummaryGenerator {
148
164
  // Wait to generate and send summary
149
165
  this.summarizeTimer.start();
150
166
  try {
167
+ telemetryContext.setCurrentSummarizeStep("generateSummary");
151
168
  // Need to save refSeqNum before we record new attempt (happens as part of submitSummaryCallback)
152
169
  const lastAttemptRefSeqNum = this.heuristicData.lastAttempt.refSequenceNumber;
153
170
 
154
- summaryData = await this.submitSummaryCallback(submitSummaryOptions);
171
+ summaryData = await this.submitSummaryCallback(submitOptions);
172
+ telemetryContext.setCurrentSummarizeStep("submitSummaryOp");
155
173
 
156
174
  // Cumulatively add telemetry properties based on how far generateSummary went.
157
175
  const referenceSequenceNumber = summaryData.referenceSequenceNumber;
@@ -189,6 +207,7 @@ export class SummaryGenerator {
189
207
  * exceed the number of ops since last summary + number of data store whose reference state changed.
190
208
  */
191
209
  if (submitSummaryOptions.fullTree !== true) {
210
+ telemetryContext.setCurrentSummarizeStep("watchSummary");
192
211
  const { summarizedDataStoreCount, gcStateUpdatedDataStoreCount = 0 } =
193
212
  summaryData.summaryStats;
194
213
  if (
@@ -227,6 +246,8 @@ export class SummaryGenerator {
227
246
  this.summarizeTimer.clear();
228
247
  }
229
248
 
249
+ telemetryContext.setCurrentSummarizeStep("waitForSummaryAck");
250
+
230
251
  try {
231
252
  const pendingTimeoutP = this.pendingAckTimer.start();
232
253
  const summary = this.summaryWatcher.watchSummary(summaryData.clientSequenceNumber);
@@ -300,6 +321,7 @@ export class SummaryGenerator {
300
321
  ...summarizeTelemetryProps,
301
322
  };
302
323
  if (ackNackOp.type === MessageType.SummaryAck) {
324
+ telemetryContext.setCurrentSummarizeStep("ackReceived");
303
325
  this.heuristicData.markLastAttemptAsSuccessful();
304
326
  this.successfulSummaryCallback();
305
327
  summarizeEvent.end({
@@ -324,6 +346,7 @@ export class SummaryGenerator {
324
346
  } else {
325
347
  // Check for retryDelay in summaryNack response.
326
348
  assert(ackNackOp.type === MessageType.SummaryNack, 0x274 /* "type check" */);
349
+ telemetryContext.setCurrentSummarizeStep("nackReceived");
327
350
  const summaryNack = ackNackOp.contents;
328
351
  const errorMessage = summaryNack?.message;
329
352
  const retryAfterSeconds = summaryNack?.retryAfter;
@@ -350,6 +373,7 @@ export class SummaryGenerator {
350
373
  }
351
374
  } finally {
352
375
  this.pendingAckTimer.clear();
376
+ this.activeTelemetryContext = undefined;
353
377
  }
354
378
  }
355
379
 
@@ -390,8 +414,6 @@ export class SummaryGenerator {
390
414
  clientSequenceNumber: summaryData.clientSequenceNumber,
391
415
  hasMissingOpData: this.heuristicData.hasMissingOpData,
392
416
  opsSizesSinceLastSummary: this.heuristicData.totalOpsSize,
393
- nonRuntimeOpsSinceLastSummary: this.heuristicData.numNonRuntimeOps,
394
- runtimeOpsSinceLastSummary: this.heuristicData.numRuntimeOps,
395
417
  };
396
418
  }
397
419
 
@@ -408,6 +430,13 @@ export class SummaryGenerator {
408
430
  eventName: "SummarizeTimeout",
409
431
  timeoutTime: time,
410
432
  timeoutCount: count,
433
+ currentSummarizeStep: this.activeTelemetryContext?.getCurrentSummarizeStep(),
434
+ });
435
+ this.emit("summarizeTimeout", {
436
+ timeoutCount: count,
437
+ currentSummarizeStep: this.activeTelemetryContext?.getCurrentSummarizeStep(),
438
+ numUnsummarizedRuntimeOps: this.heuristicData.numRuntimeOps,
439
+ numUnsummarizedNonRuntimeOps: this.heuristicData.numNonRuntimeOps,
411
440
  });
412
441
  if (count < maxSummarizeTimeoutCount) {
413
442
  // Double and start a new timer
@@ -420,5 +449,6 @@ export class SummaryGenerator {
420
449
 
421
450
  public dispose(): void {
422
451
  this.summarizeTimer.clear();
452
+ this.activeTelemetryContext = undefined;
423
453
  }
424
454
  }