@fluidframework/container-runtime 0.58.2002 → 0.58.3000-61081

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 (83) hide show
  1. package/dist/blobManager.d.ts +3 -2
  2. package/dist/blobManager.d.ts.map +1 -1
  3. package/dist/blobManager.js +13 -9
  4. package/dist/blobManager.js.map +1 -1
  5. package/dist/connectionTelemetry.d.ts.map +1 -1
  6. package/dist/connectionTelemetry.js +63 -23
  7. package/dist/connectionTelemetry.js.map +1 -1
  8. package/dist/containerRuntime.d.ts +12 -4
  9. package/dist/containerRuntime.d.ts.map +1 -1
  10. package/dist/containerRuntime.js +61 -15
  11. package/dist/containerRuntime.js.map +1 -1
  12. package/dist/opTelemetry.d.ts +22 -0
  13. package/dist/opTelemetry.d.ts.map +1 -0
  14. package/dist/opTelemetry.js +59 -0
  15. package/dist/opTelemetry.js.map +1 -0
  16. package/dist/orderedClientElection.d.ts +57 -6
  17. package/dist/orderedClientElection.d.ts.map +1 -1
  18. package/dist/orderedClientElection.js +140 -25
  19. package/dist/orderedClientElection.js.map +1 -1
  20. package/dist/packageVersion.d.ts +1 -1
  21. package/dist/packageVersion.d.ts.map +1 -1
  22. package/dist/packageVersion.js +1 -1
  23. package/dist/packageVersion.js.map +1 -1
  24. package/dist/summarizerClientElection.d.ts +2 -0
  25. package/dist/summarizerClientElection.d.ts.map +1 -1
  26. package/dist/summarizerClientElection.js +7 -2
  27. package/dist/summarizerClientElection.js.map +1 -1
  28. package/dist/summarizerTypes.d.ts +9 -0
  29. package/dist/summarizerTypes.d.ts.map +1 -1
  30. package/dist/summarizerTypes.js.map +1 -1
  31. package/dist/summaryGenerator.d.ts.map +1 -1
  32. package/dist/summaryGenerator.js +1 -1
  33. package/dist/summaryGenerator.js.map +1 -1
  34. package/dist/summaryManager.d.ts.map +1 -1
  35. package/dist/summaryManager.js +14 -3
  36. package/dist/summaryManager.js.map +1 -1
  37. package/lib/blobManager.d.ts +3 -2
  38. package/lib/blobManager.d.ts.map +1 -1
  39. package/lib/blobManager.js +14 -10
  40. package/lib/blobManager.js.map +1 -1
  41. package/lib/connectionTelemetry.d.ts.map +1 -1
  42. package/lib/connectionTelemetry.js +63 -23
  43. package/lib/connectionTelemetry.js.map +1 -1
  44. package/lib/containerRuntime.d.ts +12 -4
  45. package/lib/containerRuntime.d.ts.map +1 -1
  46. package/lib/containerRuntime.js +62 -16
  47. package/lib/containerRuntime.js.map +1 -1
  48. package/lib/opTelemetry.d.ts +22 -0
  49. package/lib/opTelemetry.d.ts.map +1 -0
  50. package/lib/opTelemetry.js +55 -0
  51. package/lib/opTelemetry.js.map +1 -0
  52. package/lib/orderedClientElection.d.ts +57 -6
  53. package/lib/orderedClientElection.d.ts.map +1 -1
  54. package/lib/orderedClientElection.js +140 -25
  55. package/lib/orderedClientElection.js.map +1 -1
  56. package/lib/packageVersion.d.ts +1 -1
  57. package/lib/packageVersion.d.ts.map +1 -1
  58. package/lib/packageVersion.js +1 -1
  59. package/lib/packageVersion.js.map +1 -1
  60. package/lib/summarizerClientElection.d.ts +2 -0
  61. package/lib/summarizerClientElection.d.ts.map +1 -1
  62. package/lib/summarizerClientElection.js +7 -2
  63. package/lib/summarizerClientElection.js.map +1 -1
  64. package/lib/summarizerTypes.d.ts +9 -0
  65. package/lib/summarizerTypes.d.ts.map +1 -1
  66. package/lib/summarizerTypes.js.map +1 -1
  67. package/lib/summaryGenerator.d.ts.map +1 -1
  68. package/lib/summaryGenerator.js +1 -1
  69. package/lib/summaryGenerator.js.map +1 -1
  70. package/lib/summaryManager.d.ts.map +1 -1
  71. package/lib/summaryManager.js +14 -3
  72. package/lib/summaryManager.js.map +1 -1
  73. package/package.json +47 -15
  74. package/src/blobManager.ts +19 -11
  75. package/src/connectionTelemetry.ts +110 -19
  76. package/src/containerRuntime.ts +85 -19
  77. package/src/opTelemetry.ts +71 -0
  78. package/src/orderedClientElection.ts +154 -25
  79. package/src/packageVersion.ts +1 -1
  80. package/src/summarizerClientElection.ts +7 -2
  81. package/src/summarizerTypes.ts +9 -0
  82. package/src/summaryGenerator.ts +9 -1
  83. package/src/summaryManager.ts +15 -4
@@ -91,7 +91,6 @@ import {
91
91
  import {
92
92
  addBlobToSummary,
93
93
  addTreeToSummary,
94
- convertToSummaryTree,
95
94
  createRootSummarizerNodeWithGC,
96
95
  IRootSummarizerNodeWithGC,
97
96
  RequestParser,
@@ -100,6 +99,7 @@ import {
100
99
  requestFluidObject,
101
100
  responseToException,
102
101
  seqFromTree,
102
+ calculateStats,
103
103
  } from "@fluidframework/runtime-utils";
104
104
  import { v4 as uuid } from "uuid";
105
105
  import { ContainerFluidHandleContext } from "./containerHandleContext";
@@ -152,6 +152,7 @@ import {
152
152
  isDataStoreAliasMessage,
153
153
  } from "./dataStore";
154
154
  import { BindBatchTracker } from "./batchTracker";
155
+ import { OpTracker } from "./opTelemetry";
155
156
 
156
157
  export enum ContainerMessageType {
157
158
  // An op to be delivered to store
@@ -277,8 +278,8 @@ export interface ISummaryRuntimeOptions {
277
278
  * Options for container runtime.
278
279
  */
279
280
  export interface IContainerRuntimeOptions {
280
- summaryOptions?: ISummaryRuntimeOptions;
281
- gcOptions?: IGCRuntimeOptions;
281
+ readonly summaryOptions?: ISummaryRuntimeOptions;
282
+ readonly gcOptions?: IGCRuntimeOptions;
282
283
  /**
283
284
  * Affects the behavior while loading the runtime when the data verification check which
284
285
  * compares the DeltaManager sequence number (obtained from protocol in summary) to the
@@ -287,13 +288,20 @@ export interface IContainerRuntimeOptions {
287
288
  * 2. "log" will log an error event to telemetry, but still continue to load.
288
289
  * 3. "bypass" will skip the check entirely. This is not recommended.
289
290
  */
290
- loadSequenceNumberVerification?: "close" | "log" | "bypass";
291
+ readonly loadSequenceNumberVerification?: "close" | "log" | "bypass";
291
292
  /**
292
293
  * Should the runtime use data store aliasing for creating root datastores.
293
294
  * In case of aliasing conflicts, the runtime will raise an exception which does
294
295
  * not effect the status of the container.
295
296
  */
296
- useDataStoreAliasing?: boolean;
297
+ readonly useDataStoreAliasing?: boolean;
298
+ /**
299
+ * Sets the flush mode for the runtime. In Immediate flush mode the runtime will immediately
300
+ * send all operations to the driver layer, while in TurnBased the operations will be buffered
301
+ * and then sent them as a single batch at the end of the turn.
302
+ * By default, flush mode is TurnBased.
303
+ */
304
+ readonly flushMode?: FlushMode;
297
305
  }
298
306
 
299
307
  type IRuntimeMessageMetadata = undefined | {
@@ -347,6 +355,13 @@ const maxOpSizeInBytesKey = "Fluid.ContainerRuntime.MaxOpSizeInBytes";
347
355
  // to not reach the 1MB limits in socket.io and Kafka.
348
356
  const defaultMaxOpSizeInBytes = 768000;
349
357
 
358
+ // By default, the size of the contents for the incoming ops is tracked.
359
+ // However, in certain situations, this may incur a performance hit.
360
+ // The feature-gate below can be used to disable this feature.
361
+ const disableOpTrackingKey = "Fluid.ContainerRuntime.DisableOpTracking";
362
+
363
+ const defaultFlushMode = FlushMode.TurnBased;
364
+
350
365
  export enum RuntimeMessage {
351
366
  FluidDataStoreOp = "component",
352
367
  Attach = "attach",
@@ -394,6 +409,7 @@ class ScheduleManagerCore {
394
409
  private currentBatchClientId: string | undefined;
395
410
  private localPaused = false;
396
411
  private timePaused = 0;
412
+ private batchCount = 0;
397
413
 
398
414
  constructor(
399
415
  private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
@@ -482,14 +498,30 @@ class ScheduleManagerCore {
482
498
  this.deltaManager.inbound.pause();
483
499
  }
484
500
 
485
- private resumeQueue(startBatch: number, endBatch: number) {
501
+ private resumeQueue(startBatch: number, messageEndBatch: ISequencedDocumentMessage) {
502
+ const endBatch = messageEndBatch.sequenceNumber;
503
+ const duration = performance.now() - this.timePaused;
504
+
505
+ this.batchCount++;
506
+ if (this.batchCount % 1000 === 1) {
507
+ this.logger.sendTelemetryEvent({
508
+ eventName: "BatchStats",
509
+ sequenceNumber: endBatch,
510
+ length: endBatch - startBatch + 1,
511
+ msnDistance: endBatch - messageEndBatch.minimumSequenceNumber,
512
+ duration,
513
+ batchCount: this.batchCount,
514
+ interrupted: this.localPaused,
515
+ });
516
+ }
517
+
486
518
  // Return early if no change in value
487
519
  if (!this.localPaused) {
488
520
  return;
489
521
  }
490
522
 
491
523
  this.localPaused = false;
492
- const duration = performance.now() - this.timePaused;
524
+
493
525
  // Random round number - we want to know when batch waiting paused op processing.
494
526
  if (duration > latencyThreshold) {
495
527
  this.logger.sendErrorEvent({
@@ -564,7 +596,7 @@ class ScheduleManagerCore {
564
596
  } else if (batchMetadata === false) {
565
597
  assert(this.pauseSequenceNumber !== undefined, 0x2a0 /* "batch presence was validated above" */);
566
598
  // Batch is complete, we can process it!
567
- this.resumeQueue(this.pauseSequenceNumber, message.sequenceNumber);
599
+ this.resumeQueue(this.pauseSequenceNumber, message);
568
600
  this.pauseSequenceNumber = undefined;
569
601
  this.currentBatchClientId = undefined;
570
602
  } else {
@@ -710,6 +742,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
710
742
  gcOptions = {},
711
743
  loadSequenceNumberVerification = "close",
712
744
  useDataStoreAliasing = false,
745
+ flushMode = defaultFlushMode,
713
746
  } = runtimeOptions;
714
747
 
715
748
  // We pack at data store level only. If isolated channels are disabled,
@@ -805,6 +838,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
805
838
  gcOptions,
806
839
  loadSequenceNumberVerification,
807
840
  useDataStoreAliasing,
841
+ flushMode,
808
842
  },
809
843
  containerScope,
810
844
  logger,
@@ -909,7 +943,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
909
943
  private readonly defaultMaxConsecutiveReconnects = 15;
910
944
 
911
945
  private _orderSequentiallyCalls: number = 0;
912
- private _flushMode: FlushMode = FlushMode.TurnBased;
946
+ private _flushMode: FlushMode;
913
947
  private needsFlush = false;
914
948
  private flushTrigger = false;
915
949
 
@@ -983,6 +1017,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
983
1017
 
984
1018
  private readonly createContainerMetadata: ICreateContainerMetadata;
985
1019
  private summaryCount: number | undefined;
1020
+ private readonly opTracker: OpTracker;
986
1021
 
987
1022
  private constructor(
988
1023
  private readonly context: IContainerContext,
@@ -1037,6 +1072,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1037
1072
  this.maxConsecutiveReconnects =
1038
1073
  this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? this.defaultMaxConsecutiveReconnects;
1039
1074
 
1075
+ this._flushMode = runtimeOptions.flushMode;
1040
1076
  this.garbageCollector = GarbageCollector.create(
1041
1077
  this,
1042
1078
  this.runtimeOptions.gcOptions,
@@ -1277,6 +1313,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1277
1313
 
1278
1314
  ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
1279
1315
  BindBatchTracker(this, this.logger);
1316
+ this.opTracker = new OpTracker(this.deltaManager, this.mc.config.getBoolean(disableOpTrackingKey) === true);
1280
1317
  }
1281
1318
 
1282
1319
  public dispose(error?: Error): void {
@@ -1446,13 +1483,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1446
1483
  const electedSummarizerContent = JSON.stringify(this.summarizerClientElection?.serialize());
1447
1484
  addBlobToSummary(summaryTree, electedSummarizerBlobName, electedSummarizerContent);
1448
1485
  }
1449
- const snapshot = this.blobManager.snapshot();
1450
1486
 
1487
+ const summary = this.blobManager.summarize();
1451
1488
  // Some storage (like git) doesn't allow empty tree, so we can omit it.
1452
1489
  // and the blob manager can handle the tree not existing when loading
1453
- if (snapshot.entries.length !== 0) {
1454
- const blobsTree = convertToSummaryTree(snapshot, false);
1455
- addTreeToSummary(summaryTree, blobsTreeName, blobsTree);
1490
+ if (Object.keys(summary.summary.tree).length > 0) {
1491
+ addTreeToSummary(summaryTree, blobsTreeName, summary);
1456
1492
  }
1457
1493
 
1458
1494
  if (this.garbageCollector.writeDataAtRoot) {
@@ -1700,6 +1736,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1700
1736
  return;
1701
1737
  }
1702
1738
 
1739
+ this.mc.logger.sendTelemetryEvent({
1740
+ eventName: "FlushMode Updated",
1741
+ old: this._flushMode,
1742
+ new: mode,
1743
+ });
1744
+
1703
1745
  // Flush any pending batches if switching to immediate
1704
1746
  if (mode === FlushMode.Immediate) {
1705
1747
  this.flush();
@@ -1864,6 +1906,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1864
1906
  Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props).realize();
1865
1907
  if (isRoot) {
1866
1908
  fluidDataStore.bindToContext();
1909
+ this.logger.sendTelemetryEvent({
1910
+ eventName: "Root datastore with props",
1911
+ hasProps: props !== undefined,
1912
+ });
1867
1913
  }
1868
1914
  return channelToDataStore(fluidDataStore, id, this, this.dataStores, this.mc.logger);
1869
1915
  }
@@ -2041,11 +2087,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2041
2087
  gcStats = await this.collectGarbage({ logger: summaryLogger, runSweep, fullGC });
2042
2088
  }
2043
2089
 
2044
- const summarizeResult = await this.summarizerNode.summarize(fullTree, trackState);
2045
- assert(summarizeResult.summary.type === SummaryType.Tree,
2090
+ const { stats, summary } = await this.summarizerNode.summarize(fullTree, trackState);
2091
+
2092
+ assert(summary.type === SummaryType.Tree,
2046
2093
  0x12f /* "Container Runtime's summarize should always return a tree" */);
2047
2094
 
2048
- return { ...summarizeResult, gcStats } as IRootSummaryTreeWithStats;
2095
+ return { stats, summary, gcStats };
2049
2096
  }
2050
2097
 
2051
2098
  /**
@@ -2144,6 +2191,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2144
2191
  await this.deltaManager.inbound.pause();
2145
2192
 
2146
2193
  const summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
2194
+ const minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
2147
2195
  const message = `Summary @${summaryRefSeqNum}:${this.deltaManager.minimumSequenceNumber}`;
2148
2196
 
2149
2197
  // We should be here is we haven't processed be here. If we are of if the last message's sequence number
@@ -2188,7 +2236,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2188
2236
 
2189
2237
  let continueResult = checkContinue();
2190
2238
  if (!continueResult.continue) {
2191
- return { stage: "base", referenceSequenceNumber: summaryRefSeqNum, error: continueResult.error };
2239
+ return {
2240
+ stage: "base",
2241
+ referenceSequenceNumber: summaryRefSeqNum,
2242
+ minimumSequenceNumber,
2243
+ error: continueResult.error,
2244
+ };
2192
2245
  }
2193
2246
 
2194
2247
  // increment summary count
@@ -2211,7 +2264,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2211
2264
  runGC: this.garbageCollector.shouldRunGC,
2212
2265
  });
2213
2266
  } catch (error) {
2214
- return { stage: "base", referenceSequenceNumber: summaryRefSeqNum, error };
2267
+ return {
2268
+ stage: "base",
2269
+ referenceSequenceNumber: summaryRefSeqNum,
2270
+ minimumSequenceNumber,
2271
+ error,
2272
+ };
2215
2273
  }
2216
2274
  const { summary: summaryTree, stats: partialStats } = summarizeResult;
2217
2275
 
@@ -2226,15 +2284,23 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2226
2284
  assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
2227
2285
  const handleCount = Object.values(dataStoreTree.tree).filter(
2228
2286
  (value) => value.type === SummaryType.Handle).length;
2287
+ const gcSummaryTreeStats = summaryTree.tree[gcTreeKey]
2288
+ ? calculateStats((summaryTree.tree[gcTreeKey] as ISummaryTree))
2289
+ : undefined;
2229
2290
 
2230
2291
  const summaryStats: IGeneratedSummaryStats = {
2231
2292
  dataStoreCount: this.dataStores.size,
2232
2293
  summarizedDataStoreCount: this.dataStores.size - handleCount,
2233
2294
  gcStateUpdatedDataStoreCount: summarizeResult.gcStats?.updatedDataStoreCount,
2295
+ gcBlobNodeCount: gcSummaryTreeStats?.blobNodeCount,
2296
+ gcTotalBlobsSize: gcSummaryTreeStats?.totalBlobSize,
2297
+ opsSizesSinceLastSummary: this.opTracker.opsSizeAccumulator,
2298
+ nonSystemOpsSinceLastSummary: this.opTracker.nonSystemOpCount,
2234
2299
  ...partialStats,
2235
2300
  };
2236
2301
  const generateSummaryData = {
2237
2302
  referenceSequenceNumber: summaryRefSeqNum,
2303
+ minimumSequenceNumber,
2238
2304
  summaryTree,
2239
2305
  summaryStats,
2240
2306
  generateDuration: trace.trace().duration,
@@ -2301,7 +2367,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2301
2367
  } as const;
2302
2368
 
2303
2369
  this.summarizerNode.completeSummary(handle);
2304
-
2370
+ this.opTracker.reset();
2305
2371
  return submitData;
2306
2372
  } finally {
2307
2373
  // Cleanup wip summary in case of failure
@@ -0,0 +1,71 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { IDeltaManager } from "@fluidframework/container-definitions";
7
+ import {
8
+ IDocumentMessage,
9
+ ISequencedDocumentMessage,
10
+ ISequencedDocumentSystemMessage,
11
+ } from "@fluidframework/protocol-definitions";
12
+ import { isSystemMessage } from "@fluidframework/protocol-base";
13
+
14
+ export class OpTracker {
15
+ /**
16
+ * Used for storing the message content size when
17
+ * the message is pushed onto the inbound queue.
18
+ */
19
+ private readonly messageSize = new Map<number, number>();
20
+ private _nonSystemOpCount: number = 0;
21
+ public get nonSystemOpCount(): number {
22
+ return this._nonSystemOpCount;
23
+ }
24
+
25
+ private _opsSizeAccumulator: number = 0;
26
+ public get opsSizeAccumulator(): number {
27
+ return this._opsSizeAccumulator;
28
+ }
29
+
30
+ public constructor(
31
+ deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
32
+ disabled: boolean,
33
+ ) {
34
+ if (disabled) {
35
+ return;
36
+ }
37
+
38
+ // Record the message content size when we receive it.
39
+ // We should not log this value, as summarization can happen between the time the message
40
+ // is received and until it is processed (the 'op' event).
41
+ deltaManager.inbound.on("push", (message: ISequencedDocumentMessage) => {
42
+ // Some messages my already have string contents at this point,
43
+ // so stringifying them again will add inaccurate overhead.
44
+ const messageContent = typeof message.contents === "string" ?
45
+ message.contents :
46
+ JSON.stringify(message.contents);
47
+ const messageData = OpTracker.messageHasData(message) ? message.data : "";
48
+ this.messageSize[OpTracker.messageId(message)] = messageContent.length + messageData.length;
49
+ });
50
+
51
+ deltaManager.on("op", (message: ISequencedDocumentMessage) => {
52
+ this._nonSystemOpCount += isSystemMessage(message) ? 0 : 1;
53
+ const id = OpTracker.messageId(message);
54
+ this._opsSizeAccumulator += this.messageSize[id] ?? 0;
55
+ this.messageSize.delete(id);
56
+ });
57
+ }
58
+
59
+ private static messageId(message: ISequencedDocumentMessage): number {
60
+ return message.sequenceNumber;
61
+ }
62
+
63
+ private static messageHasData(message: ISequencedDocumentMessage): message is ISequencedDocumentSystemMessage {
64
+ return (message as ISequencedDocumentSystemMessage).data !== undefined;
65
+ }
66
+
67
+ public reset() {
68
+ this._nonSystemOpCount = 0;
69
+ this._opsSizeAccumulator = 0;
70
+ }
71
+ }
@@ -6,8 +6,10 @@
6
6
  import { IEvent, IEventProvider, ITelemetryLogger } from "@fluidframework/common-definitions";
7
7
  import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
8
8
  import { IDeltaManager } from "@fluidframework/container-definitions";
9
+ import { UsageError } from "@fluidframework/container-utils";
9
10
  import { IClient, IQuorumClients, ISequencedClient } from "@fluidframework/protocol-definitions";
10
11
  import { ChildLogger } from "@fluidframework/telemetry-utils";
12
+ import { summarizerClientType } from "./summarizerClientElection";
11
13
 
12
14
  // helper types for recursive readonly.
13
15
  // eslint-disable-next-line @typescript-eslint/ban-types
@@ -206,16 +208,26 @@ export interface IOrderedClientElectionEvents extends IEvent {
206
208
  export interface ISerializedElection {
207
209
  /** Sequence number at the time of the latest election. */
208
210
  readonly electionSequenceNumber: number;
209
- /** Most recently elected client id. */
211
+ /** Most recently elected client id. This is either:
212
+ * 1. the interactive elected parent client, in which case electedClientId === electedParentId,
213
+ * and the SummaryManager on the elected client will spawn a summarizer client, or
214
+ * 2. the non-interactive summarizer client itself. */
210
215
  readonly electedClientId: string | undefined;
216
+ /** Most recently elected parent client id. This is always an interactive client. */
217
+ readonly electedParentId: string | undefined;
211
218
  }
212
219
 
213
220
  /** Contract for maintaining a deterministic client election based on eligibility. */
214
221
  export interface IOrderedClientElection extends IEventProvider<IOrderedClientElectionEvents> {
215
222
  /** Count of eligible clients in the collection. */
216
223
  readonly eligibleCount: number;
217
- /** Currently elected client. */
224
+ /** Currently elected client. This is either:
225
+ * 1. the interactive elected parent client, in which case electedClientId === electedParentId,
226
+ * and the SummaryManager on the elected client will spawn a summarizer client, or
227
+ * 2. the non-interactive summarizer client itself. */
218
228
  readonly electedClient: ITrackedClient | undefined;
229
+ /** Currently elected parent client. This is always an interactive client. */
230
+ readonly electedParent: ITrackedClient | undefined;
219
231
  /** Sequence number of most recent election. */
220
232
  readonly electionSequenceNumber: number;
221
233
  /** Marks the currently elected client as invalid, and elects the next eligible client. */
@@ -241,16 +253,50 @@ export class OrderedClientElection
241
253
  implements IOrderedClientElection {
242
254
  private _eligibleCount: number = 0;
243
255
  private _electedClient: ILinkedClient | undefined;
256
+ private _electedParent: ILinkedClient | undefined;
244
257
  private _electionSequenceNumber: number;
245
258
 
246
259
  public get eligibleCount() {
247
260
  return this._eligibleCount;
248
261
  }
262
+ public get electionSequenceNumber() {
263
+ return this._electionSequenceNumber;
264
+ }
265
+
266
+ /**
267
+ * OrderedClientCollection tracks electedClient and electedParent separately. This allows us to handle the case
268
+ * where a new interactive parent client has been elected, but the summarizer is still doing work, so
269
+ * a new summarizer should not yet be spawned. In this case, changing electedParent will cause SummaryManager
270
+ * to stop the current summarizer, but a new summarizer will not be spawned until the old summarizer client has
271
+ * left the quorum.
272
+ *
273
+ * Details:
274
+ *
275
+ * electedParent is the interactive client that has been elected to spawn a summarizer. It is typically the oldest
276
+ * eligible interactive client in the quorum. Only the electedParent is permitted to spawn a summarizer.
277
+ * Once elected, this client will remain the electedParent until it leaves the quorum or the summarizer that
278
+ * it spawned stops producing summaries, at which point a new electedParent will be chosen.
279
+ *
280
+ * electedClient is the non-interactive summarizer client if one exists. If not, then electedClient is equal to
281
+ * electedParent. If electedParent === electedClient, this is the signal for electedParent to spawn a new
282
+ * electedClient. Once a summarizer client becomes electedClient, a new summarizer will not be spawned until
283
+ * electedClient leaves the quorum.
284
+ *
285
+ * A typical sequence looks like this:
286
+ * i. Begin by electing A. electedParent === A, electedClient === A.
287
+ * ii. SummaryManager running on A spawns a summarizer client, A'. electedParent === A, electedClient === A'
288
+ * iii. A' stops producing summaries. A new parent client, B, is elected. electedParent === B, electedClient === A'
289
+ * iv. SummaryManager running on A detects the change to electedParent and tells the summarizer to stop, but A'
290
+ * is in mid-summarization. No new summarizer is spawned, as electedParent !== electedClient.
291
+ * v. A' completes its summary, and the summarizer and backing client are torn down.
292
+ * vi. A' leaves the quorum, and B takes its place as electedClient. electedParent === B, electedClient === B
293
+ * vii. SummaryManager running on B spawns a summarizer client, B'. electedParent === B, electedClient === B'
294
+ */
249
295
  public get electedClient() {
250
296
  return this._electedClient;
251
297
  }
252
- public get electionSequenceNumber() {
253
- return this._electionSequenceNumber;
298
+ public get electedParent() {
299
+ return this._electedParent;
254
300
  }
255
301
 
256
302
  constructor(
@@ -262,11 +308,20 @@ export class OrderedClientElection
262
308
  ) {
263
309
  super();
264
310
  let initialClient: ILinkedClient | undefined;
311
+ let initialParent: ILinkedClient | undefined;
265
312
  for (const client of orderedClientCollection.getAllClients()) {
266
313
  this.addClient(client, 0);
267
314
  if (typeof initialState !== "number") {
268
315
  if (client.clientId === initialState.electedClientId) {
269
316
  initialClient = client;
317
+ if (initialState.electedParentId === undefined &&
318
+ client.client.details.type !== summarizerClientType) {
319
+ // If there was no elected parent in the serialized data, use this one.
320
+ initialParent = client;
321
+ }
322
+ }
323
+ if (client.clientId === initialState.electedParentId) {
324
+ initialParent = client;
270
325
  }
271
326
  }
272
327
  }
@@ -288,7 +343,7 @@ export class OrderedClientElection
288
343
  });
289
344
  } else if (initialClient !== undefined && !isEligibleFn(initialClient)) {
290
345
  // Initially elected client is ineligible, so elect next eligible client.
291
- initialClient = this.findFirstEligibleClient(initialClient);
346
+ initialClient = initialParent = this.findFirstEligibleParent(initialParent);
292
347
  logger.sendErrorEvent({
293
348
  eventName: "InitialElectedClientIneligible",
294
349
  electionSequenceNumber: initialState.electionSequenceNumber,
@@ -296,31 +351,53 @@ export class OrderedClientElection
296
351
  electedClientId: initialClient?.clientId,
297
352
  });
298
353
  }
354
+ this._electedParent = initialParent;
299
355
  this._electedClient = initialClient;
300
356
  this._electionSequenceNumber = initialState.electionSequenceNumber;
301
357
  }
302
358
  }
303
359
 
304
- /** Tries changing the elected client, raising an event if it is different. */
360
+ /** Tries changing the elected client, raising an event if it is different.
361
+ * Note that this function does no eligibility or suitability checks. If we get here, then
362
+ * we will set _electedClient, and we will set _electedParent if this is an interactive client.
363
+ */
305
364
  private tryElectingClient(client: ILinkedClient | undefined, sequenceNumber: number): void {
306
- this._electionSequenceNumber = sequenceNumber;
307
- if (this._electedClient === client) {
308
- return;
309
- }
365
+ let change = false;
366
+ const isSummarizerClient = client?.client.details.type === summarizerClientType;
310
367
  const prevClient = this._electedClient;
311
- this._electedClient = client;
312
- this.emit("election", client, sequenceNumber, prevClient);
368
+ if (this._electedClient !== client) {
369
+ // Changing the elected client. Record the sequence number and note that we have to fire an event.
370
+ this._electionSequenceNumber = sequenceNumber;
371
+ this._electedClient = client;
372
+ change = true;
373
+ }
374
+ if (this._electedParent !== client && !isSummarizerClient) {
375
+ // Changing the elected parent as well.
376
+ this._electedParent = client;
377
+ change = true;
378
+ }
379
+ if (change) {
380
+ this.emit("election", client, sequenceNumber, prevClient);
381
+ }
382
+ }
383
+
384
+ private tryElectingParent(client: ILinkedClient | undefined, sequenceNumber: number): void {
385
+ if (this._electedParent !== client) {
386
+ this._electedParent = client;
387
+ this.emit("election", this._electedClient, sequenceNumber, this._electedClient);
388
+ }
313
389
  }
314
390
 
315
391
  /**
316
- * Helper function to find the first eligible client starting with the passed in client,
392
+ * Helper function to find the first eligible parent client starting with the passed in client,
317
393
  * or undefined if none are eligible.
318
394
  * @param client - client to start checking
319
395
  * @returns oldest eligible client starting with passed in client or undefined if none.
320
396
  */
321
- private findFirstEligibleClient(client: ILinkedClient | undefined): ILinkedClient | undefined {
397
+ private findFirstEligibleParent(client: ILinkedClient | undefined): ILinkedClient | undefined {
322
398
  let candidateClient = client;
323
- while (candidateClient !== undefined && !this.isEligibleFn(candidateClient)) {
399
+ while (candidateClient !== undefined &&
400
+ (!this.isEligibleFn(candidateClient) || candidateClient.client.details.type === summarizerClientType)) {
324
401
  candidateClient = candidateClient.youngerClient;
325
402
  }
326
403
  return candidateClient;
@@ -335,10 +412,16 @@ export class OrderedClientElection
335
412
  private addClient(client: ILinkedClient, sequenceNumber: number): void {
336
413
  if (this.isEligibleFn(client)) {
337
414
  this._eligibleCount++;
338
- if (this._electedClient === undefined) {
339
- // Automatically elect latest client
415
+ const newClientIsSummarizer = client.client.details.type === summarizerClientType;
416
+ const electedClientIsSummarizer = this._electedClient?.client.details.type === summarizerClientType;
417
+ // Note that we allow a summarizer client to supercede an interactive client as elected client.
418
+ if (this._electedClient === undefined || (!electedClientIsSummarizer && newClientIsSummarizer)) {
340
419
  this.tryElectingClient(client, sequenceNumber);
341
420
  }
421
+ else if (this._electedParent === undefined && !newClientIsSummarizer) {
422
+ // This is an odd case. If the _electedClient is set, the _electedParent should be as well.
423
+ this.tryElectingParent(client, sequenceNumber);
424
+ }
342
425
  }
343
426
  }
344
427
 
@@ -352,9 +435,33 @@ export class OrderedClientElection
352
435
  if (this.isEligibleFn(client)) {
353
436
  this._eligibleCount--;
354
437
  if (this._electedClient === client) {
355
- // Automatically shift to next oldest client
356
- const nextClient = this.findFirstEligibleClient(this._electedClient.youngerClient);
357
- this.tryElectingClient(nextClient, sequenceNumber);
438
+ // Removing the _electedClient. There are 2 possible cases:
439
+ if (this._electedParent !== client) {
440
+ // 1. The _electedClient is a summarizer that we've been allowing to finish its work.
441
+ // Let the _electedParent become the _electedClient so that it can start its own summarizer.
442
+ if (this._electedClient.client.details.type !== summarizerClientType) {
443
+ throw new UsageError("Elected client should be a summarizer client 1");
444
+ }
445
+ this.tryElectingClient(this._electedParent, sequenceNumber);
446
+ }
447
+ else {
448
+ // 2. The _electedClient is an interactive client that has left the quorum.
449
+ // Automatically shift to next oldest client.
450
+ const nextClient = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
451
+ this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
452
+ this.tryElectingClient(nextClient, sequenceNumber);
453
+ }
454
+ }
455
+ else if (this._electedParent === client) {
456
+ // Removing the _electedParent (but not _electedClient).
457
+ // Shift to the next oldest parent, but do not replace the _electedClient,
458
+ // which is a summarizer that is still doing work.
459
+ if (this._electedClient?.client.details.type !== summarizerClientType) {
460
+ throw new UsageError("Elected client should be a summarizer client 2");
461
+ }
462
+ const nextParent = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
463
+ this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
464
+ this.tryElectingParent(nextParent, sequenceNumber);
358
465
  }
359
466
  }
360
467
  }
@@ -363,24 +470,46 @@ export class OrderedClientElection
363
470
  return this.orderedClientCollection.getAllClients().filter(this.isEligibleFn);
364
471
  }
365
472
 
473
+ /** Advance election to the next-oldest client. This is called if the current parent is leaving the quorum,
474
+ * or if the current summarizer is not responsive and we want to stop it and spawn a new one.
475
+ */
366
476
  public incrementElectedClient(sequenceNumber: number): void {
367
- const nextClient = this.findFirstEligibleClient(this._electedClient?.youngerClient);
368
- this.tryElectingClient(nextClient, sequenceNumber);
477
+ const nextClient = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
478
+ this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
479
+ if (this._electedClient === undefined || this._electedClient === this._electedParent) {
480
+ this.tryElectingClient(nextClient, sequenceNumber);
481
+ }
482
+ else {
483
+ // The _electedClient is a summarizer and should not be replaced until it leaves the quorum.
484
+ // Changing the _electedParent will stop the summarizer.
485
+ this.tryElectingParent(nextClient, sequenceNumber);
486
+ }
369
487
  }
370
488
 
489
+ /** (Re-)start election with the oldest client in the quorum. This is called if we need to summarize
490
+ * and no client has been elected.
491
+ */
371
492
  public resetElectedClient(sequenceNumber: number): void {
372
- const firstClient = this.findFirstEligibleClient(this.orderedClientCollection.oldestClient);
373
- this.tryElectingClient(firstClient, sequenceNumber);
493
+ const firstClient = this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
494
+ if (this._electedClient === undefined || this._electedClient === this._electedParent) {
495
+ this.tryElectingClient(firstClient, sequenceNumber);
496
+ }
497
+ else {
498
+ // The _electedClient is a summarizer and should not be replaced until it leaves the quorum.
499
+ // Changing the _electedParent will stop the summarizer.
500
+ this.tryElectingParent(firstClient, sequenceNumber);
501
+ }
374
502
  }
375
503
 
376
504
  public peekNextElectedClient(): ITrackedClient | undefined {
377
- return this.findFirstEligibleClient(this._electedClient?.youngerClient);
505
+ return this.findFirstEligibleParent(this._electedParent?.youngerClient);
378
506
  }
379
507
 
380
508
  public serialize(): ISerializedElection {
381
509
  return {
382
510
  electionSequenceNumber: this.electionSequenceNumber,
383
511
  electedClientId: this.electedClient?.clientId,
512
+ electedParentId: this.electedParent?.clientId,
384
513
  };
385
514
  }
386
515
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "0.58.2002";
9
+ export const pkgVersion = "0.58.3000-61081";