@fluidframework/container-runtime 2.74.0-370705 → 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 (117) hide show
  1. package/CHANGELOG.md +8 -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 +20 -2
  10. package/dist/channelCollection.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +5 -0
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +13 -7
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/dataStoreContext.d.ts +1 -1
  16. package/dist/dataStoreContext.d.ts.map +1 -1
  17. package/dist/dataStoreContext.js +1 -1
  18. package/dist/dataStoreContext.js.map +1 -1
  19. package/dist/dataStoreContexts.d.ts +56 -9
  20. package/dist/dataStoreContexts.d.ts.map +1 -1
  21. package/dist/dataStoreContexts.js +56 -9
  22. package/dist/dataStoreContexts.js.map +1 -1
  23. package/dist/packageVersion.d.ts +1 -1
  24. package/dist/packageVersion.d.ts.map +1 -1
  25. package/dist/packageVersion.js +1 -1
  26. package/dist/packageVersion.js.map +1 -1
  27. package/dist/runtimeLayerCompatState.d.ts +10 -6
  28. package/dist/runtimeLayerCompatState.d.ts.map +1 -1
  29. package/dist/runtimeLayerCompatState.js +16 -6
  30. package/dist/runtimeLayerCompatState.js.map +1 -1
  31. package/dist/summary/summarizerClientElection.d.ts.map +1 -1
  32. package/dist/summary/summarizerClientElection.js +1 -0
  33. package/dist/summary/summarizerClientElection.js.map +1 -1
  34. package/dist/summary/summarizerTypes.d.ts +5 -0
  35. package/dist/summary/summarizerTypes.d.ts.map +1 -1
  36. package/dist/summary/summarizerTypes.js.map +1 -1
  37. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.d.ts +3 -1
  38. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
  39. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.js +39 -18
  40. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
  41. package/dist/summary/summaryDelayLoadedModule/summarizer.d.ts +1 -1
  42. package/dist/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
  43. package/dist/summary/summaryDelayLoadedModule/summarizer.js +13 -11
  44. package/dist/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
  45. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.d.ts +4 -1
  46. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.d.ts.map +1 -1
  47. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.js +30 -9
  48. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
  49. package/dist/summary/summaryManager.d.ts +1 -1
  50. package/dist/summary/summaryManager.d.ts.map +1 -1
  51. package/dist/summary/summaryManager.js +10 -9
  52. package/dist/summary/summaryManager.js.map +1 -1
  53. package/eslint.config.mts +31 -0
  54. package/lib/blobManager/blobManager.d.ts +0 -2
  55. package/lib/blobManager/blobManager.d.ts.map +1 -1
  56. package/lib/blobManager/blobManager.js +4 -5
  57. package/lib/blobManager/blobManager.js.map +1 -1
  58. package/lib/channelCollection.d.ts +3 -3
  59. package/lib/channelCollection.d.ts.map +1 -1
  60. package/lib/channelCollection.js +20 -2
  61. package/lib/channelCollection.js.map +1 -1
  62. package/lib/containerRuntime.d.ts +5 -0
  63. package/lib/containerRuntime.d.ts.map +1 -1
  64. package/lib/containerRuntime.js +13 -7
  65. package/lib/containerRuntime.js.map +1 -1
  66. package/lib/dataStoreContext.d.ts +1 -1
  67. package/lib/dataStoreContext.d.ts.map +1 -1
  68. package/lib/dataStoreContext.js +1 -1
  69. package/lib/dataStoreContext.js.map +1 -1
  70. package/lib/dataStoreContexts.d.ts +56 -9
  71. package/lib/dataStoreContexts.d.ts.map +1 -1
  72. package/lib/dataStoreContexts.js +56 -9
  73. package/lib/dataStoreContexts.js.map +1 -1
  74. package/lib/packageVersion.d.ts +1 -1
  75. package/lib/packageVersion.d.ts.map +1 -1
  76. package/lib/packageVersion.js +1 -1
  77. package/lib/packageVersion.js.map +1 -1
  78. package/lib/runtimeLayerCompatState.d.ts +10 -6
  79. package/lib/runtimeLayerCompatState.d.ts.map +1 -1
  80. package/lib/runtimeLayerCompatState.js +15 -5
  81. package/lib/runtimeLayerCompatState.js.map +1 -1
  82. package/lib/summary/summarizerClientElection.d.ts.map +1 -1
  83. package/lib/summary/summarizerClientElection.js +1 -0
  84. package/lib/summary/summarizerClientElection.js.map +1 -1
  85. package/lib/summary/summarizerTypes.d.ts +5 -0
  86. package/lib/summary/summarizerTypes.d.ts.map +1 -1
  87. package/lib/summary/summarizerTypes.js.map +1 -1
  88. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.d.ts +3 -1
  89. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
  90. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.js +39 -18
  91. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
  92. package/lib/summary/summaryDelayLoadedModule/summarizer.d.ts +1 -1
  93. package/lib/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
  94. package/lib/summary/summaryDelayLoadedModule/summarizer.js +13 -11
  95. package/lib/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
  96. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.d.ts +4 -1
  97. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.d.ts.map +1 -1
  98. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.js +26 -5
  99. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
  100. package/lib/summary/summaryManager.d.ts +1 -1
  101. package/lib/summary/summaryManager.d.ts.map +1 -1
  102. package/lib/summary/summaryManager.js +10 -9
  103. package/lib/summary/summaryManager.js.map +1 -1
  104. package/package.json +27 -26
  105. package/src/blobManager/blobManager.ts +3 -7
  106. package/src/channelCollection.ts +29 -5
  107. package/src/containerRuntime.ts +19 -6
  108. package/src/dataStoreContext.ts +2 -2
  109. package/src/dataStoreContexts.ts +56 -9
  110. package/src/packageVersion.ts +1 -1
  111. package/src/runtimeLayerCompatState.ts +25 -9
  112. package/src/summary/summarizerClientElection.ts +1 -0
  113. package/src/summary/summarizerTypes.ts +5 -0
  114. package/src/summary/summaryDelayLoadedModule/runningSummarizer.ts +54 -26
  115. package/src/summary/summaryDelayLoadedModule/summarizer.ts +13 -11
  116. package/src/summary/summaryDelayLoadedModule/summaryGenerator.ts +35 -5
  117. 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(
@@ -492,9 +492,33 @@ export class ChannelCollection
492
492
  continue;
493
493
  }
494
494
 
495
- // If a non-local operation then go and create the object, otherwise mark it as officially attached.
495
+ // Check for collision with local (not yet live / known to other clients) DataStore
496
+ // This is not a DataCorruption case if we crash the container before the DataStore becomes visible to others (it's a DataProcessingError instead)
497
+ //
498
+ // POSSIBLE CAUSES:
499
+ // - Something with ID creation, e.g. a bug in shortID logic, or somehow a generated ID matches an existing alias.
500
+ // - An invalid operation by the application or service where an existing container is returned to a new container attach call,
501
+ // resulting in duplicate accounting for objects that were supposed to be local-only. e.g. if the application patches in custom
502
+ // logic not supported by Fluid's API.
503
+ if (this.contexts.getUnbound(attachMessage.id) !== undefined) {
504
+ const error = DataProcessingError.create(
505
+ "Local DataStore matches remote DataStore id",
506
+ "DataStoreAttach",
507
+ envelope,
508
+ { ...tagCodeArtifacts({ dataStoreId: attachMessage.id }) },
509
+ );
510
+ throw error;
511
+ }
512
+
513
+ // Check for collision with already processed (attaching/attached or aliased) DataStore
514
+ // This is presumed to indicate a corrupted op stream, where we'd expect all future sessions to fail here too.
515
+ //
516
+ // POSSIBLE CAUSES:
517
+ // - A bug in the service or driver that results in ops being duplicated
518
+ // - Similar to above, an existing container being returned to a new container attach call,
519
+ // where the DataStore in question was already made locally visible before container attach.
520
+ // (Perhaps future sessions would not fail in this case, but it's hypothetical and hard to differentiate)
496
521
  if (this.alreadyProcessed(attachMessage.id)) {
497
- // TODO: dataStoreId may require a different tag from PackageData #7488
498
522
  const error = new DataCorruptionError(
499
523
  // pre-0.58 error message: duplicateDataStoreCreatedWithExistingId
500
524
  "Duplicate DataStore created with existing id",
@@ -769,7 +793,7 @@ export class ChannelCollection
769
793
  | OutboundContainerRuntimeAttachMessage
770
794
  | ContainerRuntimeAliasMessage,
771
795
  localOpMetadata: unknown,
772
- squash: boolean | undefined,
796
+ squash: boolean,
773
797
  ): void => {
774
798
  switch (message.type) {
775
799
  case ContainerMessageType.Attach:
@@ -789,7 +813,7 @@ export class ChannelCollection
789
813
  protected readonly resubmitDataStoreOp = (
790
814
  envelope: IEnvelope<FluidDataStoreMessage>,
791
815
  localOpMetadata: unknown,
792
- squash: boolean | undefined,
816
+ squash: boolean,
793
817
  ): void => {
794
818
  const context = this.contexts.get(envelope.address);
795
819
  // If the data store has been deleted, log an error and throw an error. If there are local changes for a
@@ -1796,7 +1820,7 @@ export class ComposableChannelCollection
1796
1820
  type: string,
1797
1821
  content: unknown,
1798
1822
  localOpMetadata: unknown,
1799
- squash?: boolean,
1823
+ squash: boolean,
1800
1824
  ): void {
1801
1825
  // If the cast is incorrect and type is not one of the three supported,
1802
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);
@@ -13,27 +13,41 @@ import {
13
13
  import type { FluidDataStoreContext, LocalFluidDataStoreContext } from "./dataStoreContext.js";
14
14
 
15
15
  /**
16
+ * Manages the collection of data store contexts, tracking their bound/unbound state.
17
+ *
18
+ * @remarks
19
+ * A context is "unbound" when it's created locally but not yet made visible (reachable from root).
20
+ * A context is "bound" once it's made locally visible, regardless of the Container's attach state.
21
+ * In attached containers, binding a context immediately sends an attach op and transitions it to Attaching state.
22
+ *
16
23
  * @internal
17
24
  */
18
25
  export class DataStoreContexts
19
26
  implements Iterable<[string, FluidDataStoreContext]>, IDisposable
20
27
  {
28
+ /**
29
+ * Set of IDs for contexts that are unbound (not yet made locally visible).
30
+ * These contexts exist locally but aren't known to other clients (even in an attached container).
31
+ */
21
32
  private readonly notBoundContexts = new Set<string>();
22
33
 
23
34
  /**
24
- * Attached and loaded context proxies
35
+ * Map of all data store contexts (both bound and unbound).
25
36
  */
26
37
  private readonly _contexts = new Map<string, FluidDataStoreContext>();
27
38
 
28
39
  /**
29
40
  * List of pending context waiting either to be bound or to arrive from another client.
30
41
  * This covers the case where a local context has been created but not yet bound,
31
- * or the case where a client knows a store will exist and is waiting on its creation,
42
+ * or the case where a client knows a store will exist (e.g. by alias) and is waiting on its creation,
32
43
  * so that a caller may await the deferred's promise until such a time as the context is fully ready.
33
44
  * This is a superset of _contexts, since contexts remain here once the Deferred resolves.
34
45
  */
35
46
  private readonly deferredContexts = new Map<string, Deferred<FluidDataStoreContext>>();
36
47
 
48
+ /**
49
+ * Lazy disposal logic that disposes all contexts when called.
50
+ */
37
51
  // eslint-disable-next-line unicorn/consistent-function-scoping -- Property is defined once; no need to extract inner lambda
38
52
  private readonly disposeOnce = new Lazy<void>(() => {
39
53
  // close/stop all store contexts
@@ -73,22 +87,39 @@ export class DataStoreContexts
73
87
  }
74
88
  public readonly dispose = (): void => this.disposeOnce.value;
75
89
 
90
+ /**
91
+ * Returns the count of unbound contexts (i.e. local-only on this client)
92
+ */
76
93
  public notBoundLength(): number {
77
94
  return this.notBoundContexts.size;
78
95
  }
79
96
 
97
+ /**
98
+ * Returns true if the given ID corresponds to an unbound context. (i.e. local-only on this client)
99
+ */
80
100
  public isNotBound(id: string): boolean {
81
101
  return this.notBoundContexts.has(id);
82
102
  }
83
103
 
104
+ /**
105
+ * Returns true if a context with the given ID exists (bound or unbound).
106
+ */
84
107
  public has(id: string): boolean {
85
108
  return this._contexts.has(id);
86
109
  }
87
110
 
111
+ /**
112
+ * Returns the context with the given ID, or undefined if not found.
113
+ * This returns both bound and unbound contexts.
114
+ */
88
115
  public get(id: string): FluidDataStoreContext | undefined {
89
116
  return this._contexts.get(id);
90
117
  }
91
118
 
119
+ /**
120
+ * Deletes the context with the given ID from all internal maps.
121
+ * @returns True if the context was found and deleted, false otherwise.
122
+ */
92
123
  public delete(id: string): boolean {
93
124
  this.deferredContexts.delete(id);
94
125
  this.notBoundContexts.delete(id);
@@ -100,16 +131,24 @@ export class DataStoreContexts
100
131
  return this._contexts.delete(id);
101
132
  }
102
133
 
134
+ /**
135
+ * Map of recently deleted contexts for diagnostic purposes for GC.
136
+ * Allows retrieval of context information even after deletion for logging/telemetry.
137
+ */
103
138
  private readonly _recentlyDeletedContexts: Map<string, FluidDataStoreContext | undefined> =
104
139
  new Map();
105
140
 
141
+ /**
142
+ * Returns a recently deleted context by ID, or undefined if not found.
143
+ * Used for diagnostic logging for GC, when a deleted context is referenced.
144
+ */
106
145
  public getRecentlyDeletedContext(id: string): FluidDataStoreContext | undefined {
107
146
  return this._recentlyDeletedContexts.get(id);
108
147
  }
109
148
 
110
149
  /**
111
- * Return the unbound local context with the given id,
112
- * or undefined if it's not found or not unbound.
150
+ * Returns the unbound local context with the given ID.
151
+ * @returns The unbound context, or undefined if not found or not unbound.
113
152
  */
114
153
  public getUnbound(id: string): LocalFluidDataStoreContext | undefined {
115
154
  const context = this._contexts.get(id);
@@ -121,7 +160,8 @@ export class DataStoreContexts
121
160
  }
122
161
 
123
162
  /**
124
- * Add the given context, marking it as to-be-bound
163
+ * Adds the given context to the collection, marking it as unbound (not yet locally visible).
164
+ * Asserts that no context with this ID already exists.
125
165
  */
126
166
  public addUnbound(context: LocalFluidDataStoreContext): void {
127
167
  const id = context.id;
@@ -152,6 +192,10 @@ export class DataStoreContexts
152
192
  return deferredContext.promise;
153
193
  }
154
194
 
195
+ /**
196
+ * Gets or creates a deferred promise for the given context ID.
197
+ * Used to allow waiting for contexts that don't exist yet.
198
+ */
155
199
  private ensureDeferred(id: string): Deferred<FluidDataStoreContext> {
156
200
  const deferred = this.deferredContexts.get(id);
157
201
  if (deferred) {
@@ -164,7 +208,8 @@ export class DataStoreContexts
164
208
  }
165
209
 
166
210
  /**
167
- * Update this context as bound
211
+ * Marks the context with the given ID as bound (locally visible).
212
+ * Removes it from the unbound set and resolves its deferred promise.
168
213
  */
169
214
  public bind(id: string): void {
170
215
  const removed: boolean = this.notBoundContexts.delete(id);
@@ -191,9 +236,11 @@ export class DataStoreContexts
191
236
  }
192
237
 
193
238
  /**
194
- * Add the given context, marking it as not local-only.
195
- * This could be because it's a local context that's been bound, or because it's a remote context.
196
- * @param context - The context to add
239
+ * Adds the given context to the collection as already bound or from a remote client.
240
+ * This is used when:
241
+ * - Adding a local context that's already been bound via the bind() method, OR
242
+ * - Adding a remote context that was created by another client.
243
+ * The context's deferred promise is resolved immediately.
197
244
  */
198
245
  public addBoundOrRemoted(context: FluidDataStoreContext): void {
199
246
  const id = context.id;
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.74.0-370705";
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: 2,
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}` },