@fluidframework/container-runtime 0.59.4001 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) 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.js +3 -3
  7. package/dist/connectionTelemetry.js.map +1 -1
  8. package/dist/containerRuntime.d.ts +125 -29
  9. package/dist/containerRuntime.d.ts.map +1 -1
  10. package/dist/containerRuntime.js +242 -110
  11. package/dist/containerRuntime.js.map +1 -1
  12. package/dist/dataStoreContext.d.ts +4 -2
  13. package/dist/dataStoreContext.d.ts.map +1 -1
  14. package/dist/dataStoreContext.js +16 -5
  15. package/dist/dataStoreContext.js.map +1 -1
  16. package/dist/dataStores.d.ts +4 -3
  17. package/dist/dataStores.d.ts.map +1 -1
  18. package/dist/dataStores.js +9 -3
  19. package/dist/dataStores.js.map +1 -1
  20. package/dist/garbageCollection.d.ts +3 -3
  21. package/dist/garbageCollection.d.ts.map +1 -1
  22. package/dist/garbageCollection.js +10 -8
  23. package/dist/garbageCollection.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/orderedClientElection.js +0 -4
  29. package/dist/orderedClientElection.js.map +1 -1
  30. package/dist/packageVersion.d.ts +1 -1
  31. package/dist/packageVersion.d.ts.map +1 -1
  32. package/dist/packageVersion.js +1 -1
  33. package/dist/packageVersion.js.map +1 -1
  34. package/dist/pendingStateManager.d.ts +30 -29
  35. package/dist/pendingStateManager.d.ts.map +1 -1
  36. package/dist/pendingStateManager.js +72 -109
  37. package/dist/pendingStateManager.js.map +1 -1
  38. package/dist/runningSummarizer.d.ts +4 -3
  39. package/dist/runningSummarizer.d.ts.map +1 -1
  40. package/dist/runningSummarizer.js +11 -6
  41. package/dist/runningSummarizer.js.map +1 -1
  42. package/dist/serializedSnapshotStorage.d.ts +58 -0
  43. package/dist/serializedSnapshotStorage.d.ts.map +1 -0
  44. package/dist/serializedSnapshotStorage.js +108 -0
  45. package/dist/serializedSnapshotStorage.js.map +1 -0
  46. package/dist/summarizer.d.ts +11 -4
  47. package/dist/summarizer.d.ts.map +1 -1
  48. package/dist/summarizer.js +18 -9
  49. package/dist/summarizer.js.map +1 -1
  50. package/dist/summarizerHeuristics.d.ts +5 -3
  51. package/dist/summarizerHeuristics.d.ts.map +1 -1
  52. package/dist/summarizerHeuristics.js +10 -3
  53. package/dist/summarizerHeuristics.js.map +1 -1
  54. package/dist/summarizerTypes.d.ts +4 -2
  55. package/dist/summarizerTypes.d.ts.map +1 -1
  56. package/dist/summarizerTypes.js.map +1 -1
  57. package/dist/summaryManager.d.ts +3 -3
  58. package/dist/summaryManager.d.ts.map +1 -1
  59. package/dist/summaryManager.js +7 -7
  60. package/dist/summaryManager.js.map +1 -1
  61. package/garbageCollection.md +9 -1
  62. package/lib/blobManager.d.ts +2 -2
  63. package/lib/blobManager.d.ts.map +1 -1
  64. package/lib/blobManager.js +12 -11
  65. package/lib/blobManager.js.map +1 -1
  66. package/lib/connectionTelemetry.js +3 -3
  67. package/lib/connectionTelemetry.js.map +1 -1
  68. package/lib/containerRuntime.d.ts +125 -29
  69. package/lib/containerRuntime.d.ts.map +1 -1
  70. package/lib/containerRuntime.js +243 -111
  71. package/lib/containerRuntime.js.map +1 -1
  72. package/lib/dataStoreContext.d.ts +4 -2
  73. package/lib/dataStoreContext.d.ts.map +1 -1
  74. package/lib/dataStoreContext.js +16 -5
  75. package/lib/dataStoreContext.js.map +1 -1
  76. package/lib/dataStores.d.ts +4 -3
  77. package/lib/dataStores.d.ts.map +1 -1
  78. package/lib/dataStores.js +9 -3
  79. package/lib/dataStores.js.map +1 -1
  80. package/lib/garbageCollection.d.ts +3 -3
  81. package/lib/garbageCollection.d.ts.map +1 -1
  82. package/lib/garbageCollection.js +10 -8
  83. package/lib/garbageCollection.js.map +1 -1
  84. package/lib/index.d.ts +2 -2
  85. package/lib/index.d.ts.map +1 -1
  86. package/lib/index.js +1 -1
  87. package/lib/index.js.map +1 -1
  88. package/lib/orderedClientElection.js +0 -4
  89. package/lib/orderedClientElection.js.map +1 -1
  90. package/lib/packageVersion.d.ts +1 -1
  91. package/lib/packageVersion.d.ts.map +1 -1
  92. package/lib/packageVersion.js +1 -1
  93. package/lib/packageVersion.js.map +1 -1
  94. package/lib/pendingStateManager.d.ts +30 -29
  95. package/lib/pendingStateManager.d.ts.map +1 -1
  96. package/lib/pendingStateManager.js +72 -109
  97. package/lib/pendingStateManager.js.map +1 -1
  98. package/lib/runningSummarizer.d.ts +4 -3
  99. package/lib/runningSummarizer.d.ts.map +1 -1
  100. package/lib/runningSummarizer.js +11 -6
  101. package/lib/runningSummarizer.js.map +1 -1
  102. package/lib/serializedSnapshotStorage.d.ts +58 -0
  103. package/lib/serializedSnapshotStorage.d.ts.map +1 -0
  104. package/lib/serializedSnapshotStorage.js +104 -0
  105. package/lib/serializedSnapshotStorage.js.map +1 -0
  106. package/lib/summarizer.d.ts +11 -4
  107. package/lib/summarizer.d.ts.map +1 -1
  108. package/lib/summarizer.js +18 -9
  109. package/lib/summarizer.js.map +1 -1
  110. package/lib/summarizerHeuristics.d.ts +5 -3
  111. package/lib/summarizerHeuristics.d.ts.map +1 -1
  112. package/lib/summarizerHeuristics.js +10 -3
  113. package/lib/summarizerHeuristics.js.map +1 -1
  114. package/lib/summarizerTypes.d.ts +4 -2
  115. package/lib/summarizerTypes.d.ts.map +1 -1
  116. package/lib/summarizerTypes.js.map +1 -1
  117. package/lib/summaryManager.d.ts +3 -3
  118. package/lib/summaryManager.d.ts.map +1 -1
  119. package/lib/summaryManager.js +7 -7
  120. package/lib/summaryManager.js.map +1 -1
  121. package/package.json +46 -31
  122. package/src/blobManager.ts +29 -15
  123. package/src/connectionTelemetry.ts +3 -3
  124. package/src/containerRuntime.ts +388 -135
  125. package/src/dataStoreContext.ts +27 -5
  126. package/src/dataStores.ts +15 -3
  127. package/src/garbageCollection.ts +21 -14
  128. package/src/index.ts +7 -1
  129. package/src/orderedClientElection.ts +1 -1
  130. package/src/packageVersion.ts +1 -1
  131. package/src/pendingStateManager.ts +104 -123
  132. package/src/runningSummarizer.ts +20 -10
  133. package/src/serializedSnapshotStorage.ts +146 -0
  134. package/src/summarizer.ts +20 -16
  135. package/src/summarizerHeuristics.ts +21 -5
  136. package/src/summarizerTypes.ts +4 -2
  137. 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,
@@ -61,7 +59,7 @@ import {
61
59
  IQuorumClients,
62
60
  ISequencedDocumentMessage,
63
61
  ISignalMessage,
64
- ISummaryConfiguration,
62
+ ISnapshotTree,
65
63
  ISummaryContent,
66
64
  ISummaryTree,
67
65
  MessageType,
@@ -86,9 +84,11 @@ import {
86
84
  channelsTreeName,
87
85
  IAttachMessage,
88
86
  IDataStore,
87
+ ITelemetryContext,
89
88
  } from "@fluidframework/runtime-definitions";
90
89
  import {
91
90
  addBlobToSummary,
91
+ addSummarizeResultToSummary,
92
92
  addTreeToSummary,
93
93
  createRootSummarizerNodeWithGC,
94
94
  IRootSummarizerNodeWithGC,
@@ -99,7 +99,7 @@ import {
99
99
  responseToException,
100
100
  seqFromTree,
101
101
  calculateStats,
102
- addSummarizeResultToSummary,
102
+ TelemetryContext,
103
103
  } from "@fluidframework/runtime-utils";
104
104
  import { GCDataBuilder, trimLeadingAndTrailingSlashes } from "@fluidframework/garbage-collector";
105
105
  import { v4 as uuid } from "uuid";
@@ -154,6 +154,7 @@ import {
154
154
  isDataStoreAliasMessage,
155
155
  } from "./dataStore";
156
156
  import { BindBatchTracker } from "./batchTracker";
157
+ import { ISerializedBaseSnapshotBlobs, SerializedSnapshotStorage } from "./serializedSnapshotStorage";
157
158
  import { OpTracker } from "./opTelemetry";
158
159
 
159
160
  export enum ContainerMessageType {
@@ -190,22 +191,85 @@ export interface ContainerRuntimeMessage {
190
191
  contents: any;
191
192
  type: ContainerMessageType;
192
193
  }
194
+ export interface ISummaryBaseConfiguration {
195
+ /**
196
+ * Delay before first attempt to spawn summarizing container.
197
+ */
198
+ initialSummarizerDelayMs: number;
199
+
200
+ /**
201
+ * Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
202
+ * This defaults to false (disabled) and must be explicitly set to true to enable.
203
+ */
204
+ summarizerClientElection: boolean;
205
+
206
+ /**
207
+ * Defines the maximum allowed time to wait for a pending summary ack.
208
+ * The maximum amount of time client will wait for a summarize is the minimum of
209
+ * maxSummarizeAckWaitTime (currently 10 * 60 * 1000) and maxAckWaitTime.
210
+ */
211
+ maxAckWaitTime: number;
212
+ /**
213
+ * Defines the maximum number of Ops in between Summaries that can be
214
+ * allowed before forcibly electing a new summarizer client.
215
+ */
216
+ maxOpsSinceLastSummary: number;
217
+ }
218
+
219
+ export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfiguration {
220
+ state: "enabled";
221
+ /**
222
+ * Defines the maximum allowed time in between summarizations.
223
+ */
224
+ idleTime: number;
225
+ /**
226
+ * Defines the maximum allowed time, since the last received Ack, before running the summary
227
+ * with reason maxTime.
228
+ */
229
+ maxTime: number;
230
+ /**
231
+ * Defines the maximum number of Ops, since the last received Ack, that can be allowed
232
+ * before running the summary with reason maxOps.
233
+ */
234
+ maxOps: number;
235
+ /**
236
+ * Defnines the minimum number of Ops, since the last received Ack, that can be allowed
237
+ * before running the last summary.
238
+ */
239
+ minOpsForLastSummaryAttempt: number;
240
+ }
241
+
242
+ export interface ISummaryConfigurationDisableSummarizer {
243
+ state: "disabled";
244
+ }
245
+
246
+ export interface ISummaryConfigurationDisableHeuristics extends ISummaryBaseConfiguration {
247
+ state: "disableHeuristics";
248
+ }
249
+
250
+ export type ISummaryConfiguration =
251
+ | ISummaryConfigurationDisableSummarizer
252
+ | ISummaryConfigurationDisableHeuristics
253
+ | ISummaryConfigurationHeuristics;
254
+
255
+ export const DefaultSummaryConfiguration: ISummaryConfiguration = {
256
+ state: "enabled",
257
+
258
+ idleTime: 5000 * 3,
259
+
260
+ maxTime: 5000 * 12,
261
+
262
+ maxOps: 100, // Summarize if 100 ops received since last snapshot.
193
263
 
194
- // Consider idle 5s of no activity. And snapshot if a minute has gone by with no snapshot.
195
- const IdleDetectionTime = 5000;
264
+ minOpsForLastSummaryAttempt: 10,
196
265
 
197
- const DefaultSummaryConfiguration: ISummaryConfiguration = {
198
- idleTime: IdleDetectionTime * 3,
266
+ maxAckWaitTime: 6 * 10 * 1000, // 6 min.
199
267
 
200
- maxTime: IdleDetectionTime * 12,
268
+ maxOpsSinceLastSummary: 7000,
201
269
 
202
- // Summarize if 1000 ops received since last snapshot.
203
- maxOps: 1000,
270
+ initialSummarizerDelayMs: 5000, // 5 secs.
204
271
 
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,
272
+ summarizerClientElection: false,
209
273
  };
210
274
 
211
275
  export interface IGCRuntimeOptions {
@@ -245,39 +309,43 @@ export interface IGCRuntimeOptions {
245
309
  }
246
310
 
247
311
  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
312
 
263
313
  /** Override summary configurations set by the server. */
264
- summaryConfigOverrides?: Partial<ISummaryConfiguration>;
314
+ summaryConfigOverrides?: ISummaryConfiguration;
265
315
 
266
316
  // Flag that disables putting channels in isolated subtrees for each data store
267
317
  // and the root node when generating a summary if set to true.
268
318
  // Defaults to FALSE (enabled) for now.
269
319
  disableIsolatedChannels?: boolean;
270
320
 
271
- // Defaults to 7000 ops
272
- maxOpsSinceLastSummary?: number;
321
+ /**
322
+ * @deprecated - use `summaryConfigOverrides.initialSummarizerDelayMs` instead.
323
+ * Delay before first attempt to spawn summarizing container.
324
+ */
325
+ initialSummarizerDelayMs?: number;
326
+
327
+ /**
328
+ * @deprecated - use `summaryConfigOverrides.disableSummaries` instead.
329
+ * Flag that disables summaries if it is set to true.
330
+ */
331
+ disableSummaries?: boolean;
273
332
 
274
333
  /**
334
+ * @deprecated - use `summaryConfigOverrides.maxOpsSinceLastSummary` instead.
335
+ * Defaults to 7000 ops
336
+ */
337
+ maxOpsSinceLastSummary?: number;
338
+
339
+ /**
340
+ * @deprecated - use `summaryConfigOverrides.summarizerClientElection` instead.
275
341
  * 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.
342
+ * This defaults to false (disabled) and must be explicitly set to true to enable.
277
343
  */
278
344
  summarizerClientElection?: boolean;
279
345
 
280
- /** Options that control the running summarizer behavior. */
346
+ /**
347
+ * @deprecated - use `summaryConfigOverrides.state = "DisableHeuristics"` instead.
348
+ * Options that control the running summarizer behavior. */
281
349
  summarizerOptions?: Readonly<Partial<ISummarizerOptions>>;
282
350
  }
283
351
 
@@ -309,6 +377,10 @@ export interface IContainerRuntimeOptions {
309
377
  * By default, flush mode is TurnBased.
310
378
  */
311
379
  readonly flushMode?: FlushMode;
380
+ /**
381
+ * Save enough runtime state to be able to serialize upon request and load to the same state in a new container.
382
+ */
383
+ readonly enableOfflineLoad?: boolean;
312
384
  }
313
385
 
314
386
  type IRuntimeMessageMetadata = undefined | {
@@ -349,6 +421,33 @@ interface OldContainerContextWithLogger extends Omit<IContainerContext, "taggedL
349
421
  taggedLogger: undefined;
350
422
  }
351
423
 
424
+ /**
425
+ * State saved when the container closes, to be given back to a newly
426
+ * instantiated runtime in a new instance of the container, so it can load to the
427
+ * same state
428
+ */
429
+ export interface IPendingRuntimeState {
430
+ /**
431
+ * Pending ops from PendingStateManager
432
+ */
433
+ pending?: IPendingLocalState;
434
+ /**
435
+ * A base snapshot at a sequence number prior to the first pending op
436
+ */
437
+ baseSnapshot: ISnapshotTree;
438
+ /**
439
+ * Serialized blobs from the base snapshot. Used to load offline since
440
+ * storage is not available.
441
+ */
442
+ snapshotBlobs: ISerializedBaseSnapshotBlobs;
443
+ /**
444
+ * All runtime ops since base snapshot sequence number up to the latest op
445
+ * seen when the container was closed. Used to apply stashed (saved pending)
446
+ * ops at the same sequence number at which they were made.
447
+ */
448
+ savedOps: ISequencedDocumentMessage[];
449
+ }
450
+
352
451
  const useDataStoreAliasingKey = "Fluid.ContainerRuntime.UseDataStoreAliasing";
353
452
  const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
354
453
 
@@ -749,15 +848,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
749
848
  loadSequenceNumberVerification = "close",
750
849
  useDataStoreAliasing = false,
751
850
  flushMode = defaultFlushMode,
851
+ enableOfflineLoad = false,
752
852
  } = runtimeOptions;
753
853
 
754
- const storage = context.storage;
854
+ const pendingRuntimeState = context.pendingLocalState as IPendingRuntimeState | undefined;
855
+ const baseSnapshot: ISnapshotTree | undefined = pendingRuntimeState?.baseSnapshot ?? context.baseSnapshot;
856
+ const storage = !pendingRuntimeState ?
857
+ context.storage :
858
+ new SerializedSnapshotStorage(() => { return context.storage; }, pendingRuntimeState.snapshotBlobs);
755
859
 
756
860
  const registry = new FluidDataStoreRegistry(registryEntries);
757
861
 
758
862
  const tryFetchBlob = async <T>(blobName: string): Promise<T | undefined> => {
759
- const blobId = context.baseSnapshot?.blobs[blobName];
760
- if (context.baseSnapshot && blobId) {
863
+ const blobId = baseSnapshot?.blobs[blobName];
864
+ if (baseSnapshot && blobId) {
761
865
  // IContainerContext storage api return type still has undefined in 0.39 package version.
762
866
  // So once we release 0.40 container-defn package we can remove this check.
763
867
  assert(storage !== undefined, 0x1f5 /* "Attached state should have storage" */);
@@ -776,7 +880,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
776
880
 
777
881
  // read snapshot blobs needed for BlobManager to load
778
882
  const blobManagerSnapshot = await BlobManager.load(
779
- context.baseSnapshot?.trees[blobsTreeName],
883
+ baseSnapshot?.trees[blobsTreeName],
780
884
  async (id) => {
781
885
  // IContainerContext storage api return type still has undefined in 0.39 package version.
782
886
  // So once we release 0.40 container-defn package we can remove this check.
@@ -819,14 +923,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
819
923
  loadSequenceNumberVerification,
820
924
  useDataStoreAliasing,
821
925
  flushMode,
926
+ enableOfflineLoad,
822
927
  },
823
928
  containerScope,
824
929
  logger,
825
930
  loadExisting,
826
931
  blobManagerSnapshot,
932
+ storage,
827
933
  requestHandler,
828
934
  );
829
935
 
936
+ if (pendingRuntimeState) {
937
+ await runtime.processSavedOps(pendingRuntimeState);
938
+ // delete these once runtime has seen them to save space
939
+ pendingRuntimeState.savedOps = [];
940
+ }
941
+
942
+ await runtime.getSnapshotBlobs();
943
+
830
944
  return runtime;
831
945
  }
832
946
 
@@ -847,7 +961,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
847
961
  }
848
962
 
849
963
  public get storage(): IDocumentStorageService {
850
- return this.context.storage;
964
+ return this._storage;
851
965
  }
852
966
 
853
967
  public get reSubmitFn(): (
@@ -910,7 +1024,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
910
1024
 
911
1025
  private _connected: boolean;
912
1026
 
913
- private paused: boolean = false;
1027
+ private readonly savedOps: ISequencedDocumentMessage[] = [];
1028
+ private baseSnapshotBlobs?: ISerializedBaseSnapshotBlobs;
914
1029
 
915
1030
  private consecutiveReconnects = 0;
916
1031
 
@@ -923,15 +1038,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
923
1038
  return this.summarizerClientElection?.electedClientId;
924
1039
  }
925
1040
 
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
1041
  private _disposed = false;
936
1042
  public get disposed() { return this._disposed; }
937
1043
 
@@ -969,9 +1075,68 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
969
1075
  return this._summarizer;
970
1076
  }
971
1077
 
972
- private get summariesDisabled(): boolean {
973
- return this.runtimeOptions.summaryOptions.disableSummaries === true ||
974
- this.runtimeOptions.summaryOptions.summaryConfigOverrides?.disableSummaries === true;
1078
+ private readonly summariesDisabled: boolean;
1079
+ private isSummariesDisabled(): boolean {
1080
+ // back-compat: disableSummaries was moved from ISummaryRuntimeOptions
1081
+ // to ISummaryConfiguration in 0.60.
1082
+ if (this.runtimeOptions.summaryOptions.disableSummaries === true) {
1083
+ return true;
1084
+ }
1085
+ return this.summaryConfiguration.state === "disabled";
1086
+ }
1087
+
1088
+ private readonly heuristicsDisabled: boolean;
1089
+ private isHeuristicsDisabled(): boolean {
1090
+ // back-compat: disableHeuristics was moved from ISummarizerOptions
1091
+ // to ISummaryConfiguration in 0.60.
1092
+ if (this.runtimeOptions.summaryOptions.summarizerOptions?.disableHeuristics === true) {
1093
+ return true;
1094
+ }
1095
+ return this.summaryConfiguration.state === "disableHeuristics";
1096
+ }
1097
+
1098
+ private readonly summarizerClientElectionEnabled: boolean;
1099
+ private isSummarizerClientElectionEnabled(): boolean {
1100
+ if (this.mc.config.getBoolean("Fluid.ContainerRuntime.summarizerClientElection")) {
1101
+ return this.mc.config.getBoolean("Fluid.ContainerRuntime.summarizerClientElection") ?? true;
1102
+ }
1103
+ // back-compat: summarizerClientElection was moved from ISummaryRuntimeOptions
1104
+ // to ISummaryConfiguration in 0.60.
1105
+ if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
1106
+ return true;
1107
+ }
1108
+ if (this.summaryConfiguration.state !== "disabled") {
1109
+ return this.summaryConfiguration.summarizerClientElection === true;
1110
+ } else {
1111
+ return false;
1112
+ }
1113
+ }
1114
+ private readonly maxOpsSinceLastSummary: number;
1115
+ private getMaxOpsSinceLastSummary(): number {
1116
+ // back-compat: maxOpsSinceLastSummary was moved from ISummaryRuntimeOptions
1117
+ // to ISummaryConfiguration in 0.60.
1118
+ if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
1119
+ return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
1120
+ }
1121
+ if (this.summaryConfiguration.state !== "disabled") {
1122
+ return this.summaryConfiguration.maxOpsSinceLastSummary;
1123
+ } else {
1124
+ return 0;
1125
+ }
1126
+ }
1127
+
1128
+ private readonly initialSummarizerDelayMs: number;
1129
+ private getInitialSummarizerDelayMs(): number {
1130
+ // back-compat: initialSummarizerDelayMs was moved from ISummaryRuntimeOptions
1131
+ // to ISummaryConfiguration in 0.60.
1132
+ if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
1133
+ return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
1134
+ }
1135
+ if (this.summaryConfiguration.state !== "disabled") {
1136
+ return this.summaryConfiguration.initialSummarizerDelayMs;
1137
+ } else {
1138
+ return 0;
1139
+ }
975
1140
  }
976
1141
 
977
1142
  private readonly createContainerMetadata: ICreateContainerMetadata;
@@ -994,10 +1159,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
994
1159
  public readonly logger: ITelemetryLogger,
995
1160
  existing: boolean,
996
1161
  blobManagerSnapshot: IBlobManagerLoadInfo,
1162
+ private readonly _storage: IDocumentStorageService,
997
1163
  private readonly requestHandler?: (request: IRequest, runtime: IContainerRuntime) => Promise<IResponse>,
1164
+ private readonly summaryConfiguration: ISummaryConfiguration = {
1165
+ // the defaults
1166
+ ... DefaultSummaryConfiguration,
1167
+ // the runtime configuration overrides
1168
+ ... runtimeOptions.summaryOptions?.summaryConfigOverrides,
1169
+ },
998
1170
  ) {
999
1171
  super();
1000
-
1001
1172
  this.messageAtLastSummary = metadata?.message;
1002
1173
 
1003
1174
  // Default to false (enabled).
@@ -1011,6 +1182,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1011
1182
  this.mc = loggerToMonitoringContext(
1012
1183
  ChildLogger.create(this.logger, "ContainerRuntime"));
1013
1184
 
1185
+ this.summariesDisabled = this.isSummariesDisabled();
1186
+ this.heuristicsDisabled = this.isHeuristicsDisabled();
1187
+ this.summarizerClientElectionEnabled = this.isSummarizerClientElectionEnabled();
1188
+ this.maxOpsSinceLastSummary = this.getMaxOpsSinceLastSummary();
1189
+ this.initialSummarizerDelayMs = this.getInitialSummarizerDelayMs();
1190
+
1014
1191
  this._aliasingEnabled =
1015
1192
  (this.mc.config.getBoolean(useDataStoreAliasingKey) ?? false) ||
1016
1193
  (runtimeOptions.useDataStoreAliasing ?? false);
@@ -1020,12 +1197,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1020
1197
  this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? this.defaultMaxConsecutiveReconnects;
1021
1198
 
1022
1199
  this._flushMode = runtimeOptions.flushMode;
1200
+
1201
+ const pendingRuntimeState = context.pendingLocalState as IPendingRuntimeState | undefined;
1202
+ const baseSnapshot: ISnapshotTree | undefined = pendingRuntimeState?.baseSnapshot ?? context.baseSnapshot;
1203
+
1023
1204
  this.garbageCollector = GarbageCollector.create(
1024
1205
  this,
1025
1206
  this.runtimeOptions.gcOptions,
1026
1207
  (nodePath: string) => this.getGCNodePackagePath(nodePath),
1027
1208
  () => this.messageAtLastSummary?.timestamp,
1028
- context.baseSnapshot,
1209
+ baseSnapshot,
1029
1210
  async <T>(id: string) => readAndParse<T>(this.storage, id),
1030
1211
  this.mc.logger,
1031
1212
  existing,
@@ -1037,11 +1218,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1037
1218
  this.summarizerNode = createRootSummarizerNodeWithGC(
1038
1219
  ChildLogger.create(this.logger, "SummarizerNode"),
1039
1220
  // Summarize function to call when summarize is called. Summarizer node always tracks summary state.
1040
- async (fullTree: boolean, trackState: boolean) => this.summarizeInternal(fullTree, trackState),
1221
+ async (fullTree: boolean, trackState: boolean, telemetryContext?: ITelemetryContext) =>
1222
+ this.summarizeInternal(fullTree, trackState, telemetryContext),
1041
1223
  // Latest change sequence number, no changes since summary applied yet
1042
1224
  loadedFromSequenceNumber,
1043
1225
  // Summary reference sequence number, undefined if no summary yet
1044
- context.baseSnapshot ? loadedFromSequenceNumber : undefined,
1226
+ baseSnapshot ? loadedFromSequenceNumber : undefined,
1045
1227
  {
1046
1228
  // Must set to false to prevent sending summary handle which would be pointing to
1047
1229
  // a summary with an older protocol state.
@@ -1054,12 +1236,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1054
1236
  },
1055
1237
  );
1056
1238
 
1057
- if (this.context.baseSnapshot) {
1058
- this.summarizerNode.loadBaseSummaryWithoutDifferential(this.context.baseSnapshot);
1239
+ if (baseSnapshot) {
1240
+ this.summarizerNode.loadBaseSummaryWithoutDifferential(baseSnapshot);
1059
1241
  }
1060
1242
 
1061
1243
  this.dataStores = new DataStores(
1062
- getSummaryForDatastores(context.baseSnapshot, metadata),
1244
+ getSummaryForDatastores(baseSnapshot, metadata),
1063
1245
  this,
1064
1246
  (attachMsg) => this.submit(ContainerMessageType.Attach, attachMsg),
1065
1247
  (id: string, createParam: CreateChildSummarizerNodeParam) => (
@@ -1106,10 +1288,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1106
1288
  this.deltaSender = this.deltaManager;
1107
1289
 
1108
1290
  this.pendingStateManager = new PendingStateManager(
1109
- this,
1110
- async (type, content) => this.applyStashedOp(type, content),
1291
+ {
1292
+ applyStashedOp: this.applyStashedOp.bind(this),
1293
+ clientId: () => this.clientId,
1294
+ close: this.closeFn,
1295
+ connected: () => this.connected,
1296
+ flush: this.flush.bind(this),
1297
+ flushMode: () => this.flushMode,
1298
+ reSubmit: this.reSubmit.bind(this),
1299
+ rollback: this.rollback.bind(this),
1300
+ setFlushMode: (mode) => this.setFlushMode(mode),
1301
+ },
1111
1302
  this._flushMode,
1112
- context.pendingLocalState as IPendingLocalState);
1303
+ pendingRuntimeState?.pending);
1113
1304
 
1114
1305
  this.context.quorum.on("removeMember", (clientId: string) => {
1115
1306
  this.clearPartialChunks(clientId);
@@ -1117,16 +1308,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1117
1308
 
1118
1309
  this.summaryCollection = new SummaryCollection(this.deltaManager, this.logger);
1119
1310
 
1120
- const { attachState, pendingLocalState } = this.context;
1121
- this.dirtyContainer = attachState !== AttachState.Attached
1122
- || (pendingLocalState as IPendingLocalState)?.pendingStates.length > 0;
1311
+ this.dirtyContainer = this.context.attachState !== AttachState.Attached
1312
+ || this.pendingStateManager.hasPendingMessages();
1123
1313
  this.context.updateDirtyContainerState(this.dirtyContainer);
1124
1314
 
1125
- // Map the deprecated generateSummaries flag to disableSummaries.
1126
- if (this.runtimeOptions.summaryOptions.generateSummaries === false) {
1127
- this.runtimeOptions.summaryOptions.disableSummaries = true;
1128
- }
1129
-
1130
1315
  if (this.summariesDisabled) {
1131
1316
  this.mc.logger.sendTelemetryEvent({ eventName: "SummariesDisabled" });
1132
1317
  } else {
@@ -1143,16 +1328,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1143
1328
  electedSummarizerData ?? this.context.deltaManager.lastSequenceNumber,
1144
1329
  SummarizerClientElection.isClientEligible,
1145
1330
  );
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;
1331
+
1150
1332
  this.summarizerClientElection = new SummarizerClientElection(
1151
1333
  orderedClientLogger,
1152
1334
  this.summaryCollection,
1153
1335
  orderedClientElectionForSummarizer,
1154
- maxOpsSinceLastSummary,
1155
- summarizerClientElectionEnabled,
1336
+ this.maxOpsSinceLastSummary,
1337
+ this.summarizerClientElectionEnabled,
1156
1338
  );
1157
1339
 
1158
1340
  if (this.context.clientDetails.type === summarizerClientType) {
@@ -1169,7 +1351,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1169
1351
  // Only create a SummaryManager and SummarizerClientElection
1170
1352
  // if summaries are enabled and we are not the summarizer client.
1171
1353
  const defaultAction = () => {
1172
- if (this.summaryCollection.opsSinceLastAck > maxOpsSinceLastSummary) {
1354
+ if (this.summaryCollection.opsSinceLastAck > this.maxOpsSinceLastSummary) {
1173
1355
  this.logger.sendErrorEvent({ eventName: "SummaryStatus:Behind" });
1174
1356
  // unregister default to no log on every op after falling behind
1175
1357
  // and register summary ack handler to re-register this handler
@@ -1200,9 +1382,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1200
1382
  formExponentialFn({ coefficient: 20, initialDelay: 0 }),
1201
1383
  ),
1202
1384
  {
1203
- initialDelayMs: this.runtimeOptions.summaryOptions.initialSummarizerDelayMs,
1385
+ initialDelayMs: this.initialSummarizerDelayMs,
1204
1386
  },
1205
- this.runtimeOptions.summaryOptions.summarizerOptions,
1387
+ this.heuristicsDisabled,
1206
1388
  );
1207
1389
  this.summaryManager.start();
1208
1390
  }
@@ -1231,10 +1413,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1231
1413
  this.replayPendingStates();
1232
1414
  });
1233
1415
 
1234
- if (context.pendingLocalState !== undefined) {
1235
- this.deltaManager.on("op", this.onOp);
1236
- }
1237
-
1238
1416
  // logging hardware telemetry
1239
1417
  logger.sendTelemetryEvent({
1240
1418
  eventName: "DeviceSpec",
@@ -1439,6 +1617,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1439
1617
  summaryTree: ISummaryTreeWithStats,
1440
1618
  fullTree: boolean,
1441
1619
  trackState: boolean,
1620
+ telemetryContext?: ITelemetryContext,
1442
1621
  ) {
1443
1622
  this.addMetadataToSummary(summaryTree);
1444
1623
 
@@ -1465,7 +1644,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1465
1644
  }
1466
1645
 
1467
1646
  if (this.garbageCollector.writeDataAtRoot) {
1468
- const gcSummary = this.garbageCollector.summarize(fullTree, trackState);
1647
+ const gcSummary = this.garbageCollector.summarize(fullTree, trackState, telemetryContext);
1469
1648
  if (gcSummary !== undefined) {
1470
1649
  addSummarizeResultToSummary(summaryTree, gcTreeKey, gcSummary);
1471
1650
  }
@@ -1489,7 +1668,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1489
1668
  return true;
1490
1669
  }
1491
1670
 
1492
- this.consecutiveReconnects++;
1493
1671
  if (this.consecutiveReconnects === Math.floor(this.maxConsecutiveReconnects / 2)) {
1494
1672
  // If we're halfway through the max reconnects, send an event in order
1495
1673
  // to better identify false positives, if any. If the rate of this event
@@ -1498,6 +1676,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1498
1676
  this.mc.logger.sendTelemetryEvent({
1499
1677
  eventName: "ReconnectsWithNoProgress",
1500
1678
  attempts: this.consecutiveReconnects,
1679
+ pendingMessages: this.pendingStateManager.pendingMessagesCount,
1501
1680
  });
1502
1681
  }
1503
1682
 
@@ -1538,28 +1717,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1538
1717
  this.updateDocumentDirtyState(newState);
1539
1718
  }
1540
1719
 
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
1720
  private async applyStashedOp(type: ContainerMessageType, op: ISequencedDocumentMessage): Promise<unknown> {
1564
1721
  switch (type) {
1565
1722
  case ContainerMessageType.FluidDataStoreOp:
@@ -1583,20 +1740,26 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1583
1740
 
1584
1741
  // There might be no change of state due to Container calling this API after loading runtime.
1585
1742
  const changeOfState = this._connected !== connected;
1743
+ const reconnection = changeOfState && connected;
1586
1744
  this._connected = connected;
1587
1745
 
1588
- if (changeOfState) {
1589
- this.deltaManager.off("op", this.onOp);
1590
- this.context.pendingLocalState = undefined;
1746
+ if (reconnection) {
1747
+ this.consecutiveReconnects++;
1748
+
1591
1749
  if (!this.shouldContinueReconnecting()) {
1592
1750
  this.closeFn(new GenericError(
1593
1751
  // pre-0.58 error message: MaxReconnectsWithNoProgress
1594
1752
  "Runtime detected too many reconnects with no progress syncing local ops",
1595
1753
  undefined, // error
1596
- { attempts: this.consecutiveReconnects }));
1754
+ {
1755
+ attempts: this.consecutiveReconnects,
1756
+ pendingMessages: this.pendingStateManager.pendingMessagesCount,
1757
+ }));
1597
1758
  return;
1598
1759
  }
1760
+ }
1599
1761
 
1762
+ if (changeOfState) {
1600
1763
  this.replayPendingStates();
1601
1764
  }
1602
1765
 
@@ -1613,6 +1776,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1613
1776
  return;
1614
1777
  }
1615
1778
 
1779
+ if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
1780
+ this.savedOps.push(messageArg);
1781
+ }
1782
+
1616
1783
  // Do shallow copy of message, as methods below will modify it.
1617
1784
  // There might be multiple container instances receiving same message
1618
1785
  // We do not need to make deep copy, as each layer will just replace message.content itself,
@@ -1631,8 +1798,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1631
1798
  // once all pieces are available
1632
1799
  message = this.processRemoteChunkedMessage(message);
1633
1800
 
1634
- // Call the PendingStateManager to process messages.
1635
- const { localAck, localOpMetadata } = this.pendingStateManager.processMessage(message, local);
1801
+ let localOpMetadata: unknown;
1802
+ if (local) {
1803
+ // Call the PendingStateManager to process local messages.
1804
+ // Do not process local chunked ops until all pieces are available.
1805
+ if (message.type !== ContainerMessageType.ChunkedOp) {
1806
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1807
+ }
1808
+ }
1636
1809
 
1637
1810
  // If there are no more pending messages after processing a local message,
1638
1811
  // the document is no longer dirty.
@@ -1642,14 +1815,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1642
1815
 
1643
1816
  switch (message.type) {
1644
1817
  case ContainerMessageType.Attach:
1645
- this.dataStores.processAttachMessage(message, local || localAck);
1818
+ this.dataStores.processAttachMessage(message, local);
1646
1819
  break;
1647
1820
  case ContainerMessageType.Alias:
1648
1821
  this.processAliasMessage(message, localOpMetadata, local);
1649
1822
  break;
1650
1823
  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);
1824
+ this.dataStores.processFluidDataStoreOp(message, local, localOpMetadata);
1653
1825
  break;
1654
1826
  case ContainerMessageType.BlobAttach:
1655
1827
  assert(message?.metadata?.blobId, 0x12a /* "Missing blob id on metadata" */);
@@ -1770,18 +1942,31 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1770
1942
  const savedFlushMode = this.flushMode;
1771
1943
  this.setFlushMode(FlushMode.TurnBased);
1772
1944
 
1773
- this.trackOrderSequentiallyCalls(callback);
1774
- this.flush();
1775
- this.setFlushMode(savedFlushMode);
1945
+ try {
1946
+ this.trackOrderSequentiallyCalls(callback);
1947
+ this.flush();
1948
+ } finally {
1949
+ this.setFlushMode(savedFlushMode);
1950
+ }
1776
1951
  }
1777
1952
 
1778
1953
  private trackOrderSequentiallyCalls(callback: () => void): void {
1954
+ let checkpoint: { rollback: () => void; } | undefined;
1955
+ if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
1956
+ checkpoint = this.pendingStateManager.checkpoint();
1957
+ }
1958
+
1779
1959
  try {
1780
1960
  this._orderSequentiallyCalls++;
1781
1961
  callback();
1782
1962
  } catch (error) {
1783
- // pre-0.58 error message: orderSequentiallyCallbackException
1784
- this.closeFn(new GenericError("orderSequentially callback exception", error));
1963
+ if (checkpoint) {
1964
+ // This will throw and close the container if rollback fails
1965
+ checkpoint.rollback();
1966
+ } else {
1967
+ // pre-0.58 error message: orderSequentiallyCallbackException
1968
+ this.closeFn(new GenericError("orderSequentially callback exception", error));
1969
+ }
1785
1970
  throw error; // throw the original error for the consumer of the runtime
1786
1971
  } finally {
1787
1972
  this._orderSequentiallyCalls--;
@@ -1996,13 +2181,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1996
2181
  * @param blobRedirectTable - A table passed during the attach process. While detached, blob upload is supported
1997
2182
  * using IDs generated locally. After attach, these IDs cannot be used, so this table maps the old local IDs to the
1998
2183
  * new storage IDs so requests can be redirected.
2184
+ * @param telemetryContext - summary data passed through the layers for telemetry purposes
1999
2185
  */
2000
- public createSummary(blobRedirectTable?: Map<string, string>): ISummaryTree {
2186
+ public createSummary(blobRedirectTable?: Map<string, string>, telemetryContext?: ITelemetryContext): ISummaryTree {
2001
2187
  if (blobRedirectTable) {
2002
2188
  this.blobManager.setRedirectTable(blobRedirectTable);
2003
2189
  }
2004
2190
 
2005
- const summarizeResult = this.dataStores.createSummary();
2191
+ const summarizeResult = this.dataStores.createSummary(telemetryContext);
2006
2192
  if (!this.disableIsolatedChannels) {
2007
2193
  // Wrap data store summaries in .channels subtree.
2008
2194
  wrapSummaryInChannelsTree(summarizeResult);
@@ -2010,7 +2196,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2010
2196
  this.addContainerStateToSummary(
2011
2197
  summarizeResult,
2012
2198
  true /* fullTree */,
2013
- false /* trackState */);
2199
+ false /* trackState */,
2200
+ telemetryContext,
2201
+ );
2014
2202
  return summarizeResult.summary;
2015
2203
  }
2016
2204
 
@@ -2024,8 +2212,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2024
2212
  return this.context.getAbsoluteUrl(relativeUrl);
2025
2213
  }
2026
2214
 
2027
- private async summarizeInternal(fullTree: boolean, trackState: boolean): Promise<ISummarizeInternalResult> {
2028
- const summarizeResult = await this.dataStores.summarize(fullTree, trackState);
2215
+ private async summarizeInternal(
2216
+ fullTree: boolean,
2217
+ trackState: boolean,
2218
+ telemetryContext?: ITelemetryContext,
2219
+ ): Promise<ISummarizeInternalResult> {
2220
+ const summarizeResult = await this.dataStores.summarize(fullTree, trackState, telemetryContext);
2029
2221
  let pathPartsForChildren: string[] | undefined;
2030
2222
 
2031
2223
  if (!this.disableIsolatedChannels) {
@@ -2033,7 +2225,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2033
2225
  wrapSummaryInChannelsTree(summarizeResult);
2034
2226
  pathPartsForChildren = [channelsTreeName];
2035
2227
  }
2036
- this.addContainerStateToSummary(summarizeResult, fullTree, trackState);
2228
+ this.addContainerStateToSummary(summarizeResult, fullTree, trackState, telemetryContext);
2037
2229
  return {
2038
2230
  ...summarizeResult,
2039
2231
  id: "",
@@ -2074,7 +2266,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2074
2266
  gcStats = await this.collectGarbage({ logger: summaryLogger, runSweep, fullGC });
2075
2267
  }
2076
2268
 
2077
- const { stats, summary } = await this.summarizerNode.summarize(fullTree, trackState);
2269
+ const telemetryContext = new TelemetryContext();
2270
+ const { stats, summary } = await this.summarizerNode.summarize(fullTree, trackState, telemetryContext);
2271
+
2272
+ this.logger.sendTelemetryEvent({ eventName: "SummarizeTelemetry", details: telemetryContext.serialize() });
2078
2273
 
2079
2274
  assert(summary.type === SummaryType.Tree,
2080
2275
  0x12f /* "Container Runtime's summarize should always return a tree" */);
@@ -2531,9 +2726,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2531
2726
  ): void {
2532
2727
  this.verifyNotClosed();
2533
2728
 
2534
- if (this.context.pendingLocalState !== undefined) {
2535
- this.closeFn(new GenericError("containerRuntimeSubmitWithPendingLocalState"));
2536
- }
2537
2729
  // There should be no ops in detached container state!
2538
2730
  assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
2539
2731
 
@@ -2729,6 +2921,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2729
2921
  }
2730
2922
  }
2731
2923
 
2924
+ private rollback(
2925
+ type: ContainerMessageType,
2926
+ content: any,
2927
+ localOpMetadata: unknown,
2928
+ ) {
2929
+ switch (type) {
2930
+ case ContainerMessageType.FluidDataStoreOp:
2931
+ // For operations, call rollbackDataStoreOp which will find the right store
2932
+ // and trigger rollback on it.
2933
+ this.dataStores.rollbackDataStoreOp(content, localOpMetadata);
2934
+ break;
2935
+ default:
2936
+ throw new Error(`Can't rollback ${type}`);
2937
+ }
2938
+ }
2939
+
2732
2940
  /** Implementation of ISummarizerInternalsProvider.refreshLatestSummaryAck */
2733
2941
  public async refreshLatestSummaryAck(
2734
2942
  proposalHandle: string | undefined,
@@ -2807,8 +3015,43 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2807
3015
  });
2808
3016
  }
2809
3017
 
2810
- public getPendingLocalState() {
2811
- return this.pendingStateManager.getLocalState();
3018
+ public notifyAttaching(snapshot: ISnapshotTreeWithBlobContents) {
3019
+ if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
3020
+ this.baseSnapshotBlobs = SerializedSnapshotStorage.serializeTreeWithBlobContents(snapshot);
3021
+ }
3022
+ }
3023
+
3024
+ public async getSnapshotBlobs(): Promise<void> {
3025
+ if (!(this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) ||
3026
+ this.attachState !== AttachState.Attached || this.context.pendingLocalState) {
3027
+ return;
3028
+ }
3029
+ assert(!!this.context.baseSnapshot, 0x2e5 /* "Must have a base snapshot" */);
3030
+ this.baseSnapshotBlobs = await SerializedSnapshotStorage.serializeTree(this.context.baseSnapshot, this.storage);
3031
+ }
3032
+
3033
+ public getPendingLocalState(): IPendingRuntimeState {
3034
+ if (!(this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad)) {
3035
+ throw new UsageError("can't get state when offline load disabled");
3036
+ }
3037
+
3038
+ const previousPendingState = this.context.pendingLocalState as IPendingRuntimeState | undefined;
3039
+ if (previousPendingState) {
3040
+ return {
3041
+ pending: this.pendingStateManager.getLocalState(),
3042
+ snapshotBlobs: previousPendingState.snapshotBlobs,
3043
+ baseSnapshot: previousPendingState.baseSnapshot,
3044
+ savedOps: this.savedOps,
3045
+ };
3046
+ }
3047
+ assert(!!this.context.baseSnapshot, 0x2e6 /* "Must have a base snapshot" */);
3048
+ assert(!!this.baseSnapshotBlobs, 0x2e7 /* "Must serialize base snapshot blobs before getting runtime state" */);
3049
+ return {
3050
+ pending: this.pendingStateManager.getLocalState(),
3051
+ snapshotBlobs: this.baseSnapshotBlobs,
3052
+ baseSnapshot: this.context.baseSnapshot,
3053
+ savedOps: this.savedOps,
3054
+ };
2812
3055
  }
2813
3056
 
2814
3057
  public readonly summarizeOnDemand: ISummarizer["summarizeOnDemand"] = (...args) => {
@@ -2837,7 +3080,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2837
3080
  // because it is a misuse of the API rather than an expected failure.
2838
3081
  throw new UsageError(
2839
3082
  `Can't summarize, disableSummaries: ${this.summariesDisabled}`,
2840
- );
3083
+ );
2841
3084
  }
2842
3085
  };
2843
3086
 
@@ -2870,6 +3113,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2870
3113
  return summarizer;
2871
3114
  };
2872
3115
  }
3116
+
3117
+ private async processSavedOps(state: IPendingRuntimeState) {
3118
+ for (const op of state.savedOps) {
3119
+ this.process(op, false);
3120
+ await this.pendingStateManager.applyStashedOpsAt(op.sequenceNumber);
3121
+ }
3122
+ // we may not have seen every sequence number (because of system ops) so apply everything once we
3123
+ // don't have any more saved ops
3124
+ await this.pendingStateManager.applyStashedOpsAt();
3125
+ }
2873
3126
  }
2874
3127
 
2875
3128
  /**