@fluidframework/container-runtime 0.59.4001 → 1.1.0-75972

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 (157) hide show
  1. package/.eslintrc.js +1 -1
  2. package/dist/blobManager.d.ts +2 -2
  3. package/dist/blobManager.d.ts.map +1 -1
  4. package/dist/blobManager.js +12 -11
  5. package/dist/blobManager.js.map +1 -1
  6. package/dist/connectionTelemetry.d.ts +19 -0
  7. package/dist/connectionTelemetry.d.ts.map +1 -1
  8. package/dist/connectionTelemetry.js +23 -23
  9. package/dist/connectionTelemetry.js.map +1 -1
  10. package/dist/containerRuntime.d.ts +137 -29
  11. package/dist/containerRuntime.d.ts.map +1 -1
  12. package/dist/containerRuntime.js +338 -118
  13. package/dist/containerRuntime.js.map +1 -1
  14. package/dist/dataStore.d.ts.map +1 -1
  15. package/dist/dataStore.js +14 -3
  16. package/dist/dataStore.js.map +1 -1
  17. package/dist/dataStoreContext.d.ts +4 -2
  18. package/dist/dataStoreContext.d.ts.map +1 -1
  19. package/dist/dataStoreContext.js +16 -5
  20. package/dist/dataStoreContext.js.map +1 -1
  21. package/dist/dataStoreRegistry.d.ts +0 -4
  22. package/dist/dataStoreRegistry.d.ts.map +1 -1
  23. package/dist/dataStoreRegistry.js +12 -1
  24. package/dist/dataStoreRegistry.js.map +1 -1
  25. package/dist/dataStores.d.ts +4 -3
  26. package/dist/dataStores.d.ts.map +1 -1
  27. package/dist/dataStores.js +13 -7
  28. package/dist/dataStores.js.map +1 -1
  29. package/dist/garbageCollection.d.ts +23 -27
  30. package/dist/garbageCollection.d.ts.map +1 -1
  31. package/dist/garbageCollection.js +44 -119
  32. package/dist/garbageCollection.js.map +1 -1
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/orderedClientElection.js +0 -4
  38. package/dist/orderedClientElection.js.map +1 -1
  39. package/dist/packageVersion.d.ts +1 -1
  40. package/dist/packageVersion.d.ts.map +1 -1
  41. package/dist/packageVersion.js +1 -1
  42. package/dist/packageVersion.js.map +1 -1
  43. package/dist/pendingStateManager.d.ts +30 -29
  44. package/dist/pendingStateManager.d.ts.map +1 -1
  45. package/dist/pendingStateManager.js +72 -109
  46. package/dist/pendingStateManager.js.map +1 -1
  47. package/dist/runningSummarizer.d.ts +4 -3
  48. package/dist/runningSummarizer.d.ts.map +1 -1
  49. package/dist/runningSummarizer.js +11 -6
  50. package/dist/runningSummarizer.js.map +1 -1
  51. package/dist/serializedSnapshotStorage.d.ts +58 -0
  52. package/dist/serializedSnapshotStorage.d.ts.map +1 -0
  53. package/dist/serializedSnapshotStorage.js +108 -0
  54. package/dist/serializedSnapshotStorage.js.map +1 -0
  55. package/dist/summarizer.d.ts +11 -4
  56. package/dist/summarizer.d.ts.map +1 -1
  57. package/dist/summarizer.js +18 -9
  58. package/dist/summarizer.js.map +1 -1
  59. package/dist/summarizerHeuristics.d.ts +5 -3
  60. package/dist/summarizerHeuristics.d.ts.map +1 -1
  61. package/dist/summarizerHeuristics.js +10 -3
  62. package/dist/summarizerHeuristics.js.map +1 -1
  63. package/dist/summarizerTypes.d.ts +4 -2
  64. package/dist/summarizerTypes.d.ts.map +1 -1
  65. package/dist/summarizerTypes.js.map +1 -1
  66. package/dist/summaryManager.d.ts +3 -3
  67. package/dist/summaryManager.d.ts.map +1 -1
  68. package/dist/summaryManager.js +7 -7
  69. package/dist/summaryManager.js.map +1 -1
  70. package/garbageCollection.md +9 -1
  71. package/lib/blobManager.d.ts +2 -2
  72. package/lib/blobManager.d.ts.map +1 -1
  73. package/lib/blobManager.js +12 -11
  74. package/lib/blobManager.js.map +1 -1
  75. package/lib/connectionTelemetry.d.ts +19 -0
  76. package/lib/connectionTelemetry.d.ts.map +1 -1
  77. package/lib/connectionTelemetry.js +23 -23
  78. package/lib/connectionTelemetry.js.map +1 -1
  79. package/lib/containerRuntime.d.ts +137 -29
  80. package/lib/containerRuntime.d.ts.map +1 -1
  81. package/lib/containerRuntime.js +341 -121
  82. package/lib/containerRuntime.js.map +1 -1
  83. package/lib/dataStore.d.ts.map +1 -1
  84. package/lib/dataStore.js +15 -4
  85. package/lib/dataStore.js.map +1 -1
  86. package/lib/dataStoreContext.d.ts +4 -2
  87. package/lib/dataStoreContext.d.ts.map +1 -1
  88. package/lib/dataStoreContext.js +16 -5
  89. package/lib/dataStoreContext.js.map +1 -1
  90. package/lib/dataStoreRegistry.d.ts +0 -4
  91. package/lib/dataStoreRegistry.d.ts.map +1 -1
  92. package/lib/dataStoreRegistry.js +12 -1
  93. package/lib/dataStoreRegistry.js.map +1 -1
  94. package/lib/dataStores.d.ts +4 -3
  95. package/lib/dataStores.d.ts.map +1 -1
  96. package/lib/dataStores.js +14 -8
  97. package/lib/dataStores.js.map +1 -1
  98. package/lib/garbageCollection.d.ts +23 -27
  99. package/lib/garbageCollection.d.ts.map +1 -1
  100. package/lib/garbageCollection.js +43 -117
  101. package/lib/garbageCollection.js.map +1 -1
  102. package/lib/index.d.ts +2 -2
  103. package/lib/index.d.ts.map +1 -1
  104. package/lib/index.js +1 -1
  105. package/lib/index.js.map +1 -1
  106. package/lib/orderedClientElection.js +0 -4
  107. package/lib/orderedClientElection.js.map +1 -1
  108. package/lib/packageVersion.d.ts +1 -1
  109. package/lib/packageVersion.d.ts.map +1 -1
  110. package/lib/packageVersion.js +1 -1
  111. package/lib/packageVersion.js.map +1 -1
  112. package/lib/pendingStateManager.d.ts +30 -29
  113. package/lib/pendingStateManager.d.ts.map +1 -1
  114. package/lib/pendingStateManager.js +72 -109
  115. package/lib/pendingStateManager.js.map +1 -1
  116. package/lib/runningSummarizer.d.ts +4 -3
  117. package/lib/runningSummarizer.d.ts.map +1 -1
  118. package/lib/runningSummarizer.js +11 -6
  119. package/lib/runningSummarizer.js.map +1 -1
  120. package/lib/serializedSnapshotStorage.d.ts +58 -0
  121. package/lib/serializedSnapshotStorage.d.ts.map +1 -0
  122. package/lib/serializedSnapshotStorage.js +104 -0
  123. package/lib/serializedSnapshotStorage.js.map +1 -0
  124. package/lib/summarizer.d.ts +11 -4
  125. package/lib/summarizer.d.ts.map +1 -1
  126. package/lib/summarizer.js +18 -9
  127. package/lib/summarizer.js.map +1 -1
  128. package/lib/summarizerHeuristics.d.ts +5 -3
  129. package/lib/summarizerHeuristics.d.ts.map +1 -1
  130. package/lib/summarizerHeuristics.js +10 -3
  131. package/lib/summarizerHeuristics.js.map +1 -1
  132. package/lib/summarizerTypes.d.ts +4 -2
  133. package/lib/summarizerTypes.d.ts.map +1 -1
  134. package/lib/summarizerTypes.js.map +1 -1
  135. package/lib/summaryManager.d.ts +3 -3
  136. package/lib/summaryManager.d.ts.map +1 -1
  137. package/lib/summaryManager.js +7 -7
  138. package/lib/summaryManager.js.map +1 -1
  139. package/package.json +19 -32
  140. package/src/blobManager.ts +29 -15
  141. package/src/connectionTelemetry.ts +60 -39
  142. package/src/containerRuntime.ts +502 -156
  143. package/src/dataStore.ts +21 -4
  144. package/src/dataStoreContext.ts +27 -5
  145. package/src/dataStoreRegistry.ts +8 -1
  146. package/src/dataStores.ts +21 -8
  147. package/src/garbageCollection.ts +81 -166
  148. package/src/index.ts +7 -1
  149. package/src/orderedClientElection.ts +1 -1
  150. package/src/packageVersion.ts +1 -1
  151. package/src/pendingStateManager.ts +104 -123
  152. package/src/runningSummarizer.ts +20 -10
  153. package/src/serializedSnapshotStorage.ts +146 -0
  154. package/src/summarizer.ts +20 -16
  155. package/src/summarizerHeuristics.ts +21 -5
  156. package/src/summarizerTypes.ts +4 -2
  157. package/src/summaryManager.ts +5 -6
@@ -2,8 +2,6 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- // See #9219
6
- /* eslint-disable max-lines */
7
5
  import { EventEmitter } from "events";
8
6
  import { ITelemetryBaseLogger, ITelemetryGenericEvent, ITelemetryLogger } from "@fluidframework/common-definitions";
9
7
  import {
@@ -25,6 +23,7 @@ import {
25
23
  AttachState,
26
24
  ILoaderOptions,
27
25
  LoaderHeader,
26
+ ISnapshotTreeWithBlobContents,
28
27
  } from "@fluidframework/container-definitions";
29
28
  import {
30
29
  IContainerRuntime,
@@ -41,7 +40,6 @@ import {
41
40
  ChildLogger,
42
41
  raiseConnectedEvent,
43
42
  PerformanceEvent,
44
- normalizeError,
45
43
  TaggedLoggerAdapter,
46
44
  MonitoringContext,
47
45
  loggerToMonitoringContext,
@@ -51,6 +49,7 @@ import { DriverHeader, IDocumentStorageService, ISummaryContext } from "@fluidfr
51
49
  import { readAndParse } from "@fluidframework/driver-utils";
52
50
  import {
53
51
  DataCorruptionError,
52
+ DataProcessingError,
54
53
  GenericError,
55
54
  UsageError,
56
55
  extractSafePropertiesFromMessage,
@@ -61,7 +60,7 @@ import {
61
60
  IQuorumClients,
62
61
  ISequencedDocumentMessage,
63
62
  ISignalMessage,
64
- ISummaryConfiguration,
63
+ ISnapshotTree,
65
64
  ISummaryContent,
66
65
  ISummaryTree,
67
66
  MessageType,
@@ -86,9 +85,11 @@ import {
86
85
  channelsTreeName,
87
86
  IAttachMessage,
88
87
  IDataStore,
88
+ ITelemetryContext,
89
89
  } from "@fluidframework/runtime-definitions";
90
90
  import {
91
91
  addBlobToSummary,
92
+ addSummarizeResultToSummary,
92
93
  addTreeToSummary,
93
94
  createRootSummarizerNodeWithGC,
94
95
  IRootSummarizerNodeWithGC,
@@ -99,7 +100,7 @@ import {
99
100
  responseToException,
100
101
  seqFromTree,
101
102
  calculateStats,
102
- addSummarizeResultToSummary,
103
+ TelemetryContext,
103
104
  } from "@fluidframework/runtime-utils";
104
105
  import { GCDataBuilder, trimLeadingAndTrailingSlashes } from "@fluidframework/garbage-collector";
105
106
  import { v4 as uuid } from "uuid";
@@ -108,7 +109,11 @@ import { FluidDataStoreRegistry } from "./dataStoreRegistry";
108
109
  import { Summarizer } from "./summarizer";
109
110
  import { SummaryManager } from "./summaryManager";
110
111
  import { DeltaScheduler } from "./deltaScheduler";
111
- import { ReportOpPerfTelemetry, latencyThreshold } from "./connectionTelemetry";
112
+ import {
113
+ ReportOpPerfTelemetry,
114
+ latencyThreshold,
115
+ IPerfSignalReport,
116
+ } from "./connectionTelemetry";
112
117
  import { IPendingLocalState, PendingStateManager } from "./pendingStateManager";
113
118
  import { pkgVersion } from "./packageVersion";
114
119
  import { BlobManager, IBlobManagerLoadInfo } from "./blobManager";
@@ -154,6 +159,7 @@ import {
154
159
  isDataStoreAliasMessage,
155
160
  } from "./dataStore";
156
161
  import { BindBatchTracker } from "./batchTracker";
162
+ import { ISerializedBaseSnapshotBlobs, SerializedSnapshotStorage } from "./serializedSnapshotStorage";
157
163
  import { OpTracker } from "./opTelemetry";
158
164
 
159
165
  export enum ContainerMessageType {
@@ -190,22 +196,85 @@ export interface ContainerRuntimeMessage {
190
196
  contents: any;
191
197
  type: ContainerMessageType;
192
198
  }
199
+ export interface ISummaryBaseConfiguration {
200
+ /**
201
+ * Delay before first attempt to spawn summarizing container.
202
+ */
203
+ initialSummarizerDelayMs: number;
204
+
205
+ /**
206
+ * Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
207
+ * This defaults to false (disabled) and must be explicitly set to true to enable.
208
+ */
209
+ summarizerClientElection: boolean;
193
210
 
194
- // Consider idle 5s of no activity. And snapshot if a minute has gone by with no snapshot.
195
- const IdleDetectionTime = 5000;
211
+ /**
212
+ * Defines the maximum allowed time to wait for a pending summary ack.
213
+ * The maximum amount of time client will wait for a summarize is the minimum of
214
+ * maxSummarizeAckWaitTime (currently 10 * 60 * 1000) and maxAckWaitTime.
215
+ */
216
+ maxAckWaitTime: number;
217
+ /**
218
+ * Defines the maximum number of Ops in between Summaries that can be
219
+ * allowed before forcibly electing a new summarizer client.
220
+ */
221
+ maxOpsSinceLastSummary: number;
222
+ }
196
223
 
197
- const DefaultSummaryConfiguration: ISummaryConfiguration = {
198
- idleTime: IdleDetectionTime * 3,
224
+ export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfiguration {
225
+ state: "enabled";
226
+ /**
227
+ * Defines the maximum allowed time in between summarizations.
228
+ */
229
+ idleTime: number;
230
+ /**
231
+ * Defines the maximum allowed time, since the last received Ack, before running the summary
232
+ * with reason maxTime.
233
+ */
234
+ maxTime: number;
235
+ /**
236
+ * Defines the maximum number of Ops, since the last received Ack, that can be allowed
237
+ * before running the summary with reason maxOps.
238
+ */
239
+ maxOps: number;
240
+ /**
241
+ * Defines the minimum number of Ops, since the last received Ack, that can be allowed
242
+ * before running the last summary.
243
+ */
244
+ minOpsForLastSummaryAttempt: number;
245
+ }
246
+
247
+ export interface ISummaryConfigurationDisableSummarizer {
248
+ state: "disabled";
249
+ }
250
+
251
+ export interface ISummaryConfigurationDisableHeuristics extends ISummaryBaseConfiguration {
252
+ state: "disableHeuristics";
253
+ }
199
254
 
200
- maxTime: IdleDetectionTime * 12,
255
+ export type ISummaryConfiguration =
256
+ | ISummaryConfigurationDisableSummarizer
257
+ | ISummaryConfigurationDisableHeuristics
258
+ | ISummaryConfigurationHeuristics;
201
259
 
202
- // Summarize if 1000 ops received since last snapshot.
203
- maxOps: 1000,
260
+ export const DefaultSummaryConfiguration: ISummaryConfiguration = {
261
+ state: "enabled",
204
262
 
205
- // Wait 10 minutes for summary ack
206
- // this is less than maxSummarizeAckWaitTime
207
- // the min of the two will be chosen
208
- maxAckWaitTime: 600000,
263
+ idleTime: 5000 * 3,
264
+
265
+ maxTime: 5000 * 12,
266
+
267
+ maxOps: 100, // Summarize if 100 ops received since last snapshot.
268
+
269
+ minOpsForLastSummaryAttempt: 10,
270
+
271
+ maxAckWaitTime: 6 * 10 * 1000, // 6 min.
272
+
273
+ maxOpsSinceLastSummary: 7000,
274
+
275
+ initialSummarizerDelayMs: 5000, // 5 secs.
276
+
277
+ summarizerClientElection: false,
209
278
  };
210
279
 
211
280
  export interface IGCRuntimeOptions {
@@ -245,39 +314,43 @@ export interface IGCRuntimeOptions {
245
314
  }
246
315
 
247
316
  export interface ISummaryRuntimeOptions {
248
- /**
249
- * Flag that disables summaries if it is set to true.
250
- */
251
- disableSummaries?: boolean;
252
-
253
- /**
254
- * @deprecated - To disable summaries, please set disableSummaries===true.
255
- * Flag that will generate summaries if connected to a service that supports them.
256
- * This defaults to true and must be explicitly set to false to disable.
257
- */
258
- generateSummaries?: boolean;
259
-
260
- /* Delay before first attempt to spawn summarizing container. */
261
- initialSummarizerDelayMs?: number;
262
317
 
263
318
  /** Override summary configurations set by the server. */
264
- summaryConfigOverrides?: Partial<ISummaryConfiguration>;
319
+ summaryConfigOverrides?: ISummaryConfiguration;
265
320
 
266
321
  // Flag that disables putting channels in isolated subtrees for each data store
267
322
  // and the root node when generating a summary if set to true.
268
323
  // Defaults to FALSE (enabled) for now.
269
324
  disableIsolatedChannels?: boolean;
270
325
 
271
- // Defaults to 7000 ops
272
- maxOpsSinceLastSummary?: number;
326
+ /**
327
+ * @deprecated - use `summaryConfigOverrides.initialSummarizerDelayMs` instead.
328
+ * Delay before first attempt to spawn summarizing container.
329
+ */
330
+ initialSummarizerDelayMs?: number;
273
331
 
274
332
  /**
333
+ * @deprecated - use `summaryConfigOverrides.disableSummaries` instead.
334
+ * Flag that disables summaries if it is set to true.
335
+ */
336
+ disableSummaries?: boolean;
337
+
338
+ /**
339
+ * @deprecated - use `summaryConfigOverrides.maxOpsSinceLastSummary` instead.
340
+ * Defaults to 7000 ops
341
+ */
342
+ maxOpsSinceLastSummary?: number;
343
+
344
+ /**
345
+ * @deprecated - use `summaryConfigOverrides.summarizerClientElection` instead.
275
346
  * Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
276
- * THis defaults to false (disabled) and must be explicitly set to true to enable.
347
+ * This defaults to false (disabled) and must be explicitly set to true to enable.
277
348
  */
278
349
  summarizerClientElection?: boolean;
279
350
 
280
- /** Options that control the running summarizer behavior. */
351
+ /**
352
+ * @deprecated - use `summaryConfigOverrides.state = "DisableHeuristics"` instead.
353
+ * Options that control the running summarizer behavior. */
281
354
  summarizerOptions?: Readonly<Partial<ISummarizerOptions>>;
282
355
  }
283
356
 
@@ -309,6 +382,10 @@ export interface IContainerRuntimeOptions {
309
382
  * By default, flush mode is TurnBased.
310
383
  */
311
384
  readonly flushMode?: FlushMode;
385
+ /**
386
+ * Save enough runtime state to be able to serialize upon request and load to the same state in a new container.
387
+ */
388
+ readonly enableOfflineLoad?: boolean;
312
389
  }
313
390
 
314
391
  type IRuntimeMessageMetadata = undefined | {
@@ -349,6 +426,33 @@ interface OldContainerContextWithLogger extends Omit<IContainerContext, "taggedL
349
426
  taggedLogger: undefined;
350
427
  }
351
428
 
429
+ /**
430
+ * State saved when the container closes, to be given back to a newly
431
+ * instantiated runtime in a new instance of the container, so it can load to the
432
+ * same state
433
+ */
434
+ export interface IPendingRuntimeState {
435
+ /**
436
+ * Pending ops from PendingStateManager
437
+ */
438
+ pending?: IPendingLocalState;
439
+ /**
440
+ * A base snapshot at a sequence number prior to the first pending op
441
+ */
442
+ baseSnapshot: ISnapshotTree;
443
+ /**
444
+ * Serialized blobs from the base snapshot. Used to load offline since
445
+ * storage is not available.
446
+ */
447
+ snapshotBlobs: ISerializedBaseSnapshotBlobs;
448
+ /**
449
+ * All runtime ops since base snapshot sequence number up to the latest op
450
+ * seen when the container was closed. Used to apply stashed (saved pending)
451
+ * ops at the same sequence number at which they were made.
452
+ */
453
+ savedOps: ISequencedDocumentMessage[];
454
+ }
455
+
352
456
  const useDataStoreAliasingKey = "Fluid.ContainerRuntime.UseDataStoreAliasing";
353
457
  const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
354
458
 
@@ -507,7 +611,7 @@ class ScheduleManagerCore {
507
611
 
508
612
  private resumeQueue(startBatch: number, messageEndBatch: ISequencedDocumentMessage) {
509
613
  const endBatch = messageEndBatch.sequenceNumber;
510
- const duration = performance.now() - this.timePaused;
614
+ const duration = this.localPaused ? (performance.now() - this.timePaused) : undefined;
511
615
 
512
616
  this.batchCount++;
513
617
  if (this.batchCount % 1000 === 1) {
@@ -530,7 +634,7 @@ class ScheduleManagerCore {
530
634
  this.localPaused = false;
531
635
 
532
636
  // Random round number - we want to know when batch waiting paused op processing.
533
- if (duration > latencyThreshold) {
637
+ if (duration !== undefined && duration > latencyThreshold) {
534
638
  this.logger.sendErrorEvent({
535
639
  eventName: "MaxBatchWaitTimeExceeded",
536
640
  duration,
@@ -749,15 +853,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
749
853
  loadSequenceNumberVerification = "close",
750
854
  useDataStoreAliasing = false,
751
855
  flushMode = defaultFlushMode,
856
+ enableOfflineLoad = false,
752
857
  } = runtimeOptions;
753
858
 
754
- const storage = context.storage;
859
+ const pendingRuntimeState = context.pendingLocalState as IPendingRuntimeState | undefined;
860
+ const baseSnapshot: ISnapshotTree | undefined = pendingRuntimeState?.baseSnapshot ?? context.baseSnapshot;
861
+ const storage = !pendingRuntimeState ?
862
+ context.storage :
863
+ new SerializedSnapshotStorage(() => { return context.storage; }, pendingRuntimeState.snapshotBlobs);
755
864
 
756
865
  const registry = new FluidDataStoreRegistry(registryEntries);
757
866
 
758
867
  const tryFetchBlob = async <T>(blobName: string): Promise<T | undefined> => {
759
- const blobId = context.baseSnapshot?.blobs[blobName];
760
- if (context.baseSnapshot && blobId) {
868
+ const blobId = baseSnapshot?.blobs[blobName];
869
+ if (baseSnapshot && blobId) {
761
870
  // IContainerContext storage api return type still has undefined in 0.39 package version.
762
871
  // So once we release 0.40 container-defn package we can remove this check.
763
872
  assert(storage !== undefined, 0x1f5 /* "Attached state should have storage" */);
@@ -776,7 +885,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
776
885
 
777
886
  // read snapshot blobs needed for BlobManager to load
778
887
  const blobManagerSnapshot = await BlobManager.load(
779
- context.baseSnapshot?.trees[blobsTreeName],
888
+ baseSnapshot?.trees[blobsTreeName],
780
889
  async (id) => {
781
890
  // IContainerContext storage api return type still has undefined in 0.39 package version.
782
891
  // So once we release 0.40 container-defn package we can remove this check.
@@ -787,7 +896,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
787
896
 
788
897
  // Verify summary runtime sequence number matches protocol sequence number.
789
898
  const runtimeSequenceNumber = metadata?.message?.sequenceNumber;
790
- if (runtimeSequenceNumber !== undefined) {
899
+ // When we load with pending state, we reuse an old snapshot so we don't expect these numbers to match
900
+ if (!pendingRuntimeState && runtimeSequenceNumber !== undefined) {
791
901
  const protocolSequenceNumber = context.deltaManager.initialSequenceNumber;
792
902
  // Unless bypass is explicitly set, then take action when sequence numbers mismatch.
793
903
  if (loadSequenceNumberVerification !== "bypass" && runtimeSequenceNumber !== protocolSequenceNumber) {
@@ -819,14 +929,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
819
929
  loadSequenceNumberVerification,
820
930
  useDataStoreAliasing,
821
931
  flushMode,
932
+ enableOfflineLoad,
822
933
  },
823
934
  containerScope,
824
935
  logger,
825
936
  loadExisting,
826
937
  blobManagerSnapshot,
938
+ storage,
827
939
  requestHandler,
828
940
  );
829
941
 
942
+ if (pendingRuntimeState) {
943
+ await runtime.processSavedOps(pendingRuntimeState);
944
+ // delete these once runtime has seen them to save space
945
+ pendingRuntimeState.savedOps = [];
946
+ }
947
+
948
+ await runtime.getSnapshotBlobs();
949
+
830
950
  return runtime;
831
951
  }
832
952
 
@@ -847,7 +967,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
847
967
  }
848
968
 
849
969
  public get storage(): IDocumentStorageService {
850
- return this.context.storage;
970
+ return this._storage;
851
971
  }
852
972
 
853
973
  public get reSubmitFn(): (
@@ -910,7 +1030,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
910
1030
 
911
1031
  private _connected: boolean;
912
1032
 
913
- private paused: boolean = false;
1033
+ private readonly savedOps: ISequencedDocumentMessage[] = [];
1034
+ private baseSnapshotBlobs?: ISerializedBaseSnapshotBlobs;
914
1035
 
915
1036
  private consecutiveReconnects = 0;
916
1037
 
@@ -923,21 +1044,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
923
1044
  return this.summarizerClientElection?.electedClientId;
924
1045
  }
925
1046
 
926
- private get summaryConfiguration() {
927
- return {
928
- // the defaults
929
- ... DefaultSummaryConfiguration,
930
- // the runtime configuration overrides
931
- ... this.runtimeOptions.summaryOptions?.summaryConfigOverrides,
932
- };
933
- }
934
-
935
1047
  private _disposed = false;
936
1048
  public get disposed() { return this._disposed; }
937
1049
 
938
1050
  private dirtyContainer: boolean;
939
1051
  private emitDirtyDocumentEvent = true;
940
1052
 
1053
+ private readonly defaultTelemetrySignalSampleCount = 100;
1054
+ private _perfSignalData: IPerfSignalReport = {
1055
+ signalsLost: 0,
1056
+ signalSequenceNumber: 0,
1057
+ signalTimestamp: 0,
1058
+ trackingSignalSequenceNumber: undefined,
1059
+ };
1060
+
941
1061
  /**
942
1062
  * Summarizer is responsible for coordinating when to send generate and send summaries.
943
1063
  * It is the main entry point for summary work.
@@ -969,9 +1089,68 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
969
1089
  return this._summarizer;
970
1090
  }
971
1091
 
972
- private get summariesDisabled(): boolean {
973
- return this.runtimeOptions.summaryOptions.disableSummaries === true ||
974
- this.runtimeOptions.summaryOptions.summaryConfigOverrides?.disableSummaries === true;
1092
+ private readonly summariesDisabled: boolean;
1093
+ private isSummariesDisabled(): boolean {
1094
+ // back-compat: disableSummaries was moved from ISummaryRuntimeOptions
1095
+ // to ISummaryConfiguration in 0.60.
1096
+ if (this.runtimeOptions.summaryOptions.disableSummaries === true) {
1097
+ return true;
1098
+ }
1099
+ return this.summaryConfiguration.state === "disabled";
1100
+ }
1101
+
1102
+ private readonly heuristicsDisabled: boolean;
1103
+ private isHeuristicsDisabled(): boolean {
1104
+ // back-compat: disableHeuristics was moved from ISummarizerOptions
1105
+ // to ISummaryConfiguration in 0.60.
1106
+ if (this.runtimeOptions.summaryOptions.summarizerOptions?.disableHeuristics === true) {
1107
+ return true;
1108
+ }
1109
+ return this.summaryConfiguration.state === "disableHeuristics";
1110
+ }
1111
+
1112
+ private readonly summarizerClientElectionEnabled: boolean;
1113
+ private isSummarizerClientElectionEnabled(): boolean {
1114
+ if (this.mc.config.getBoolean("Fluid.ContainerRuntime.summarizerClientElection")) {
1115
+ return this.mc.config.getBoolean("Fluid.ContainerRuntime.summarizerClientElection") ?? true;
1116
+ }
1117
+ // back-compat: summarizerClientElection was moved from ISummaryRuntimeOptions
1118
+ // to ISummaryConfiguration in 0.60.
1119
+ if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
1120
+ return true;
1121
+ }
1122
+ if (this.summaryConfiguration.state !== "disabled") {
1123
+ return this.summaryConfiguration.summarizerClientElection === true;
1124
+ } else {
1125
+ return false;
1126
+ }
1127
+ }
1128
+ private readonly maxOpsSinceLastSummary: number;
1129
+ private getMaxOpsSinceLastSummary(): number {
1130
+ // back-compat: maxOpsSinceLastSummary was moved from ISummaryRuntimeOptions
1131
+ // to ISummaryConfiguration in 0.60.
1132
+ if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
1133
+ return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
1134
+ }
1135
+ if (this.summaryConfiguration.state !== "disabled") {
1136
+ return this.summaryConfiguration.maxOpsSinceLastSummary;
1137
+ } else {
1138
+ return 0;
1139
+ }
1140
+ }
1141
+
1142
+ private readonly initialSummarizerDelayMs: number;
1143
+ private getInitialSummarizerDelayMs(): number {
1144
+ // back-compat: initialSummarizerDelayMs was moved from ISummaryRuntimeOptions
1145
+ // to ISummaryConfiguration in 0.60.
1146
+ if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
1147
+ return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
1148
+ }
1149
+ if (this.summaryConfiguration.state !== "disabled") {
1150
+ return this.summaryConfiguration.initialSummarizerDelayMs;
1151
+ } else {
1152
+ return 0;
1153
+ }
975
1154
  }
976
1155
 
977
1156
  private readonly createContainerMetadata: ICreateContainerMetadata;
@@ -994,10 +1173,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
994
1173
  public readonly logger: ITelemetryLogger,
995
1174
  existing: boolean,
996
1175
  blobManagerSnapshot: IBlobManagerLoadInfo,
1176
+ private readonly _storage: IDocumentStorageService,
997
1177
  private readonly requestHandler?: (request: IRequest, runtime: IContainerRuntime) => Promise<IResponse>,
1178
+ private readonly summaryConfiguration: ISummaryConfiguration = {
1179
+ // the defaults
1180
+ ... DefaultSummaryConfiguration,
1181
+ // the runtime configuration overrides
1182
+ ... runtimeOptions.summaryOptions?.summaryConfigOverrides,
1183
+ },
998
1184
  ) {
999
1185
  super();
1000
-
1001
1186
  this.messageAtLastSummary = metadata?.message;
1002
1187
 
1003
1188
  // Default to false (enabled).
@@ -1011,6 +1196,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1011
1196
  this.mc = loggerToMonitoringContext(
1012
1197
  ChildLogger.create(this.logger, "ContainerRuntime"));
1013
1198
 
1199
+ this.summariesDisabled = this.isSummariesDisabled();
1200
+ this.heuristicsDisabled = this.isHeuristicsDisabled();
1201
+ this.summarizerClientElectionEnabled = this.isSummarizerClientElectionEnabled();
1202
+ this.maxOpsSinceLastSummary = this.getMaxOpsSinceLastSummary();
1203
+ this.initialSummarizerDelayMs = this.getInitialSummarizerDelayMs();
1204
+
1014
1205
  this._aliasingEnabled =
1015
1206
  (this.mc.config.getBoolean(useDataStoreAliasingKey) ?? false) ||
1016
1207
  (runtimeOptions.useDataStoreAliasing ?? false);
@@ -1020,28 +1211,33 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1020
1211
  this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? this.defaultMaxConsecutiveReconnects;
1021
1212
 
1022
1213
  this._flushMode = runtimeOptions.flushMode;
1023
- this.garbageCollector = GarbageCollector.create(
1024
- this,
1025
- this.runtimeOptions.gcOptions,
1026
- (nodePath: string) => this.getGCNodePackagePath(nodePath),
1027
- () => this.messageAtLastSummary?.timestamp,
1028
- context.baseSnapshot,
1029
- async <T>(id: string) => readAndParse<T>(this.storage, id),
1030
- this.mc.logger,
1214
+
1215
+ const pendingRuntimeState = context.pendingLocalState as IPendingRuntimeState | undefined;
1216
+ const baseSnapshot: ISnapshotTree | undefined = pendingRuntimeState?.baseSnapshot ?? context.baseSnapshot;
1217
+
1218
+ this.garbageCollector = GarbageCollector.create({
1219
+ runtime: this,
1220
+ gcOptions: this.runtimeOptions.gcOptions,
1221
+ baseSnapshot,
1222
+ baseLogger: this.mc.logger,
1031
1223
  existing,
1032
1224
  metadata,
1033
- this.context.clientDetails.type === summarizerClientType,
1034
- );
1225
+ isSummarizerClient: this.context.clientDetails.type === summarizerClientType,
1226
+ getNodePackagePath: (nodePath: string) => this.getGCNodePackagePath(nodePath),
1227
+ getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
1228
+ readAndParseBlob: async <T>(id: string) => readAndParse<T>(this.storage, id),
1229
+ });
1035
1230
 
1036
1231
  const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
1037
1232
  this.summarizerNode = createRootSummarizerNodeWithGC(
1038
1233
  ChildLogger.create(this.logger, "SummarizerNode"),
1039
1234
  // Summarize function to call when summarize is called. Summarizer node always tracks summary state.
1040
- async (fullTree: boolean, trackState: boolean) => this.summarizeInternal(fullTree, trackState),
1235
+ async (fullTree: boolean, trackState: boolean, telemetryContext?: ITelemetryContext) =>
1236
+ this.summarizeInternal(fullTree, trackState, telemetryContext),
1041
1237
  // Latest change sequence number, no changes since summary applied yet
1042
1238
  loadedFromSequenceNumber,
1043
1239
  // Summary reference sequence number, undefined if no summary yet
1044
- context.baseSnapshot ? loadedFromSequenceNumber : undefined,
1240
+ baseSnapshot ? loadedFromSequenceNumber : undefined,
1045
1241
  {
1046
1242
  // Must set to false to prevent sending summary handle which would be pointing to
1047
1243
  // a summary with an older protocol state.
@@ -1054,12 +1250,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1054
1250
  },
1055
1251
  );
1056
1252
 
1057
- if (this.context.baseSnapshot) {
1058
- this.summarizerNode.loadBaseSummaryWithoutDifferential(this.context.baseSnapshot);
1253
+ if (baseSnapshot) {
1254
+ this.summarizerNode.loadBaseSummaryWithoutDifferential(baseSnapshot);
1059
1255
  }
1060
1256
 
1061
1257
  this.dataStores = new DataStores(
1062
- getSummaryForDatastores(context.baseSnapshot, metadata),
1258
+ getSummaryForDatastores(baseSnapshot, metadata),
1063
1259
  this,
1064
1260
  (attachMsg) => this.submit(ContainerMessageType.Attach, attachMsg),
1065
1261
  (id: string, createParam: CreateChildSummarizerNodeParam) => (
@@ -1106,10 +1302,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1106
1302
  this.deltaSender = this.deltaManager;
1107
1303
 
1108
1304
  this.pendingStateManager = new PendingStateManager(
1109
- this,
1110
- async (type, content) => this.applyStashedOp(type, content),
1305
+ {
1306
+ applyStashedOp: this.applyStashedOp.bind(this),
1307
+ clientId: () => this.clientId,
1308
+ close: this.closeFn,
1309
+ connected: () => this.connected,
1310
+ flush: this.flush.bind(this),
1311
+ flushMode: () => this.flushMode,
1312
+ reSubmit: this.reSubmit.bind(this),
1313
+ rollback: this.rollback.bind(this),
1314
+ setFlushMode: (mode) => this.setFlushMode(mode),
1315
+ },
1111
1316
  this._flushMode,
1112
- context.pendingLocalState as IPendingLocalState);
1317
+ pendingRuntimeState?.pending);
1113
1318
 
1114
1319
  this.context.quorum.on("removeMember", (clientId: string) => {
1115
1320
  this.clearPartialChunks(clientId);
@@ -1117,16 +1322,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1117
1322
 
1118
1323
  this.summaryCollection = new SummaryCollection(this.deltaManager, this.logger);
1119
1324
 
1120
- const { attachState, pendingLocalState } = this.context;
1121
- this.dirtyContainer = attachState !== AttachState.Attached
1122
- || (pendingLocalState as IPendingLocalState)?.pendingStates.length > 0;
1325
+ this.dirtyContainer = this.context.attachState !== AttachState.Attached
1326
+ || this.pendingStateManager.hasPendingMessages();
1123
1327
  this.context.updateDirtyContainerState(this.dirtyContainer);
1124
1328
 
1125
- // Map the deprecated generateSummaries flag to disableSummaries.
1126
- if (this.runtimeOptions.summaryOptions.generateSummaries === false) {
1127
- this.runtimeOptions.summaryOptions.disableSummaries = true;
1128
- }
1129
-
1130
1329
  if (this.summariesDisabled) {
1131
1330
  this.mc.logger.sendTelemetryEvent({ eventName: "SummariesDisabled" });
1132
1331
  } else {
@@ -1143,16 +1342,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1143
1342
  electedSummarizerData ?? this.context.deltaManager.lastSequenceNumber,
1144
1343
  SummarizerClientElection.isClientEligible,
1145
1344
  );
1146
- const summarizerClientElectionEnabled =
1147
- this.mc.config.getBoolean("Fluid.ContainerRuntime.summarizerClientElection") ??
1148
- this.runtimeOptions.summaryOptions?.summarizerClientElection === true;
1149
- const maxOpsSinceLastSummary = this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary ?? 7000;
1345
+
1150
1346
  this.summarizerClientElection = new SummarizerClientElection(
1151
1347
  orderedClientLogger,
1152
1348
  this.summaryCollection,
1153
1349
  orderedClientElectionForSummarizer,
1154
- maxOpsSinceLastSummary,
1155
- summarizerClientElectionEnabled,
1350
+ this.maxOpsSinceLastSummary,
1351
+ this.summarizerClientElectionEnabled,
1156
1352
  );
1157
1353
 
1158
1354
  if (this.context.clientDetails.type === summarizerClientType) {
@@ -1169,7 +1365,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1169
1365
  // Only create a SummaryManager and SummarizerClientElection
1170
1366
  // if summaries are enabled and we are not the summarizer client.
1171
1367
  const defaultAction = () => {
1172
- if (this.summaryCollection.opsSinceLastAck > maxOpsSinceLastSummary) {
1368
+ if (this.summaryCollection.opsSinceLastAck > this.maxOpsSinceLastSummary) {
1173
1369
  this.logger.sendErrorEvent({ eventName: "SummaryStatus:Behind" });
1174
1370
  // unregister default to no log on every op after falling behind
1175
1371
  // and register summary ack handler to re-register this handler
@@ -1200,9 +1396,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1200
1396
  formExponentialFn({ coefficient: 20, initialDelay: 0 }),
1201
1397
  ),
1202
1398
  {
1203
- initialDelayMs: this.runtimeOptions.summaryOptions.initialSummarizerDelayMs,
1399
+ initialDelayMs: this.initialSummarizerDelayMs,
1204
1400
  },
1205
- this.runtimeOptions.summaryOptions.summarizerOptions,
1401
+ this.heuristicsDisabled,
1206
1402
  );
1207
1403
  this.summaryManager.start();
1208
1404
  }
@@ -1231,10 +1427,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1231
1427
  this.replayPendingStates();
1232
1428
  });
1233
1429
 
1234
- if (context.pendingLocalState !== undefined) {
1235
- this.deltaManager.on("op", this.onOp);
1236
- }
1237
-
1238
1430
  // logging hardware telemetry
1239
1431
  logger.sendTelemetryEvent({
1240
1432
  eventName: "DeviceSpec",
@@ -1379,11 +1571,17 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1379
1571
  }
1380
1572
  }
1381
1573
 
1574
+ private internalId(maybeAlias: string): string {
1575
+ return this.dataStores.aliases().get(maybeAlias) ?? maybeAlias;
1576
+ }
1577
+
1382
1578
  private async getDataStoreFromRequest(id: string, request: IRequest): Promise<IFluidRouter> {
1383
1579
  const wait = typeof request.headers?.[RuntimeHeaders.wait] === "boolean"
1384
1580
  ? request.headers?.[RuntimeHeaders.wait]
1385
1581
  : true;
1386
- const dataStoreContext = await this.dataStores.getDataStore(id, wait);
1582
+
1583
+ const internalId = this.internalId(id);
1584
+ const dataStoreContext = await this.dataStores.getDataStore(internalId, wait);
1387
1585
 
1388
1586
  /**
1389
1587
  * If GC should run and this an external app request with "externalRequest" header, we need to return
@@ -1439,6 +1637,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1439
1637
  summaryTree: ISummaryTreeWithStats,
1440
1638
  fullTree: boolean,
1441
1639
  trackState: boolean,
1640
+ telemetryContext?: ITelemetryContext,
1442
1641
  ) {
1443
1642
  this.addMetadataToSummary(summaryTree);
1444
1643
 
@@ -1465,7 +1664,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1465
1664
  }
1466
1665
 
1467
1666
  if (this.garbageCollector.writeDataAtRoot) {
1468
- const gcSummary = this.garbageCollector.summarize(fullTree, trackState);
1667
+ const gcSummary = this.garbageCollector.summarize(fullTree, trackState, telemetryContext);
1469
1668
  if (gcSummary !== undefined) {
1470
1669
  addSummarizeResultToSummary(summaryTree, gcTreeKey, gcSummary);
1471
1670
  }
@@ -1489,7 +1688,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1489
1688
  return true;
1490
1689
  }
1491
1690
 
1492
- this.consecutiveReconnects++;
1493
1691
  if (this.consecutiveReconnects === Math.floor(this.maxConsecutiveReconnects / 2)) {
1494
1692
  // If we're halfway through the max reconnects, send an event in order
1495
1693
  // to better identify false positives, if any. If the rate of this event
@@ -1498,6 +1696,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1498
1696
  this.mc.logger.sendTelemetryEvent({
1499
1697
  eventName: "ReconnectsWithNoProgress",
1500
1698
  attempts: this.consecutiveReconnects,
1699
+ pendingMessages: this.pendingStateManager.pendingMessagesCount,
1501
1700
  });
1502
1701
  }
1503
1702
 
@@ -1538,28 +1737,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1538
1737
  this.updateDocumentDirtyState(newState);
1539
1738
  }
1540
1739
 
1541
- /**
1542
- * Used to apply stashed ops at their reference sequence number.
1543
- * Normal op processing is synchronous, but applying stashed ops is async since the
1544
- * data store may not be loaded yet, so we pause DeltaManager between ops.
1545
- * It's also important that we see each op so we know all stashed ops have
1546
- * been applied by "connected" event, but process() doesn't see system ops,
1547
- * so we listen directly from DeltaManager instead.
1548
- */
1549
- private readonly onOp = (op: ISequencedDocumentMessage) => {
1550
- assert(!this.paused, 0x128 /* "Container should not already be paused before applying stashed ops" */);
1551
- this.paused = true;
1552
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1553
- this.context.deltaManager.inbound.pause();
1554
- const stashP = this.pendingStateManager.applyStashedOpsAt(op.sequenceNumber);
1555
- stashP.then(() => {
1556
- this.paused = false;
1557
- this.context.deltaManager.inbound.resume();
1558
- }, (error) => {
1559
- this.closeFn(normalizeError(error));
1560
- });
1561
- };
1562
-
1563
1740
  private async applyStashedOp(type: ContainerMessageType, op: ISequencedDocumentMessage): Promise<unknown> {
1564
1741
  switch (type) {
1565
1742
  case ContainerMessageType.FluidDataStoreOp:
@@ -1583,20 +1760,35 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1583
1760
 
1584
1761
  // There might be no change of state due to Container calling this API after loading runtime.
1585
1762
  const changeOfState = this._connected !== connected;
1763
+ const reconnection = changeOfState && connected;
1586
1764
  this._connected = connected;
1587
1765
 
1588
- if (changeOfState) {
1589
- this.deltaManager.off("op", this.onOp);
1590
- this.context.pendingLocalState = undefined;
1766
+ if (!connected) {
1767
+ this._perfSignalData.signalsLost = 0;
1768
+ this._perfSignalData.signalTimestamp = 0;
1769
+ this._perfSignalData.trackingSignalSequenceNumber = undefined;
1770
+ }
1771
+
1772
+ if (reconnection) {
1773
+ this.consecutiveReconnects++;
1774
+
1591
1775
  if (!this.shouldContinueReconnecting()) {
1592
- this.closeFn(new GenericError(
1776
+ this.closeFn(
1593
1777
  // pre-0.58 error message: MaxReconnectsWithNoProgress
1594
- "Runtime detected too many reconnects with no progress syncing local ops",
1595
- undefined, // error
1596
- { attempts: this.consecutiveReconnects }));
1778
+ DataProcessingError.create(
1779
+ "Runtime detected too many reconnects with no progress syncing local ops",
1780
+ "setConnectionState",
1781
+ undefined,
1782
+ {
1783
+ dataLoss: 1,
1784
+ attempts: this.consecutiveReconnects,
1785
+ pendingMessages: this.pendingStateManager.pendingMessagesCount,
1786
+ }));
1597
1787
  return;
1598
1788
  }
1789
+ }
1599
1790
 
1791
+ if (changeOfState) {
1600
1792
  this.replayPendingStates();
1601
1793
  }
1602
1794
 
@@ -1613,6 +1805,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1613
1805
  return;
1614
1806
  }
1615
1807
 
1808
+ if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
1809
+ this.savedOps.push(messageArg);
1810
+ }
1811
+
1616
1812
  // Do shallow copy of message, as methods below will modify it.
1617
1813
  // There might be multiple container instances receiving same message
1618
1814
  // We do not need to make deep copy, as each layer will just replace message.content itself,
@@ -1631,8 +1827,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1631
1827
  // once all pieces are available
1632
1828
  message = this.processRemoteChunkedMessage(message);
1633
1829
 
1634
- // Call the PendingStateManager to process messages.
1635
- const { localAck, localOpMetadata } = this.pendingStateManager.processMessage(message, local);
1830
+ let localOpMetadata: unknown;
1831
+ if (local) {
1832
+ // Call the PendingStateManager to process local messages.
1833
+ // Do not process local chunked ops until all pieces are available.
1834
+ if (message.type !== ContainerMessageType.ChunkedOp) {
1835
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1836
+ }
1837
+ }
1636
1838
 
1637
1839
  // If there are no more pending messages after processing a local message,
1638
1840
  // the document is no longer dirty.
@@ -1642,14 +1844,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1642
1844
 
1643
1845
  switch (message.type) {
1644
1846
  case ContainerMessageType.Attach:
1645
- this.dataStores.processAttachMessage(message, local || localAck);
1847
+ this.dataStores.processAttachMessage(message, local);
1646
1848
  break;
1647
1849
  case ContainerMessageType.Alias:
1648
1850
  this.processAliasMessage(message, localOpMetadata, local);
1649
1851
  break;
1650
1852
  case ContainerMessageType.FluidDataStoreOp:
1651
- // if localAck === true, treat this as a local op because it's one we sent on a previous container
1652
- this.dataStores.processFluidDataStoreOp(message, local || localAck, localOpMetadata);
1853
+ this.dataStores.processFluidDataStoreOp(message, local, localOpMetadata);
1653
1854
  break;
1654
1855
  case ContainerMessageType.BlobAttach:
1655
1856
  assert(message?.metadata?.blobId, 0x12a /* "Missing blob id on metadata" */);
@@ -1681,6 +1882,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1681
1882
  this.dataStores.processAliasMessage(message, localOpMetadata, local);
1682
1883
  }
1683
1884
 
1885
+ /**
1886
+ * Emits the Signal event and update the perf signal data.
1887
+ * @param clientSignalSequenceNumber - is the client signal sequence number to be uploaded.
1888
+ */
1889
+ private sendSignalTelemetryEvent(clientSignalSequenceNumber: number) {
1890
+ const duration = Date.now() - this._perfSignalData.signalTimestamp;
1891
+ this.logger.sendPerformanceEvent({
1892
+ eventName: "SignalLatency",
1893
+ duration,
1894
+ signalsLost: this._perfSignalData.signalsLost,
1895
+ });
1896
+
1897
+ this._perfSignalData.signalsLost = 0;
1898
+ this._perfSignalData.signalTimestamp = 0;
1899
+ }
1900
+
1684
1901
  public processSignal(message: ISignalMessage, local: boolean) {
1685
1902
  const envelope = message.content as ISignalEnvelope;
1686
1903
  const transformed: IInboundSignalMessage = {
@@ -1689,6 +1906,26 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1689
1906
  type: envelope.contents.type,
1690
1907
  };
1691
1908
 
1909
+ // Only collect signal telemetry for messages sent by the current client.
1910
+ if (message.clientId === this.clientId && this.connected) {
1911
+ // Check to see if the signal was lost.
1912
+ if (this._perfSignalData.trackingSignalSequenceNumber !== undefined &&
1913
+ envelope.clientSignalSequenceNumber > this._perfSignalData.trackingSignalSequenceNumber) {
1914
+ this._perfSignalData.signalsLost++;
1915
+ this._perfSignalData.trackingSignalSequenceNumber = undefined;
1916
+ this.logger.sendErrorEvent({
1917
+ eventName: "SignalLost",
1918
+ type: envelope.contents.type,
1919
+ signalsLost: this._perfSignalData.signalsLost,
1920
+ trackingSequenceNumber: this._perfSignalData.trackingSignalSequenceNumber,
1921
+ clientSignalSequenceNumber: envelope.clientSignalSequenceNumber,
1922
+ });
1923
+ } else if (envelope.clientSignalSequenceNumber === this._perfSignalData.trackingSignalSequenceNumber) {
1924
+ this.sendSignalTelemetryEvent(envelope.clientSignalSequenceNumber);
1925
+ this._perfSignalData.trackingSignalSequenceNumber = undefined;
1926
+ }
1927
+ }
1928
+
1692
1929
  if (envelope.address === undefined) {
1693
1930
  // No address indicates a container signal message.
1694
1931
  this.emit("signal", transformed, local);
@@ -1699,7 +1936,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1699
1936
  }
1700
1937
 
1701
1938
  public async getRootDataStore(id: string, wait = true): Promise<IFluidRouter> {
1702
- const context = await this.dataStores.getDataStore(id, wait);
1939
+ const internalId = this.internalId(id);
1940
+ const context = await this.dataStores.getDataStore(internalId, wait);
1703
1941
  assert(await context.isRoot(), 0x12b /* "did not get root data store" */);
1704
1942
  return context.realize();
1705
1943
  }
@@ -1770,18 +2008,31 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1770
2008
  const savedFlushMode = this.flushMode;
1771
2009
  this.setFlushMode(FlushMode.TurnBased);
1772
2010
 
1773
- this.trackOrderSequentiallyCalls(callback);
1774
- this.flush();
1775
- this.setFlushMode(savedFlushMode);
2011
+ try {
2012
+ this.trackOrderSequentiallyCalls(callback);
2013
+ this.flush();
2014
+ } finally {
2015
+ this.setFlushMode(savedFlushMode);
2016
+ }
1776
2017
  }
1777
2018
 
1778
2019
  private trackOrderSequentiallyCalls(callback: () => void): void {
2020
+ let checkpoint: { rollback: () => void; } | undefined;
2021
+ if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
2022
+ checkpoint = this.pendingStateManager.checkpoint();
2023
+ }
2024
+
1779
2025
  try {
1780
2026
  this._orderSequentiallyCalls++;
1781
2027
  callback();
1782
2028
  } catch (error) {
1783
- // pre-0.58 error message: orderSequentiallyCallbackException
1784
- this.closeFn(new GenericError("orderSequentially callback exception", error));
2029
+ if (checkpoint) {
2030
+ // This will throw and close the container if rollback fails
2031
+ checkpoint.rollback();
2032
+ } else {
2033
+ // pre-0.58 error message: orderSequentiallyCallbackException
2034
+ this.closeFn(new GenericError("orderSequentially callback exception", error));
2035
+ }
1785
2036
  throw error; // throw the original error for the consumer of the runtime
1786
2037
  } finally {
1787
2038
  this._orderSequentiallyCalls--;
@@ -1816,7 +2067,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1816
2067
  return fluidDataStore;
1817
2068
  }
1818
2069
 
2070
+ /**
2071
+ * @deprecated - will be removed in an upcoming release. See #9660.
2072
+ */
1819
2073
  public async createRootDataStore(pkg: string | string[], rootDataStoreId: string): Promise<IFluidRouter> {
2074
+ if (rootDataStoreId.includes("/")) {
2075
+ throw new UsageError(`Id cannot contain slashes: '${rootDataStoreId}'`);
2076
+ }
1820
2077
  return this._aliasingEnabled === true ?
1821
2078
  this.createAndAliasDataStore(pkg, rootDataStoreId) :
1822
2079
  this.createRootDataStoreLegacy(pkg, rootDataStoreId);
@@ -1861,6 +2118,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1861
2118
  public createDetachedRootDataStore(
1862
2119
  pkg: Readonly<string[]>,
1863
2120
  rootDataStoreId: string): IFluidDataStoreContextDetached {
2121
+ if (rootDataStoreId.includes("/")) {
2122
+ throw new UsageError(`Id cannot contain slashes: '${rootDataStoreId}'`);
2123
+ }
1864
2124
  return this.dataStores.createDetachedDataStoreCore(pkg, true, rootDataStoreId);
1865
2125
  }
1866
2126
 
@@ -1958,6 +2218,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1958
2218
  return true;
1959
2219
  }
1960
2220
 
2221
+ private createNewSignalEnvelope(address: string | undefined, type: string, content: any): ISignalEnvelope {
2222
+ const newSequenceNumber = ++this._perfSignalData.signalSequenceNumber;
2223
+ const newEnvelope: ISignalEnvelope = {
2224
+ address,
2225
+ clientSignalSequenceNumber: newSequenceNumber,
2226
+ contents: { type, content },
2227
+ };
2228
+
2229
+ // We should not track any signals in case we already have a tracking number.
2230
+ if (newSequenceNumber % this.defaultTelemetrySignalSampleCount === 1 &&
2231
+ this._perfSignalData.trackingSignalSequenceNumber === undefined) {
2232
+ this._perfSignalData.signalTimestamp = Date.now();
2233
+ this._perfSignalData.trackingSignalSequenceNumber = newSequenceNumber;
2234
+ }
2235
+
2236
+ return newEnvelope;
2237
+ }
2238
+
1961
2239
  /**
1962
2240
  * Submits the signal to be sent to other clients.
1963
2241
  * @param type - Type of the signal.
@@ -1965,13 +2243,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1965
2243
  */
1966
2244
  public submitSignal(type: string, content: any) {
1967
2245
  this.verifyNotClosed();
1968
- const envelope: ISignalEnvelope = { address: undefined, contents: { type, content } };
2246
+ const envelope = this.createNewSignalEnvelope(undefined /* address */, type, content);
1969
2247
  return this.context.submitSignalFn(envelope);
1970
2248
  }
1971
2249
 
1972
2250
  public submitDataStoreSignal(address: string, type: string, content: any) {
1973
- const envelope: ISignalEnvelope = { address, contents: { type, content } };
1974
- return this.context.submitSignalFn(envelope);
2251
+ const envelope = this.createNewSignalEnvelope(address, type, content);
2252
+ return this.context.submitSignalFn(envelope);
1975
2253
  }
1976
2254
 
1977
2255
  public setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void {
@@ -1996,13 +2274,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1996
2274
  * @param blobRedirectTable - A table passed during the attach process. While detached, blob upload is supported
1997
2275
  * using IDs generated locally. After attach, these IDs cannot be used, so this table maps the old local IDs to the
1998
2276
  * new storage IDs so requests can be redirected.
2277
+ * @param telemetryContext - summary data passed through the layers for telemetry purposes
1999
2278
  */
2000
- public createSummary(blobRedirectTable?: Map<string, string>): ISummaryTree {
2279
+ public createSummary(blobRedirectTable?: Map<string, string>, telemetryContext?: ITelemetryContext): ISummaryTree {
2001
2280
  if (blobRedirectTable) {
2002
2281
  this.blobManager.setRedirectTable(blobRedirectTable);
2003
2282
  }
2004
2283
 
2005
- const summarizeResult = this.dataStores.createSummary();
2284
+ const summarizeResult = this.dataStores.createSummary(telemetryContext);
2006
2285
  if (!this.disableIsolatedChannels) {
2007
2286
  // Wrap data store summaries in .channels subtree.
2008
2287
  wrapSummaryInChannelsTree(summarizeResult);
@@ -2010,7 +2289,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2010
2289
  this.addContainerStateToSummary(
2011
2290
  summarizeResult,
2012
2291
  true /* fullTree */,
2013
- false /* trackState */);
2292
+ false /* trackState */,
2293
+ telemetryContext,
2294
+ );
2014
2295
  return summarizeResult.summary;
2015
2296
  }
2016
2297
 
@@ -2024,8 +2305,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2024
2305
  return this.context.getAbsoluteUrl(relativeUrl);
2025
2306
  }
2026
2307
 
2027
- private async summarizeInternal(fullTree: boolean, trackState: boolean): Promise<ISummarizeInternalResult> {
2028
- const summarizeResult = await this.dataStores.summarize(fullTree, trackState);
2308
+ private async summarizeInternal(
2309
+ fullTree: boolean,
2310
+ trackState: boolean,
2311
+ telemetryContext?: ITelemetryContext,
2312
+ ): Promise<ISummarizeInternalResult> {
2313
+ const summarizeResult = await this.dataStores.summarize(fullTree, trackState, telemetryContext);
2029
2314
  let pathPartsForChildren: string[] | undefined;
2030
2315
 
2031
2316
  if (!this.disableIsolatedChannels) {
@@ -2033,7 +2318,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2033
2318
  wrapSummaryInChannelsTree(summarizeResult);
2034
2319
  pathPartsForChildren = [channelsTreeName];
2035
2320
  }
2036
- this.addContainerStateToSummary(summarizeResult, fullTree, trackState);
2321
+ this.addContainerStateToSummary(summarizeResult, fullTree, trackState, telemetryContext);
2037
2322
  return {
2038
2323
  ...summarizeResult,
2039
2324
  id: "",
@@ -2074,7 +2359,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2074
2359
  gcStats = await this.collectGarbage({ logger: summaryLogger, runSweep, fullGC });
2075
2360
  }
2076
2361
 
2077
- const { stats, summary } = await this.summarizerNode.summarize(fullTree, trackState);
2362
+ const telemetryContext = new TelemetryContext();
2363
+ const { stats, summary } = await this.summarizerNode.summarize(fullTree, trackState, telemetryContext);
2364
+
2365
+ this.logger.sendTelemetryEvent({ eventName: "SummarizeTelemetry", details: telemetryContext.serialize() });
2078
2366
 
2079
2367
  assert(summary.type === SummaryType.Tree,
2080
2368
  0x12f /* "Container Runtime's summarize should always return a tree" */);
@@ -2531,9 +2819,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2531
2819
  ): void {
2532
2820
  this.verifyNotClosed();
2533
2821
 
2534
- if (this.context.pendingLocalState !== undefined) {
2535
- this.closeFn(new GenericError("containerRuntimeSubmitWithPendingLocalState"));
2536
- }
2537
2822
  // There should be no ops in detached container state!
2538
2823
  assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
2539
2824
 
@@ -2729,6 +3014,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2729
3014
  }
2730
3015
  }
2731
3016
 
3017
+ private rollback(
3018
+ type: ContainerMessageType,
3019
+ content: any,
3020
+ localOpMetadata: unknown,
3021
+ ) {
3022
+ switch (type) {
3023
+ case ContainerMessageType.FluidDataStoreOp:
3024
+ // For operations, call rollbackDataStoreOp which will find the right store
3025
+ // and trigger rollback on it.
3026
+ this.dataStores.rollbackDataStoreOp(content, localOpMetadata);
3027
+ break;
3028
+ default:
3029
+ throw new Error(`Can't rollback ${type}`);
3030
+ }
3031
+ }
3032
+
2732
3033
  /** Implementation of ISummarizerInternalsProvider.refreshLatestSummaryAck */
2733
3034
  public async refreshLatestSummaryAck(
2734
3035
  proposalHandle: string | undefined,
@@ -2807,8 +3108,43 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2807
3108
  });
2808
3109
  }
2809
3110
 
2810
- public getPendingLocalState() {
2811
- return this.pendingStateManager.getLocalState();
3111
+ public notifyAttaching(snapshot: ISnapshotTreeWithBlobContents) {
3112
+ if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
3113
+ this.baseSnapshotBlobs = SerializedSnapshotStorage.serializeTreeWithBlobContents(snapshot);
3114
+ }
3115
+ }
3116
+
3117
+ public async getSnapshotBlobs(): Promise<void> {
3118
+ if (!(this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) ||
3119
+ this.attachState !== AttachState.Attached || this.context.pendingLocalState) {
3120
+ return;
3121
+ }
3122
+ assert(!!this.context.baseSnapshot, 0x2e5 /* "Must have a base snapshot" */);
3123
+ this.baseSnapshotBlobs = await SerializedSnapshotStorage.serializeTree(this.context.baseSnapshot, this.storage);
3124
+ }
3125
+
3126
+ public getPendingLocalState(): IPendingRuntimeState {
3127
+ if (!(this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad)) {
3128
+ throw new UsageError("can't get state when offline load disabled");
3129
+ }
3130
+
3131
+ const previousPendingState = this.context.pendingLocalState as IPendingRuntimeState | undefined;
3132
+ if (previousPendingState) {
3133
+ return {
3134
+ pending: this.pendingStateManager.getLocalState(),
3135
+ snapshotBlobs: previousPendingState.snapshotBlobs,
3136
+ baseSnapshot: previousPendingState.baseSnapshot,
3137
+ savedOps: this.savedOps,
3138
+ };
3139
+ }
3140
+ assert(!!this.context.baseSnapshot, 0x2e6 /* "Must have a base snapshot" */);
3141
+ assert(!!this.baseSnapshotBlobs, 0x2e7 /* "Must serialize base snapshot blobs before getting runtime state" */);
3142
+ return {
3143
+ pending: this.pendingStateManager.getLocalState(),
3144
+ snapshotBlobs: this.baseSnapshotBlobs,
3145
+ baseSnapshot: this.context.baseSnapshot,
3146
+ savedOps: this.savedOps,
3147
+ };
2812
3148
  }
2813
3149
 
2814
3150
  public readonly summarizeOnDemand: ISummarizer["summarizeOnDemand"] = (...args) => {
@@ -2837,7 +3173,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2837
3173
  // because it is a misuse of the API rather than an expected failure.
2838
3174
  throw new UsageError(
2839
3175
  `Can't summarize, disableSummaries: ${this.summariesDisabled}`,
2840
- );
3176
+ );
2841
3177
  }
2842
3178
  };
2843
3179
 
@@ -2870,6 +3206,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2870
3206
  return summarizer;
2871
3207
  };
2872
3208
  }
3209
+
3210
+ private async processSavedOps(state: IPendingRuntimeState) {
3211
+ for (const op of state.savedOps) {
3212
+ this.process(op, false);
3213
+ await this.pendingStateManager.applyStashedOpsAt(op.sequenceNumber);
3214
+ }
3215
+ // we may not have seen every sequence number (because of system ops) so apply everything once we
3216
+ // don't have any more saved ops
3217
+ await this.pendingStateManager.applyStashedOpsAt();
3218
+ }
2873
3219
  }
2874
3220
 
2875
3221
  /**