@fluidframework/container-runtime 1.3.0 → 2.0.0-dev.1.4.5.105745

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 (222) hide show
  1. package/.eslintrc.js +8 -22
  2. package/.mocharc.js +12 -0
  3. package/dist/batchManager.d.ts +37 -0
  4. package/dist/batchManager.d.ts.map +1 -0
  5. package/dist/batchManager.js +73 -0
  6. package/dist/batchManager.js.map +1 -0
  7. package/dist/batchTracker.d.ts +1 -2
  8. package/dist/batchTracker.d.ts.map +1 -1
  9. package/dist/batchTracker.js +2 -3
  10. package/dist/batchTracker.js.map +1 -1
  11. package/dist/blobManager.d.ts +87 -25
  12. package/dist/blobManager.d.ts.map +1 -1
  13. package/dist/blobManager.js +317 -99
  14. package/dist/blobManager.js.map +1 -1
  15. package/dist/containerRuntime.d.ts +110 -125
  16. package/dist/containerRuntime.d.ts.map +1 -1
  17. package/dist/containerRuntime.js +360 -549
  18. package/dist/containerRuntime.js.map +1 -1
  19. package/dist/dataStore.js +29 -24
  20. package/dist/dataStore.js.map +1 -1
  21. package/dist/dataStoreContext.d.ts +20 -14
  22. package/dist/dataStoreContext.d.ts.map +1 -1
  23. package/dist/dataStoreContext.js +49 -58
  24. package/dist/dataStoreContext.js.map +1 -1
  25. package/dist/dataStores.d.ts +12 -5
  26. package/dist/dataStores.d.ts.map +1 -1
  27. package/dist/dataStores.js +21 -20
  28. package/dist/dataStores.js.map +1 -1
  29. package/dist/deltaScheduler.d.ts +6 -4
  30. package/dist/deltaScheduler.d.ts.map +1 -1
  31. package/dist/deltaScheduler.js +6 -4
  32. package/dist/deltaScheduler.js.map +1 -1
  33. package/dist/garbageCollection.d.ts +74 -14
  34. package/dist/garbageCollection.d.ts.map +1 -1
  35. package/dist/garbageCollection.js +248 -169
  36. package/dist/garbageCollection.js.map +1 -1
  37. package/dist/gcSweepReadyUsageDetection.d.ts +53 -0
  38. package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -0
  39. package/dist/gcSweepReadyUsageDetection.js +135 -0
  40. package/dist/gcSweepReadyUsageDetection.js.map +1 -0
  41. package/dist/index.d.ts +2 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/opProperties.d.ts +7 -0
  46. package/dist/opProperties.d.ts.map +1 -0
  47. package/dist/opProperties.js +20 -0
  48. package/dist/opProperties.js.map +1 -0
  49. package/dist/orderedClientElection.d.ts +28 -10
  50. package/dist/orderedClientElection.d.ts.map +1 -1
  51. package/dist/orderedClientElection.js +14 -4
  52. package/dist/orderedClientElection.js.map +1 -1
  53. package/dist/packageVersion.d.ts +1 -1
  54. package/dist/packageVersion.d.ts.map +1 -1
  55. package/dist/packageVersion.js +1 -1
  56. package/dist/packageVersion.js.map +1 -1
  57. package/dist/pendingStateManager.d.ts +0 -11
  58. package/dist/pendingStateManager.d.ts.map +1 -1
  59. package/dist/pendingStateManager.js +24 -46
  60. package/dist/pendingStateManager.js.map +1 -1
  61. package/dist/runningSummarizer.d.ts +14 -4
  62. package/dist/runningSummarizer.d.ts.map +1 -1
  63. package/dist/runningSummarizer.js +69 -27
  64. package/dist/runningSummarizer.js.map +1 -1
  65. package/dist/scheduleManager.d.ts +31 -0
  66. package/dist/scheduleManager.d.ts.map +1 -0
  67. package/dist/scheduleManager.js +243 -0
  68. package/dist/scheduleManager.js.map +1 -0
  69. package/dist/summarizer.d.ts +0 -2
  70. package/dist/summarizer.d.ts.map +1 -1
  71. package/dist/summarizer.js +1 -12
  72. package/dist/summarizer.js.map +1 -1
  73. package/dist/summarizerHeuristics.d.ts +26 -4
  74. package/dist/summarizerHeuristics.d.ts.map +1 -1
  75. package/dist/summarizerHeuristics.js +98 -18
  76. package/dist/summarizerHeuristics.js.map +1 -1
  77. package/dist/summarizerTypes.d.ts +45 -18
  78. package/dist/summarizerTypes.d.ts.map +1 -1
  79. package/dist/summarizerTypes.js +1 -1
  80. package/dist/summarizerTypes.js.map +1 -1
  81. package/dist/summaryCollection.d.ts +1 -0
  82. package/dist/summaryCollection.d.ts.map +1 -1
  83. package/dist/summaryCollection.js +31 -15
  84. package/dist/summaryCollection.js.map +1 -1
  85. package/dist/summaryFormat.d.ts +0 -5
  86. package/dist/summaryFormat.d.ts.map +1 -1
  87. package/dist/summaryFormat.js.map +1 -1
  88. package/dist/summaryGenerator.d.ts +1 -0
  89. package/dist/summaryGenerator.d.ts.map +1 -1
  90. package/dist/summaryGenerator.js +11 -9
  91. package/dist/summaryGenerator.js.map +1 -1
  92. package/dist/summaryManager.d.ts +2 -2
  93. package/dist/summaryManager.d.ts.map +1 -1
  94. package/dist/summaryManager.js +22 -7
  95. package/dist/summaryManager.js.map +1 -1
  96. package/lib/batchManager.d.ts +37 -0
  97. package/lib/batchManager.d.ts.map +1 -0
  98. package/lib/batchManager.js +69 -0
  99. package/lib/batchManager.js.map +1 -0
  100. package/lib/batchTracker.d.ts +1 -2
  101. package/lib/batchTracker.d.ts.map +1 -1
  102. package/lib/batchTracker.js +2 -3
  103. package/lib/batchTracker.js.map +1 -1
  104. package/lib/blobManager.d.ts +87 -25
  105. package/lib/blobManager.d.ts.map +1 -1
  106. package/lib/blobManager.js +319 -101
  107. package/lib/blobManager.js.map +1 -1
  108. package/lib/containerRuntime.d.ts +110 -125
  109. package/lib/containerRuntime.d.ts.map +1 -1
  110. package/lib/containerRuntime.js +366 -554
  111. package/lib/containerRuntime.js.map +1 -1
  112. package/lib/dataStore.js +29 -24
  113. package/lib/dataStore.js.map +1 -1
  114. package/lib/dataStoreContext.d.ts +20 -14
  115. package/lib/dataStoreContext.d.ts.map +1 -1
  116. package/lib/dataStoreContext.js +46 -55
  117. package/lib/dataStoreContext.js.map +1 -1
  118. package/lib/dataStores.d.ts +12 -5
  119. package/lib/dataStores.d.ts.map +1 -1
  120. package/lib/dataStores.js +21 -20
  121. package/lib/dataStores.js.map +1 -1
  122. package/lib/deltaScheduler.d.ts +6 -4
  123. package/lib/deltaScheduler.d.ts.map +1 -1
  124. package/lib/deltaScheduler.js +6 -4
  125. package/lib/deltaScheduler.js.map +1 -1
  126. package/lib/garbageCollection.d.ts +74 -14
  127. package/lib/garbageCollection.d.ts.map +1 -1
  128. package/lib/garbageCollection.js +237 -159
  129. package/lib/garbageCollection.js.map +1 -1
  130. package/lib/gcSweepReadyUsageDetection.d.ts +53 -0
  131. package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -0
  132. package/lib/gcSweepReadyUsageDetection.js +130 -0
  133. package/lib/gcSweepReadyUsageDetection.js.map +1 -0
  134. package/lib/index.d.ts +2 -1
  135. package/lib/index.d.ts.map +1 -1
  136. package/lib/index.js +2 -1
  137. package/lib/index.js.map +1 -1
  138. package/lib/opProperties.d.ts +7 -0
  139. package/lib/opProperties.d.ts.map +1 -0
  140. package/lib/opProperties.js +16 -0
  141. package/lib/opProperties.js.map +1 -0
  142. package/lib/orderedClientElection.d.ts +28 -10
  143. package/lib/orderedClientElection.d.ts.map +1 -1
  144. package/lib/orderedClientElection.js +14 -4
  145. package/lib/orderedClientElection.js.map +1 -1
  146. package/lib/packageVersion.d.ts +1 -1
  147. package/lib/packageVersion.d.ts.map +1 -1
  148. package/lib/packageVersion.js +1 -1
  149. package/lib/packageVersion.js.map +1 -1
  150. package/lib/pendingStateManager.d.ts +0 -11
  151. package/lib/pendingStateManager.d.ts.map +1 -1
  152. package/lib/pendingStateManager.js +24 -46
  153. package/lib/pendingStateManager.js.map +1 -1
  154. package/lib/runningSummarizer.d.ts +14 -4
  155. package/lib/runningSummarizer.d.ts.map +1 -1
  156. package/lib/runningSummarizer.js +69 -27
  157. package/lib/runningSummarizer.js.map +1 -1
  158. package/lib/scheduleManager.d.ts +31 -0
  159. package/lib/scheduleManager.d.ts.map +1 -0
  160. package/lib/scheduleManager.js +239 -0
  161. package/lib/scheduleManager.js.map +1 -0
  162. package/lib/summarizer.d.ts +0 -2
  163. package/lib/summarizer.d.ts.map +1 -1
  164. package/lib/summarizer.js +1 -12
  165. package/lib/summarizer.js.map +1 -1
  166. package/lib/summarizerHeuristics.d.ts +26 -4
  167. package/lib/summarizerHeuristics.d.ts.map +1 -1
  168. package/lib/summarizerHeuristics.js +98 -18
  169. package/lib/summarizerHeuristics.js.map +1 -1
  170. package/lib/summarizerTypes.d.ts +45 -18
  171. package/lib/summarizerTypes.d.ts.map +1 -1
  172. package/lib/summarizerTypes.js +1 -1
  173. package/lib/summarizerTypes.js.map +1 -1
  174. package/lib/summaryCollection.d.ts +1 -0
  175. package/lib/summaryCollection.d.ts.map +1 -1
  176. package/lib/summaryCollection.js +31 -15
  177. package/lib/summaryCollection.js.map +1 -1
  178. package/lib/summaryFormat.d.ts +0 -5
  179. package/lib/summaryFormat.d.ts.map +1 -1
  180. package/lib/summaryFormat.js.map +1 -1
  181. package/lib/summaryGenerator.d.ts +1 -0
  182. package/lib/summaryGenerator.d.ts.map +1 -1
  183. package/lib/summaryGenerator.js +11 -9
  184. package/lib/summaryGenerator.js.map +1 -1
  185. package/lib/summaryManager.d.ts +2 -2
  186. package/lib/summaryManager.d.ts.map +1 -1
  187. package/lib/summaryManager.js +22 -7
  188. package/lib/summaryManager.js.map +1 -1
  189. package/package.json +68 -25
  190. package/src/batchManager.ts +91 -0
  191. package/src/batchTracker.ts +2 -3
  192. package/src/blobManager.ts +385 -118
  193. package/src/containerRuntime.ts +523 -732
  194. package/src/dataStore.ts +49 -37
  195. package/src/dataStoreContext.ts +44 -56
  196. package/src/dataStores.ts +34 -30
  197. package/src/deltaScheduler.ts +6 -4
  198. package/src/garbageCollection.ts +296 -205
  199. package/src/gcSweepReadyUsageDetection.ts +147 -0
  200. package/src/index.ts +1 -2
  201. package/src/opProperties.ts +19 -0
  202. package/src/orderedClientElection.ts +31 -10
  203. package/src/packageVersion.ts +1 -1
  204. package/src/pendingStateManager.ts +27 -59
  205. package/src/runningSummarizer.ts +76 -23
  206. package/src/scheduleManager.ts +314 -0
  207. package/src/summarizer.ts +1 -18
  208. package/src/summarizerHeuristics.ts +136 -19
  209. package/src/summarizerTypes.ts +53 -18
  210. package/src/summaryCollection.ts +33 -18
  211. package/src/summaryFormat.ts +0 -6
  212. package/src/summaryGenerator.ts +40 -22
  213. package/src/summaryManager.ts +22 -7
  214. package/dist/opTelemetry.d.ts +0 -22
  215. package/dist/opTelemetry.d.ts.map +0 -1
  216. package/dist/opTelemetry.js +0 -59
  217. package/dist/opTelemetry.js.map +0 -1
  218. package/lib/opTelemetry.d.ts +0 -22
  219. package/lib/opTelemetry.d.ts.map +0 -1
  220. package/lib/opTelemetry.js +0 -55
  221. package/lib/opTelemetry.js.map +0 -1
  222. package/src/opTelemetry.ts +0 -71
@@ -2,7 +2,6 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { EventEmitter } from "events";
6
5
  import { ITelemetryBaseLogger, ITelemetryGenericEvent, ITelemetryLogger } from "@fluidframework/common-definitions";
7
6
  import {
8
7
  FluidObject,
@@ -24,6 +23,7 @@ import {
24
23
  ILoaderOptions,
25
24
  LoaderHeader,
26
25
  ISnapshotTreeWithBlobContents,
26
+ IBatchMessage,
27
27
  } from "@fluidframework/container-definitions";
28
28
  import {
29
29
  IContainerRuntime,
@@ -34,7 +34,6 @@ import {
34
34
  Trace,
35
35
  TypedEventEmitter,
36
36
  unreachableCase,
37
- performance,
38
37
  } from "@fluidframework/common-utils";
39
38
  import {
40
39
  ChildLogger,
@@ -43,16 +42,20 @@ import {
43
42
  TaggedLoggerAdapter,
44
43
  MonitoringContext,
45
44
  loggerToMonitoringContext,
46
- TelemetryDataTag,
45
+ wrapError,
47
46
  } from "@fluidframework/telemetry-utils";
48
- import { DriverHeader, IDocumentStorageService, ISummaryContext } from "@fluidframework/driver-definitions";
49
- import { readAndParse, isUnpackedRuntimeMessage } from "@fluidframework/driver-utils";
47
+ import {
48
+ DriverHeader,
49
+ FetchSource,
50
+ IDocumentStorageService,
51
+ ISummaryContext,
52
+ } from "@fluidframework/driver-definitions";
53
+ import { readAndParse } from "@fluidframework/driver-utils";
50
54
  import {
51
55
  DataCorruptionError,
52
56
  DataProcessingError,
53
57
  GenericError,
54
58
  UsageError,
55
- extractSafePropertiesFromMessage,
56
59
  } from "@fluidframework/container-utils";
57
60
  import {
58
61
  IClientDetails,
@@ -108,15 +111,17 @@ import { ContainerFluidHandleContext } from "./containerHandleContext";
108
111
  import { FluidDataStoreRegistry } from "./dataStoreRegistry";
109
112
  import { Summarizer } from "./summarizer";
110
113
  import { SummaryManager } from "./summaryManager";
111
- import { DeltaScheduler } from "./deltaScheduler";
112
114
  import {
113
115
  ReportOpPerfTelemetry,
114
- latencyThreshold,
115
116
  IPerfSignalReport,
116
117
  } from "./connectionTelemetry";
117
- import { IPendingLocalState, PendingStateManager } from "./pendingStateManager";
118
+ import {
119
+ IPendingLocalState,
120
+ PendingStateManager,
121
+ } from "./pendingStateManager";
122
+ import { BatchManager, BatchMessage } from "./batchManager";
118
123
  import { pkgVersion } from "./packageVersion";
119
- import { BlobManager, IBlobManagerLoadInfo } from "./blobManager";
124
+ import { BlobManager, IBlobManagerLoadInfo, IPendingBlobs } from "./blobManager";
120
125
  import { DataStores, getSummaryForDatastores } from "./dataStores";
121
126
  import {
122
127
  aliasBlobName,
@@ -160,7 +165,7 @@ import {
160
165
  } from "./dataStore";
161
166
  import { BindBatchTracker } from "./batchTracker";
162
167
  import { ISerializedBaseSnapshotBlobs, SerializedSnapshotStorage } from "./serializedSnapshotStorage";
163
- import { OpTracker } from "./opTelemetry";
168
+ import { ScheduleManager } from "./scheduleManager";
164
169
 
165
170
  export enum ContainerMessageType {
166
171
  // An op to be delivered to store
@@ -196,9 +201,10 @@ export interface ContainerRuntimeMessage {
196
201
  contents: any;
197
202
  type: ContainerMessageType;
198
203
  }
204
+
199
205
  export interface ISummaryBaseConfiguration {
200
206
  /**
201
- * Delay before first attempt to spawn summarizing container.
207
+ * Delay before first attempt to spawn summarizing container.
202
208
  */
203
209
  initialSummarizerDelayMs: number;
204
210
 
@@ -224,12 +230,15 @@ export interface ISummaryBaseConfiguration {
224
230
  export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfiguration {
225
231
  state: "enabled";
226
232
  /**
227
- * Defines the maximum allowed time in between summarizations.
233
+ * @deprecated Please move all implementations to {@link ISummaryConfigurationHeuristics.minIdleTime} and
234
+ * {@link ISummaryConfigurationHeuristics.maxIdleTime} instead.
228
235
  */
229
- idleTime: number;
236
+ idleTime?: number;
230
237
  /**
231
- * Defines the maximum allowed time, since the last received Ack, before running the summary
238
+ * Defines the maximum allowed time, since the last received Ack, before running the summary
232
239
  * with reason maxTime.
240
+ * For example, say we receive ops one by one just before the idle time is triggered.
241
+ * In this case, we still want to run a summary since it's been a while since the last summary.
233
242
  */
234
243
  maxTime: number;
235
244
  /**
@@ -242,6 +251,34 @@ export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfigurati
242
251
  * before running the last summary.
243
252
  */
244
253
  minOpsForLastSummaryAttempt: number;
254
+ /**
255
+ * Defines the lower boundary for the allowed time in between summarizations.
256
+ * Pairs with maxIdleTime to form a range.
257
+ * For example, if we only receive 1 op, we don't want to have the same idle time as say 100 ops.
258
+ * Based on the boundaries we set in minIdleTime and maxIdleTime, the idle time will change
259
+ * linearly depending on the number of ops we receive.
260
+ */
261
+ minIdleTime: number;
262
+ /**
263
+ * Defines the upper boundary for the allowed time in between summarizations.
264
+ * Pairs with minIdleTime to form a range.
265
+ * For example, if we only receive 1 op, we don't want to have the same idle time as say 100 ops.
266
+ * Based on the boundaries we set in minIdleTime and maxIdleTime, the idle time will change
267
+ * linearly depending on the number of ops we receive.
268
+ */
269
+ maxIdleTime: number;
270
+ /**
271
+ * Runtime op weight to use in heuristic summarizing.
272
+ * This number is a multiplier on the number of runtime ops we process when running summarize heuristics.
273
+ * For example: (multiplier) * (number of runtime ops) = weighted number of runtime ops
274
+ */
275
+ runtimeOpWeight: number;
276
+ /**
277
+ * Non-runtime op weight to use in heuristic summarizing
278
+ * This number is a multiplier on the number of non-runtime ops we process when running summarize heuristics.
279
+ * For example: (multiplier) * (number of non-runtime ops) = weighted number of non-runtime ops
280
+ */
281
+ nonRuntimeOpWeight: number;
245
282
  }
246
283
 
247
284
  export interface ISummaryConfigurationDisableSummarizer {
@@ -260,44 +297,55 @@ export type ISummaryConfiguration =
260
297
  export const DefaultSummaryConfiguration: ISummaryConfiguration = {
261
298
  state: "enabled",
262
299
 
263
- idleTime: 5000 * 3,
300
+ minIdleTime: 0,
264
301
 
265
- maxTime: 5000 * 12,
302
+ maxIdleTime: 30 * 1000, // 30 secs.
266
303
 
267
- maxOps: 100, // Summarize if 100 ops received since last snapshot.
304
+ maxTime: 60 * 1000, // 1 min.
305
+
306
+ maxOps: 100, // Summarize if 100 weighted ops received since last snapshot.
268
307
 
269
308
  minOpsForLastSummaryAttempt: 10,
270
309
 
271
- maxAckWaitTime: 6 * 10 * 1000, // 6 min.
310
+ maxAckWaitTime: 10 * 60 * 1000, // 10 mins.
272
311
 
273
312
  maxOpsSinceLastSummary: 7000,
274
313
 
275
- initialSummarizerDelayMs: 5000, // 5 secs.
314
+ initialSummarizerDelayMs: 5 * 1000, // 5 secs.
276
315
 
277
316
  summarizerClientElection: false,
317
+
318
+ nonRuntimeOpWeight: 0.1,
319
+
320
+ runtimeOpWeight: 1.0,
278
321
  };
279
322
 
280
323
  export interface IGCRuntimeOptions {
281
324
  /**
282
- * Flag that if true, will enable running garbage collection (GC) in a container. GC has mark phase and sweep phase.
283
- * In mark phase, unreferenced objects are identified and marked as such in the summary. This option enables the
284
- * mark phase.
325
+ * Flag that if true, will enable running garbage collection (GC) for a new container.
326
+ *
327
+ * GC has mark phase and sweep phase. In mark phase, unreferenced objects are identified
328
+ * and marked as such in the summary. This option enables the mark phase.
285
329
  * In sweep phase, unreferenced objects are eventually deleted from the container if they meet certain conditions.
286
330
  * Sweep phase can be enabled via the "sweepAllowed" option.
287
- * Note: This setting becomes part of the container's summary and cannot be changed.
331
+ *
332
+ * Note: This setting is persisted in the container's summary and cannot be changed.
288
333
  */
289
334
  gcAllowed?: boolean;
290
335
 
291
336
  /**
292
- * Flag that if true, enables GC's sweep phase which will eventually delete unreferenced objects from the container.
337
+ * Flag that if true, enables GC's sweep phase for a new container.
338
+ *
339
+ * This will allow GC to eventually delete unreferenced objects from the container.
293
340
  * This flag should only be set to true if "gcAllowed" is true.
294
- * Note: This setting becomes part of the container's summary and cannot be changed.
341
+ *
342
+ * Note: This setting is persisted in the container's summary and cannot be changed.
295
343
  */
296
344
  sweepAllowed?: boolean;
297
345
 
298
346
  /**
299
- * Flag that will disable garbage collection if set to true. Can be used to disable running GC on container where
300
- * is allowed via the gcAllowed option.
347
+ * Flag that if true, will disable garbage collection for the session.
348
+ * Can be used to disable running GC on containers where it is allowed via the gcAllowed option.
301
349
  */
302
350
  disableGC?: boolean;
303
351
 
@@ -307,6 +355,13 @@ export interface IGCRuntimeOptions {
307
355
  */
308
356
  runFullGC?: boolean;
309
357
 
358
+ /**
359
+ * Maximum session duration for a new container. If not present, a default value will be used.
360
+ *
361
+ * Note: This setting is persisted in the container's summary and cannot be changed.
362
+ */
363
+ sessionExpiryTimeoutMs?: number;
364
+
310
365
  /**
311
366
  * Allows additional GC options to be passed.
312
367
  */
@@ -318,39 +373,46 @@ export interface ISummaryRuntimeOptions {
318
373
  /** Override summary configurations set by the server. */
319
374
  summaryConfigOverrides?: ISummaryConfiguration;
320
375
 
321
- // Flag that disables putting channels in isolated subtrees for each data store
322
- // and the root node when generating a summary if set to true.
323
- // Defaults to FALSE (enabled) for now.
324
- disableIsolatedChannels?: boolean;
325
-
326
376
  /**
327
- * @deprecated - use `summaryConfigOverrides.initialSummarizerDelayMs` instead.
328
- * Delay before first attempt to spawn summarizing container.
329
- */
377
+ * Delay before first attempt to spawn summarizing container.
378
+ *
379
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
380
+ * {@link ISummaryBaseConfiguration.initialSummarizerDelayMs} instead.
381
+ */
330
382
  initialSummarizerDelayMs?: number;
331
383
 
332
384
  /**
333
- * @deprecated - use `summaryConfigOverrides.disableSummaries` instead.
334
385
  * Flag that disables summaries if it is set to true.
386
+ *
387
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
388
+ * {@link ISummaryConfigurationDisableSummarizer.state} instead.
335
389
  */
336
390
  disableSummaries?: boolean;
337
391
 
338
392
  /**
339
- * @deprecated - use `summaryConfigOverrides.maxOpsSinceLastSummary` instead.
340
- * Defaults to 7000 ops
393
+ * @defaultValue 7000 operations (ops)
394
+ *
395
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
396
+ * {@link ISummaryBaseConfiguration.maxOpsSinceLastSummary} instead.
341
397
  */
342
398
  maxOpsSinceLastSummary?: number;
343
399
 
344
400
  /**
345
- * @deprecated - use `summaryConfigOverrides.summarizerClientElection` instead.
346
- * Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
347
- * This defaults to false (disabled) and must be explicitly set to true to enable.
348
- */
401
+ * Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
402
+ *
403
+ * @defaultValue `false` (disabled) and must be explicitly set to true to enable.
404
+ *
405
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
406
+ * {@link ISummaryBaseConfiguration.summarizerClientElection} instead.
407
+ */
349
408
  summarizerClientElection?: boolean;
350
409
 
351
410
  /**
352
- * @deprecated - use `summaryConfigOverrides.state = "DisableHeuristics"` instead.
353
- * Options that control the running summarizer behavior. */
411
+ * Options that control the running summarizer behavior.
412
+ *
413
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
414
+ * `{@link ISummaryConfiguration.state} = "DisableHeuristics"` instead.
415
+ * */
354
416
  summarizerOptions?: Readonly<Partial<ISummarizerOptions>>;
355
417
  }
356
418
 
@@ -369,12 +431,6 @@ export interface IContainerRuntimeOptions {
369
431
  * 3. "bypass" will skip the check entirely. This is not recommended.
370
432
  */
371
433
  readonly loadSequenceNumberVerification?: "close" | "log" | "bypass";
372
- /**
373
- * Should the runtime use data store aliasing for creating root datastores.
374
- * In case of aliasing conflicts, the runtime will raise an exception which does
375
- * not effect the status of the container.
376
- */
377
- readonly useDataStoreAliasing?: boolean;
378
434
  /**
379
435
  * Sets the flush mode for the runtime. In Immediate flush mode the runtime will immediately
380
436
  * send all operations to the driver layer, while in TurnBased the operations will be buffered
@@ -388,10 +444,6 @@ export interface IContainerRuntimeOptions {
388
444
  readonly enableOfflineLoad?: boolean;
389
445
  }
390
446
 
391
- type IRuntimeMessageMetadata = undefined | {
392
- batch?: boolean;
393
- };
394
-
395
447
  /**
396
448
  * The summary tree returned by the root node. It adds state relevant to the root of the tree.
397
449
  */
@@ -431,11 +483,15 @@ interface OldContainerContextWithLogger extends Omit<IContainerContext, "taggedL
431
483
  * instantiated runtime in a new instance of the container, so it can load to the
432
484
  * same state
433
485
  */
434
- export interface IPendingRuntimeState {
486
+ interface IPendingRuntimeState {
435
487
  /**
436
488
  * Pending ops from PendingStateManager
437
489
  */
438
490
  pending?: IPendingLocalState;
491
+ /**
492
+ * Pending blobs from BlobManager
493
+ */
494
+ pendingAttachmentBlobs?: IPendingBlobs;
439
495
  /**
440
496
  * A base snapshot at a sequence number prior to the first pending op
441
497
  */
@@ -453,26 +509,13 @@ export interface IPendingRuntimeState {
453
509
  savedOps: ISequencedDocumentMessage[];
454
510
  }
455
511
 
456
- const useDataStoreAliasingKey = "Fluid.ContainerRuntime.UseDataStoreAliasing";
457
512
  const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
458
513
 
459
- // Feature gate for the max op size. If the value is negative, chunking is enabled
460
- // and all ops over 16k would be chunked. If the value is positive, all ops with
461
- // a size strictly larger will be rejected and the container closed with an error.
462
- const maxOpSizeInBytesKey = "Fluid.ContainerRuntime.MaxOpSizeInBytes";
463
-
464
- // By default, we should reject any op larger than 768KB,
465
- // in order to account for some extra overhead from serialization
466
- // to not reach the 1MB limits in socket.io and Kafka.
467
- const defaultMaxOpSizeInBytes = 768000;
468
-
469
- // By default, the size of the contents for the incoming ops is tracked.
470
- // However, in certain situations, this may incur a performance hit.
471
- // The feature-gate below can be used to disable this feature.
472
- const disableOpTrackingKey = "Fluid.ContainerRuntime.DisableOpTracking";
473
-
474
514
  const defaultFlushMode = FlushMode.TurnBased;
475
515
 
516
+ /**
517
+ * @deprecated - use ContainerRuntimeMessage instead
518
+ */
476
519
  export enum RuntimeMessage {
477
520
  FluidDataStoreOp = "component",
478
521
  Attach = "attach",
@@ -483,6 +526,9 @@ export enum RuntimeMessage {
483
526
  Operation = "op",
484
527
  }
485
528
 
529
+ /**
530
+ * @deprecated - please use version in driver-utils
531
+ */
486
532
  export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
487
533
  if ((Object.values(RuntimeMessage) as string[]).includes(message.type)) {
488
534
  return true;
@@ -490,6 +536,15 @@ export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
490
536
  return false;
491
537
  }
492
538
 
539
+ /**
540
+ * Unpacks runtime messages
541
+ *
542
+ * @remarks This API makes no promises regarding backward-compatability. This is internal API.
543
+ * @param message - message (as it observed in storage / service)
544
+ * @returns unpacked runtime message
545
+ *
546
+ * @internal
547
+ */
493
548
  export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
494
549
  if (message.type === MessageType.Operation) {
495
550
  // legacy op format?
@@ -502,287 +557,13 @@ export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
502
557
  message.type = innerContents.type;
503
558
  message.contents = innerContents.contents;
504
559
  }
505
- assert(isUnpackedRuntimeMessage(message), 0x122 /* "Message to unpack is not proper runtime message" */);
560
+ return true;
506
561
  } else {
507
562
  // Legacy format, but it's already "unpacked",
508
563
  // i.e. message.type is actually ContainerMessageType.
564
+ // Or it's non-runtime message.
509
565
  // Nothing to do in such case.
510
- }
511
- return message;
512
- }
513
-
514
- /**
515
- * This class controls pausing and resuming of inbound queue to ensure that we never
516
- * start processing ops in a batch IF we do not have all ops in the batch.
517
- */
518
- class ScheduleManagerCore {
519
- private pauseSequenceNumber: number | undefined;
520
- private currentBatchClientId: string | undefined;
521
- private localPaused = false;
522
- private timePaused = 0;
523
- private batchCount = 0;
524
-
525
- constructor(
526
- private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
527
- private readonly logger: ITelemetryLogger,
528
- ) {
529
- // Listen for delta manager sends and add batch metadata to messages
530
- this.deltaManager.on("prepareSend", (messages: IDocumentMessage[]) => {
531
- if (messages.length === 0) {
532
- return;
533
- }
534
-
535
- // First message will have the batch flag set to true if doing a batched send
536
- const firstMessageMetadata = messages[0].metadata as IRuntimeMessageMetadata;
537
- if (!firstMessageMetadata?.batch) {
538
- return;
539
- }
540
-
541
- // If the batch contains only a single op, clear the batch flag.
542
- if (messages.length === 1) {
543
- delete firstMessageMetadata.batch;
544
- return;
545
- }
546
-
547
- // Set the batch flag to false on the last message to indicate the end of the send batch
548
- const lastMessage = messages[messages.length - 1];
549
- lastMessage.metadata = { ...lastMessage.metadata, batch: false };
550
- });
551
-
552
- // Listen for updates and peek at the inbound
553
- this.deltaManager.inbound.on(
554
- "push",
555
- (message: ISequencedDocumentMessage) => {
556
- this.trackPending(message);
557
- });
558
-
559
- // Start with baseline - empty inbound queue.
560
- assert(!this.localPaused, 0x293 /* "initial state" */);
561
-
562
- const allPending = this.deltaManager.inbound.toArray();
563
- for (const pending of allPending) {
564
- this.trackPending(pending);
565
- }
566
-
567
- // We are intentionally directly listening to the "op" to inspect system ops as well.
568
- // If we do not observe system ops, we are likely to hit 0x296 assert when system ops
569
- // precedes start of incomplete batch.
570
- this.deltaManager.on("op", (message) => this.afterOpProcessing(message.sequenceNumber));
571
- }
572
-
573
- /**
574
- * The only public function in this class - called when we processed an op,
575
- * to make decision if op processing should be paused or not afer that.
576
- */
577
- public afterOpProcessing(sequenceNumber: number) {
578
- assert(!this.localPaused, 0x294 /* "can't have op processing paused if we are processing an op" */);
579
-
580
- // If the inbound queue is ever empty, nothing to do!
581
- if (this.deltaManager.inbound.length === 0) {
582
- assert(this.pauseSequenceNumber === undefined,
583
- 0x295 /* "there should be no pending batch if we have no ops" */);
584
- return;
585
- }
586
-
587
- // The queue is
588
- // 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
589
- // - here (processing ops until reaching start of incomplete batch)
590
- // - in trackPending(), when queue was empty and start of batch showed up.
591
- // 2. resumed when batch end comes in (in trackPending())
592
-
593
- // do we have incomplete batch to worry about?
594
- if (this.pauseSequenceNumber !== undefined) {
595
- assert(sequenceNumber < this.pauseSequenceNumber,
596
- 0x296 /* "we should never start processing incomplete batch!" */);
597
- // If the next op is the start of incomplete batch, then we can't process it until it's fully in - pause!
598
- if (sequenceNumber + 1 === this.pauseSequenceNumber) {
599
- this.pauseQueue();
600
- }
601
- }
602
- }
603
-
604
- private pauseQueue() {
605
- assert(!this.localPaused, 0x297 /* "always called from resumed state" */);
606
- this.localPaused = true;
607
- this.timePaused = performance.now();
608
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
609
- this.deltaManager.inbound.pause();
610
- }
611
-
612
- private resumeQueue(startBatch: number, messageEndBatch: ISequencedDocumentMessage) {
613
- const endBatch = messageEndBatch.sequenceNumber;
614
- const duration = this.localPaused ? (performance.now() - this.timePaused) : undefined;
615
-
616
- this.batchCount++;
617
- if (this.batchCount % 1000 === 1) {
618
- this.logger.sendTelemetryEvent({
619
- eventName: "BatchStats",
620
- sequenceNumber: endBatch,
621
- length: endBatch - startBatch + 1,
622
- msnDistance: endBatch - messageEndBatch.minimumSequenceNumber,
623
- duration,
624
- batchCount: this.batchCount,
625
- interrupted: this.localPaused,
626
- });
627
- }
628
-
629
- // Return early if no change in value
630
- if (!this.localPaused) {
631
- return;
632
- }
633
-
634
- this.localPaused = false;
635
-
636
- // Random round number - we want to know when batch waiting paused op processing.
637
- if (duration !== undefined && duration > latencyThreshold) {
638
- this.logger.sendErrorEvent({
639
- eventName: "MaxBatchWaitTimeExceeded",
640
- duration,
641
- sequenceNumber: endBatch,
642
- length: endBatch - startBatch,
643
- });
644
- }
645
- this.deltaManager.inbound.resume();
646
- }
647
-
648
- /**
649
- * Called for each incoming op (i.e. inbound "push" notification)
650
- */
651
- private trackPending(message: ISequencedDocumentMessage) {
652
- assert(this.deltaManager.inbound.length !== 0,
653
- 0x298 /* "we have something in the queue that generates this event" */);
654
-
655
- assert((this.currentBatchClientId === undefined) === (this.pauseSequenceNumber === undefined),
656
- 0x299 /* "non-synchronized state" */);
657
-
658
- const metadata = message.metadata as IRuntimeMessageMetadata;
659
- const batchMetadata = metadata?.batch;
660
-
661
- // Protocol messages are never part of a runtime batch of messages
662
- if (!isUnpackedRuntimeMessage(message)) {
663
- // Protocol messages should never show up in the middle of the batch!
664
- assert(this.currentBatchClientId === undefined, 0x29a /* "System message in the middle of batch!" */);
665
- assert(batchMetadata === undefined, 0x29b /* "system op in a batch?" */);
666
- assert(!this.localPaused, 0x29c /* "we should be processing ops when there is no active batch" */);
667
- return;
668
- }
669
-
670
- if (this.currentBatchClientId === undefined && batchMetadata === undefined) {
671
- assert(!this.localPaused, 0x29d /* "we should be processing ops when there is no active batch" */);
672
- return;
673
- }
674
-
675
- // If the client ID changes then we can move the pause point. If it stayed the same then we need to check.
676
- // If batchMetadata is not undefined then if it's true we've begun a new batch - if false we've ended
677
- // the previous one
678
- if (this.currentBatchClientId !== undefined || batchMetadata === false) {
679
- if (this.currentBatchClientId !== message.clientId) {
680
- // "Batch not closed, yet message from another client!"
681
- throw new DataCorruptionError(
682
- "OpBatchIncomplete",
683
- {
684
- runtimeVersion: pkgVersion,
685
- batchClientId: this.currentBatchClientId,
686
- ...extractSafePropertiesFromMessage(message),
687
- });
688
- }
689
- }
690
-
691
- // The queue is
692
- // 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
693
- // - in afterOpProcessing() - processing ops until reaching start of incomplete batch
694
- // - here (batchMetadata == false below), when queue was empty and start of batch showed up.
695
- // 2. resumed when batch end comes in (batchMetadata === true case below)
696
-
697
- if (batchMetadata) {
698
- assert(this.currentBatchClientId === undefined, 0x29e /* "there can't be active batch" */);
699
- assert(!this.localPaused, 0x29f /* "we should be processing ops when there is no active batch" */);
700
- this.pauseSequenceNumber = message.sequenceNumber;
701
- this.currentBatchClientId = message.clientId;
702
- // Start of the batch
703
- // Only pause processing if queue has no other ops!
704
- // If there are any other ops in the queue, processing will be stopped when they are processed!
705
- if (this.deltaManager.inbound.length === 1) {
706
- this.pauseQueue();
707
- }
708
- } else if (batchMetadata === false) {
709
- assert(this.pauseSequenceNumber !== undefined, 0x2a0 /* "batch presence was validated above" */);
710
- // Batch is complete, we can process it!
711
- this.resumeQueue(this.pauseSequenceNumber, message);
712
- this.pauseSequenceNumber = undefined;
713
- this.currentBatchClientId = undefined;
714
- } else {
715
- // Continuation of current batch. Do nothing
716
- assert(this.currentBatchClientId !== undefined, 0x2a1 /* "logic error" */);
717
- }
718
- }
719
- }
720
-
721
- /**
722
- * This class has the following responsibilities:
723
- * 1. It tracks batches as we process ops and raises "batchBegin" and "batchEnd" events.
724
- * As part of it, it validates batch correctness (i.e. no system ops in the middle of batch)
725
- * 2. It creates instance of ScheduleManagerCore that ensures we never start processing ops from batch
726
- * unless all ops of the batch are in.
727
- */
728
- export class ScheduleManager {
729
- private readonly deltaScheduler: DeltaScheduler;
730
- private batchClientId: string | undefined;
731
- private hitError = false;
732
-
733
- constructor(
734
- private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
735
- private readonly emitter: EventEmitter,
736
- private readonly logger: ITelemetryLogger,
737
- ) {
738
- this.deltaScheduler = new DeltaScheduler(
739
- this.deltaManager,
740
- ChildLogger.create(this.logger, "DeltaScheduler"),
741
- );
742
- void new ScheduleManagerCore(deltaManager, logger);
743
- }
744
-
745
- public beforeOpProcessing(message: ISequencedDocumentMessage) {
746
- if (this.batchClientId !== message.clientId) {
747
- assert(this.batchClientId === undefined,
748
- 0x2a2 /* "Batch is interrupted by other client op. Should be caught by trackPending()" */);
749
-
750
- // This could be the beginning of a new batch or an individual message.
751
- this.emitter.emit("batchBegin", message);
752
- this.deltaScheduler.batchBegin(message);
753
-
754
- const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
755
- if (batch) {
756
- this.batchClientId = message.clientId;
757
- } else {
758
- this.batchClientId = undefined;
759
- }
760
- }
761
- }
762
-
763
- public afterOpProcessing(error: any | undefined, message: ISequencedDocumentMessage) {
764
- // If this is no longer true, we need to revisit what we do where we set this.hitError.
765
- assert(!this.hitError, 0x2a3 /* "container should be closed on any error" */);
766
-
767
- if (error) {
768
- // We assume here that loader will close container and stop processing all future ops.
769
- // This is implicit dependency. If this flow changes, this code might no longer be correct.
770
- this.hitError = true;
771
- this.batchClientId = undefined;
772
- this.emitter.emit("batchEnd", error, message);
773
- this.deltaScheduler.batchEnd(message);
774
- return;
775
- }
776
-
777
- const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
778
- // If no batchClientId has been set then we're in an individual batch. Else, if we get
779
- // batch end metadata, this is end of the current batch.
780
- if (this.batchClientId === undefined || batch === false) {
781
- this.batchClientId = undefined;
782
- this.emitter.emit("batchEnd", undefined, message);
783
- this.deltaScheduler.batchEnd(message);
784
- return;
785
- }
566
+ return false;
786
567
  }
787
568
  }
788
569
 
@@ -852,7 +633,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
852
633
  summaryOptions = {},
853
634
  gcOptions = {},
854
635
  loadSequenceNumberVerification = "close",
855
- useDataStoreAliasing = false,
856
636
  flushMode = defaultFlushMode,
857
637
  enableOfflineLoad = false,
858
638
  } = runtimeOptions;
@@ -928,7 +708,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
928
708
  summaryOptions,
929
709
  gcOptions,
930
710
  loadSequenceNumberVerification,
931
- useDataStoreAliasing,
932
711
  flushMode,
933
712
  enableOfflineLoad,
934
713
  },
@@ -1018,15 +797,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1018
797
  private readonly summaryCollection: SummaryCollection;
1019
798
 
1020
799
  private readonly summarizerNode: IRootSummarizerNodeWithGC;
1021
- private readonly _aliasingEnabled: boolean;
1022
- private readonly _maxOpSizeInBytes: number;
1023
800
 
1024
801
  private readonly maxConsecutiveReconnects: number;
1025
- private readonly defaultMaxConsecutiveReconnects = 15;
802
+ private readonly defaultMaxConsecutiveReconnects = 7;
1026
803
 
1027
804
  private _orderSequentiallyCalls: number = 0;
1028
805
  private _flushMode: FlushMode;
1029
- private needsFlush = false;
1030
806
  private flushTrigger = false;
1031
807
 
1032
808
  private _connected: boolean;
@@ -1036,6 +812,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1036
812
 
1037
813
  private consecutiveReconnects = 0;
1038
814
 
815
+ /**
816
+ * Used to delay transition to "connected" state while we upload
817
+ * attachment blobs that were added while disconnected
818
+ */
819
+ private delayConnectClientId?: string;
820
+
1039
821
  public get connected(): boolean {
1040
822
  return this._connected;
1041
823
  }
@@ -1069,6 +851,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1069
851
  private readonly scheduleManager: ScheduleManager;
1070
852
  private readonly blobManager: BlobManager;
1071
853
  private readonly pendingStateManager: PendingStateManager;
854
+
855
+ // Provide lower soft limit - we want to have some number of ops to get efficiency in compression & bandwidth usage,
856
+ // but at the same time we want to send these ops sooner, to reduce overall latency of processing a batch.
857
+ // So there is some ballance here, that depends on compression algorithm and its efficiency working with smaller
858
+ // payloads. That number represents final (compressed) bits (once compression is implemented).
859
+ private readonly pendingAttachBatch = new BatchManager(64 * 1024);
860
+ private readonly pendingBatch = new BatchManager();
861
+
1072
862
  private readonly garbageCollector: IGarbageCollector;
1073
863
 
1074
864
  // Local copy of incomplete received chunks.
@@ -1076,15 +866,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1076
866
 
1077
867
  private readonly dataStores: DataStores;
1078
868
 
1079
- /**
1080
- * True if generating summaries with isolated channels is
1081
- * explicitly disabled. This only affects how summaries are written,
1082
- * and is the single source of truth for this container.
1083
- */
1084
- public readonly disableIsolatedChannels: boolean;
1085
869
  /** The last message processed at the time of the last summary. */
1086
870
  private messageAtLastSummary: ISummaryMetadataMessage | undefined;
1087
871
 
872
+ private get emptyBatch() {
873
+ return this.pendingBatch.empty && this.pendingAttachBatch.empty;
874
+ }
875
+
1088
876
  private get summarizer(): Summarizer {
1089
877
  assert(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
1090
878
  return this._summarizer;
@@ -1120,11 +908,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1120
908
  if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
1121
909
  return true;
1122
910
  }
1123
- if (this.summaryConfiguration.state !== "disabled") {
1124
- return this.summaryConfiguration.summarizerClientElection === true;
1125
- } else {
1126
- return false;
1127
- }
911
+ return this.summaryConfiguration.state !== "disabled"
912
+ ? this.summaryConfiguration.summarizerClientElection === true
913
+ : false;
1128
914
  }
1129
915
  private readonly maxOpsSinceLastSummary: number;
1130
916
  private getMaxOpsSinceLastSummary(): number {
@@ -1133,11 +919,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1133
919
  if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
1134
920
  return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
1135
921
  }
1136
- if (this.summaryConfiguration.state !== "disabled") {
1137
- return this.summaryConfiguration.maxOpsSinceLastSummary;
1138
- } else {
1139
- return 0;
1140
- }
922
+ return this.summaryConfiguration.state !== "disabled"
923
+ ? this.summaryConfiguration.maxOpsSinceLastSummary
924
+ : 0;
1141
925
  }
1142
926
 
1143
927
  private readonly initialSummarizerDelayMs: number;
@@ -1147,11 +931,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1147
931
  if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
1148
932
  return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
1149
933
  }
1150
- if (this.summaryConfiguration.state !== "disabled") {
1151
- return this.summaryConfiguration.initialSummarizerDelayMs;
1152
- } else {
1153
- return 0;
1154
- }
934
+ return this.summaryConfiguration.state !== "disabled"
935
+ ? this.summaryConfiguration.initialSummarizerDelayMs
936
+ : 0;
1155
937
  }
1156
938
 
1157
939
  private readonly createContainerMetadata: ICreateContainerMetadata;
@@ -1160,7 +942,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1160
942
  * a summary is generated.
1161
943
  */
1162
944
  private nextSummaryNumber: number;
1163
- private readonly opTracker: OpTracker;
1164
945
 
1165
946
  private constructor(
1166
947
  private readonly context: IContainerContext,
@@ -1186,9 +967,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1186
967
  super();
1187
968
  this.messageAtLastSummary = metadata?.message;
1188
969
 
1189
- // Default to false (enabled).
1190
- this.disableIsolatedChannels = this.runtimeOptions.summaryOptions.disableIsolatedChannels ?? false;
1191
-
1192
970
  this._connected = this.context.connected;
1193
971
  this.chunkMap = new Map<string, string[]>(chunks);
1194
972
 
@@ -1197,17 +975,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1197
975
  this.mc = loggerToMonitoringContext(
1198
976
  ChildLogger.create(this.logger, "ContainerRuntime"));
1199
977
 
978
+ if (this.summaryConfiguration.state === "enabled") {
979
+ this.validateSummaryHeuristicConfiguration(this.summaryConfiguration);
980
+ }
981
+
1200
982
  this.summariesDisabled = this.isSummariesDisabled();
1201
983
  this.heuristicsDisabled = this.isHeuristicsDisabled();
1202
984
  this.summarizerClientElectionEnabled = this.isSummarizerClientElectionEnabled();
1203
985
  this.maxOpsSinceLastSummary = this.getMaxOpsSinceLastSummary();
1204
986
  this.initialSummarizerDelayMs = this.getInitialSummarizerDelayMs();
1205
987
 
1206
- this._aliasingEnabled =
1207
- (this.mc.config.getBoolean(useDataStoreAliasingKey) ?? false) ||
1208
- (runtimeOptions.useDataStoreAliasing ?? false);
1209
-
1210
- this._maxOpSizeInBytes = (this.mc.config.getNumber(maxOpSizeInBytesKey) ?? defaultMaxOpSizeInBytes);
1211
988
  this.maxConsecutiveReconnects =
1212
989
  this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? this.defaultMaxConsecutiveReconnects;
1213
990
 
@@ -1227,6 +1004,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1227
1004
  getNodePackagePath: async (nodePath: string) => this.getGCNodePackagePath(nodePath),
1228
1005
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
1229
1006
  readAndParseBlob: async <T>(id: string) => readAndParse<T>(this.storage, id),
1007
+ getContainerDiagnosticId: () => this.context.id,
1008
+ activeConnection: () => this.deltaManager.active,
1230
1009
  });
1231
1010
 
1232
1011
  const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
@@ -1288,15 +1067,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1288
1067
  this.handleContext,
1289
1068
  blobManagerSnapshot,
1290
1069
  () => this.storage,
1291
- (blobId: string) => this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId }),
1070
+ (blobId, localId) => {
1071
+ if (!this.disposed) {
1072
+ this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId, localId });
1073
+ }
1074
+ },
1292
1075
  (blobPath: string) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"),
1293
1076
  this,
1294
- this.logger,
1077
+ pendingRuntimeState?.pendingAttachmentBlobs,
1295
1078
  );
1296
1079
 
1297
1080
  this.scheduleManager = new ScheduleManager(
1298
1081
  context.deltaManager,
1299
1082
  this,
1083
+ () => this.clientId,
1300
1084
  ChildLogger.create(this.logger, "ScheduleManager"),
1301
1085
  );
1302
1086
 
@@ -1311,7 +1095,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1311
1095
  flush: this.flush.bind(this),
1312
1096
  flushMode: () => this.flushMode,
1313
1097
  reSubmit: this.reSubmit.bind(this),
1314
- rollback: this.rollback.bind(this),
1315
1098
  setFlushMode: (mode) => this.setFlushMode(mode),
1316
1099
  },
1317
1100
  this._flushMode,
@@ -1442,9 +1225,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1442
1225
  createContainerRuntimeVersion: metadata?.createContainerRuntimeVersion,
1443
1226
  createContainerTimestamp: metadata?.createContainerTimestamp,
1444
1227
  };
1445
- // back-compat 0.59.3000 - Older document may either write summaryCount or not write it at all. If it does
1446
- // not write it, initialize summaryNumber to 0.
1447
- loadSummaryNumber = metadata?.summaryNumber ?? metadata?.summaryCount ?? 0;
1228
+ // summaryNumber was renamed from summaryCount. For older docs that haven't been opened for a long time,
1229
+ // the count is reset to 0.
1230
+ loadSummaryNumber = metadata?.summaryNumber ?? 0;
1448
1231
  } else {
1449
1232
  this.createContainerMetadata = {
1450
1233
  createContainerRuntimeVersion: pkgVersion,
@@ -1466,7 +1249,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1466
1249
 
1467
1250
  ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
1468
1251
  BindBatchTracker(this, this.logger);
1469
- this.opTracker = new OpTracker(this.deltaManager, this.mc.config.getBoolean(disableOpTrackingKey) === true);
1470
1252
  }
1471
1253
 
1472
1254
  public dispose(error?: Error): void {
@@ -1546,16 +1328,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1546
1328
  }
1547
1329
 
1548
1330
  if (id === BlobManager.basePath && requestParser.isLeaf(2)) {
1549
- const handle = await this.blobManager.getBlob(requestParser.pathParts[1]);
1550
- if (handle) {
1551
- return {
1331
+ const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
1332
+ return blob
1333
+ ? {
1552
1334
  status: 200,
1553
1335
  mimeType: "fluid/object",
1554
- value: handle.get(),
1555
- };
1556
- } else {
1557
- return create404Response(request);
1558
- }
1336
+ value: blob,
1337
+ } : create404Response(request);
1559
1338
  } else if (requestParser.pathParts.length > 0) {
1560
1339
  const dataStore = await this.getDataStoreFromRequest(id, request);
1561
1340
  const subRequest = requestParser.createSubRequest(1);
@@ -1573,7 +1352,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1573
1352
  }
1574
1353
 
1575
1354
  private internalId(maybeAlias: string): string {
1576
- return this.dataStores.aliases().get(maybeAlias) ?? maybeAlias;
1355
+ return this.dataStores.aliases.get(maybeAlias) ?? maybeAlias;
1577
1356
  }
1578
1357
 
1579
1358
  private async getDataStoreFromRequest(id: string, request: IRequest): Promise<IFluidRouter> {
@@ -1581,6 +1360,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1581
1360
  ? request.headers?.[RuntimeHeaders.wait]
1582
1361
  : true;
1583
1362
 
1363
+ await this.dataStores.waitIfPendingAlias(id);
1584
1364
  const internalId = this.internalId(id);
1585
1365
  const dataStoreContext = await this.dataStores.getDataStore(internalId, wait);
1586
1366
 
@@ -1620,12 +1400,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1620
1400
  private addMetadataToSummary(summaryTree: ISummaryTreeWithStats) {
1621
1401
  const metadata: IContainerRuntimeMetadata = {
1622
1402
  ...this.createContainerMetadata,
1623
- // back-compat 0.59.3000: This is renamed to summaryNumber. Can be removed when 0.59.3000 saturates.
1624
- summaryCount: this.nextSummaryNumber,
1625
1403
  // Increment the summary number for the next summary that will be generated.
1626
1404
  summaryNumber: this.nextSummaryNumber++,
1627
1405
  summaryFormatVersion: 1,
1628
- disableIsolatedChannels: this.disableIsolatedChannels || undefined,
1629
1406
  ...this.garbageCollector.getMetadata(),
1630
1407
  // The last message processed at the time of summary. If there are no new messages, use the message from the
1631
1408
  // last summary.
@@ -1647,7 +1424,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1647
1424
  addBlobToSummary(summaryTree, chunksBlobName, content);
1648
1425
  }
1649
1426
 
1650
- const dataStoreAliases = this.dataStores.aliases();
1427
+ const dataStoreAliases = this.dataStores.aliases;
1651
1428
  if (dataStoreAliases.size > 0) {
1652
1429
  addBlobToSummary(summaryTree, aliasBlobName, JSON.stringify([...dataStoreAliases]));
1653
1430
  }
@@ -1683,7 +1460,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1683
1460
  return true;
1684
1461
  }
1685
1462
 
1686
- if (!this.pendingStateManager.hasPendingMessages()) {
1463
+ if (!this.hasPendingMessages()) {
1687
1464
  // If there are no pending messages, we can always reconnect
1688
1465
  this.resetReconnectCount();
1689
1466
  return true;
@@ -1757,34 +1534,72 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1757
1534
  }
1758
1535
 
1759
1536
  public setConnectionState(connected: boolean, clientId?: string) {
1537
+ if (connected === false && this.delayConnectClientId !== undefined) {
1538
+ this.delayConnectClientId = undefined;
1539
+ this.mc.logger.sendTelemetryEvent({
1540
+ eventName: "UnsuccessfulConnectedTransition",
1541
+ });
1542
+ // Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
1543
+ return;
1544
+ }
1545
+
1546
+ // If attachment blobs were added while disconnected, we need to delay
1547
+ // propagation of the "connected" event until we have uploaded them to
1548
+ // ensure we don't submit ops referencing a blob that has not been uploaded
1549
+ const connecting = connected && !this._connected && !this.deltaManager.readOnlyInfo.readonly;
1550
+ if (connecting && this.blobManager.hasPendingOfflineUploads) {
1551
+ assert(!this.delayConnectClientId,
1552
+ 0x392 /* Connect event delay must be canceled before subsequent connect event */);
1553
+ assert(!!clientId, 0x393 /* Must have clientId when connecting */);
1554
+ this.delayConnectClientId = clientId;
1555
+ this.blobManager.onConnected().then(() => {
1556
+ // make sure we didn't reconnect before the promise resolved
1557
+ if (this.delayConnectClientId === clientId && !this.disposed) {
1558
+ this.delayConnectClientId = undefined;
1559
+ this.setConnectionStateCore(connected, clientId);
1560
+ }
1561
+ }, (error) => this.closeFn(error));
1562
+ return;
1563
+ }
1564
+
1565
+ this.setConnectionStateCore(connected, clientId);
1566
+ }
1567
+
1568
+ private setConnectionStateCore(connected: boolean, clientId?: string) {
1569
+ assert(!this.delayConnectClientId,
1570
+ 0x394 /* connect event delay must be cleared before propagating connect event */);
1760
1571
  this.verifyNotClosed();
1761
1572
 
1762
1573
  // There might be no change of state due to Container calling this API after loading runtime.
1763
1574
  const changeOfState = this._connected !== connected;
1764
- const reconnection = changeOfState && connected;
1575
+ const reconnection = changeOfState && !connected;
1765
1576
  this._connected = connected;
1766
1577
 
1767
1578
  if (!connected) {
1768
1579
  this._perfSignalData.signalsLost = 0;
1769
1580
  this._perfSignalData.signalTimestamp = 0;
1770
1581
  this._perfSignalData.trackingSignalSequenceNumber = undefined;
1582
+ } else {
1583
+ assert(this.attachState === AttachState.Attached,
1584
+ 0x3cd /* Connection is possible only if container exists in storage */);
1771
1585
  }
1772
1586
 
1587
+ // Fail while disconnected
1773
1588
  if (reconnection) {
1774
1589
  this.consecutiveReconnects++;
1775
1590
 
1776
1591
  if (!this.shouldContinueReconnecting()) {
1777
1592
  this.closeFn(
1778
- // pre-0.58 error message: MaxReconnectsWithNoProgress
1779
1593
  DataProcessingError.create(
1780
- "Runtime detected too many reconnects with no progress syncing local ops",
1594
+ // eslint-disable-next-line max-len
1595
+ "Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)",
1781
1596
  "setConnectionState",
1782
1597
  undefined,
1783
- {
1784
- dataLoss: 1,
1785
- attempts: this.consecutiveReconnects,
1786
- pendingMessages: this.pendingStateManager.pendingMessagesCount,
1787
- }));
1598
+ {
1599
+ dataLoss: 1,
1600
+ attempts: this.consecutiveReconnects,
1601
+ pendingMessages: this.pendingStateManager.pendingMessagesCount,
1602
+ }));
1788
1603
  return;
1789
1604
  }
1790
1605
  }
@@ -1794,6 +1609,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1794
1609
  }
1795
1610
 
1796
1611
  this.dataStores.setConnectionState(connected, clientId);
1612
+ this.garbageCollector.setConnectionState(connected, clientId);
1797
1613
 
1798
1614
  raiseConnectedEvent(this.mc.logger, this, connected, clientId);
1799
1615
  }
@@ -1801,49 +1617,50 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1801
1617
  public process(messageArg: ISequencedDocumentMessage, local: boolean) {
1802
1618
  this.verifyNotClosed();
1803
1619
 
1804
- // If it's not message for runtime, bail out right away.
1805
- if (!isUnpackedRuntimeMessage(messageArg)) {
1806
- return;
1807
- }
1808
-
1809
- if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
1810
- this.savedOps.push(messageArg);
1811
- }
1812
-
1813
1620
  // Do shallow copy of message, as methods below will modify it.
1814
1621
  // There might be multiple container instances receiving same message
1815
1622
  // We do not need to make deep copy, as each layer will just replace message.content itself,
1816
1623
  // but would not modify contents details
1817
1624
  let message = { ...messageArg };
1818
1625
 
1626
+ // back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
1627
+ // System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
1628
+ // Old ops may contain empty string (I assume noops).
1629
+ if (typeof message.contents === "string" && message.contents !== "") {
1630
+ message.contents = JSON.parse(message.contents);
1631
+ }
1632
+
1633
+ // Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
1634
+ // This format was not shipped to production workflows.
1635
+ const runtimeMessage = unpackRuntimeMessage(message);
1636
+
1637
+ if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
1638
+ this.savedOps.push(messageArg);
1639
+ }
1640
+
1819
1641
  // Surround the actual processing of the operation with messages to the schedule manager indicating
1820
1642
  // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
1821
1643
  // messages once a batch has been fully processed.
1822
1644
  this.scheduleManager.beforeOpProcessing(message);
1823
1645
 
1824
1646
  try {
1825
- message = unpackRuntimeMessage(message);
1826
-
1827
1647
  // Chunk processing must come first given that we will transform the message to the unchunked version
1828
1648
  // once all pieces are available
1829
1649
  message = this.processRemoteChunkedMessage(message);
1830
1650
 
1831
1651
  let localOpMetadata: unknown;
1832
- if (local) {
1833
- // Call the PendingStateManager to process local messages.
1834
- // Do not process local chunked ops until all pieces are available.
1835
- if (message.type !== ContainerMessageType.ChunkedOp) {
1836
- localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1837
- }
1652
+ if (local && runtimeMessage) {
1653
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1838
1654
  }
1839
1655
 
1840
1656
  // If there are no more pending messages after processing a local message,
1841
1657
  // the document is no longer dirty.
1842
- if (!this.pendingStateManager.hasPendingMessages()) {
1658
+ if (!this.hasPendingMessages()) {
1843
1659
  this.updateDocumentDirtyState(false);
1844
1660
  }
1845
1661
 
1846
- switch (message.type) {
1662
+ const type = message.type as ContainerMessageType;
1663
+ switch (type) {
1847
1664
  case ContainerMessageType.Attach:
1848
1665
  this.dataStores.processAttachMessage(message, local);
1849
1666
  break;
@@ -1854,13 +1671,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1854
1671
  this.dataStores.processFluidDataStoreOp(message, local, localOpMetadata);
1855
1672
  break;
1856
1673
  case ContainerMessageType.BlobAttach:
1857
- assert(message?.metadata?.blobId, 0x12a /* "Missing blob id on metadata" */);
1858
- this.blobManager.processBlobAttachOp(message.metadata.blobId, local);
1674
+ this.blobManager.processBlobAttachOp(message, local);
1675
+ break;
1676
+ case ContainerMessageType.ChunkedOp:
1677
+ case ContainerMessageType.Rejoin:
1859
1678
  break;
1860
1679
  default:
1680
+ assert(!runtimeMessage, 0x3ce /* Runtime message of unknown type */);
1681
+ }
1682
+
1683
+ // For back-compat, notify only about runtime messages for now.
1684
+ if (runtimeMessage) {
1685
+ this.emit("op", message, runtimeMessage);
1861
1686
  }
1862
1687
 
1863
- this.emit("op", message);
1864
1688
  this.scheduleManager.afterOpProcessing(undefined, message);
1865
1689
 
1866
1690
  if (local) {
@@ -1937,6 +1761,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1937
1761
  }
1938
1762
 
1939
1763
  public async getRootDataStore(id: string, wait = true): Promise<IFluidRouter> {
1764
+ return this.getRootDataStoreChannel(id, wait);
1765
+ }
1766
+
1767
+ private async getRootDataStoreChannel(id: string, wait = true): Promise<IFluidDataStoreChannel> {
1768
+ await this.dataStores.waitIfPendingAlias(id);
1940
1769
  const internalId = this.internalId(id);
1941
1770
  const context = await this.dataStores.getDataStore(internalId, wait);
1942
1771
  assert(await context.isRoot(), 0x12b /* "did not get root data store" */);
@@ -1969,30 +1798,76 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1969
1798
  assert(this._orderSequentiallyCalls === 0,
1970
1799
  0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
1971
1800
 
1972
- if (!this.deltaSender) {
1973
- return;
1974
- }
1801
+ this.flushBatch(this.pendingAttachBatch.popBatch());
1802
+ this.flushBatch(this.pendingBatch.popBatch());
1975
1803
 
1976
- // Let the PendingStateManager know that there was an attempt to flush messages.
1977
- // Note that this should happen before the `this.needsFlush` check below because in the scenario where we are
1978
- // not connected, `this.needsFlush` will be false but the PendingStateManager might have pending messages and
1979
- // hence needs to track this.
1980
- this.pendingStateManager.onFlush();
1804
+ assert(this.emptyBatch, 0x3cf /* reentrancy */);
1805
+ }
1981
1806
 
1982
- // If flush has already been called then exit early
1983
- if (!this.needsFlush) {
1984
- return;
1807
+ protected flushBatch(batch: BatchMessage[]): void {
1808
+ const length = batch.length;
1809
+
1810
+ if (length > 1) {
1811
+ batch[0].metadata = { ...batch[0].metadata, batch: true };
1812
+ batch[length - 1].metadata = { ...batch[length - 1].metadata, batch: false };
1813
+
1814
+ // This assert fires for the following reason (there might be more cases like that):
1815
+ // AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
1816
+ // i.e. in the middle of op processing!
1817
+ // Sending ops while processing ops is not good idea - it's not defined when
1818
+ // referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
1819
+ // If we send ops in response to processing multiple ops, then we for sure hit this assert!
1820
+ // Tracked via ADO #1834
1821
+ // assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
1822
+ // "Batch should be generated synchronously, without processing ops in the middle!");
1985
1823
  }
1986
1824
 
1987
- this.needsFlush = false;
1825
+ let clientSequenceNumber: number = -1;
1988
1826
 
1989
1827
  // Did we disconnect in the middle of turn-based batch?
1990
1828
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
1991
- if (!this.canSendOps()) {
1992
- return;
1829
+ if (this.canSendOps()) {
1830
+ if (this.context.submitBatchFn !== undefined) {
1831
+ const batchToSend: IBatchMessage[] = [];
1832
+ for (const message of batch) {
1833
+ batchToSend.push({ contents: message.contents, metadata: message.metadata });
1834
+ }
1835
+ // returns clientSequenceNumber of last message in a batch
1836
+ clientSequenceNumber = this.context.submitBatchFn(batchToSend);
1837
+ } else {
1838
+ // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
1839
+ // version that has support for batches (submitBatchFn)
1840
+ for (const message of batch) {
1841
+ clientSequenceNumber = this.context.submitFn(
1842
+ MessageType.Operation,
1843
+ message.deserializedContent,
1844
+ true, // batch
1845
+ message.metadata);
1846
+ }
1847
+
1848
+ this.deltaSender.flush();
1849
+ }
1850
+
1851
+ // Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
1852
+ clientSequenceNumber -= batch.length - 1;
1853
+ assert(clientSequenceNumber >= 0, 0x3d0 /* clientSequenceNumber can't be negative */);
1993
1854
  }
1994
1855
 
1995
- return this.deltaSender.flush();
1856
+ // Let the PendingStateManager know that a message was submitted.
1857
+ // In future, need to shift toward keeping batch as a whole!
1858
+ for (const message of batch) {
1859
+ this.pendingStateManager.onSubmitMessage(
1860
+ message.deserializedContent.type,
1861
+ clientSequenceNumber,
1862
+ message.referenceSequenceNumber,
1863
+ message.deserializedContent.contents,
1864
+ message.localOpMetadata,
1865
+ message.metadata,
1866
+ );
1867
+ clientSequenceNumber++;
1868
+ }
1869
+
1870
+ this.pendingStateManager.onFlush();
1996
1871
  }
1997
1872
 
1998
1873
  public orderSequentially(callback: () => void): void {
@@ -2018,9 +1893,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2018
1893
  }
2019
1894
 
2020
1895
  private trackOrderSequentiallyCalls(callback: () => void): void {
2021
- let checkpoint: { rollback: () => void; } | undefined;
1896
+ let checkpoint: { rollback: (action: (message: BatchMessage) => void) => void; } | undefined;
2022
1897
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
2023
- checkpoint = this.pendingStateManager.checkpoint();
1898
+ // Note: we are not touching this.pendingAttachBatch here, for two reasons:
1899
+ // 1. It would not help, as we flush attach ops as they become available.
1900
+ // 2. There is no way to undo process of data store creation.
1901
+ checkpoint = this.pendingBatch.checkpoint();
2024
1902
  }
2025
1903
 
2026
1904
  try {
@@ -2029,7 +1907,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2029
1907
  } catch (error) {
2030
1908
  if (checkpoint) {
2031
1909
  // This will throw and close the container if rollback fails
2032
- checkpoint.rollback();
1910
+ try {
1911
+ checkpoint.rollback((message: BatchMessage) =>
1912
+ this.rollback(
1913
+ message.deserializedContent.type,
1914
+ message.deserializedContent.contents,
1915
+ message.localOpMetadata));
1916
+ } catch (err) {
1917
+ const error2 = wrapError(err, (message) => {
1918
+ return DataProcessingError.create(
1919
+ `RollbackError: ${message}`,
1920
+ "checkpointRollback",
1921
+ undefined) as DataProcessingError;
1922
+ });
1923
+ this.closeFn(error2);
1924
+ throw error2;
1925
+ }
2033
1926
  } else {
2034
1927
  // pre-0.58 error message: orderSequentiallyCallbackException
2035
1928
  this.closeFn(new GenericError("orderSequentially callback exception", error));
@@ -2043,79 +1936,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2043
1936
  public async createDataStore(pkg: string | string[]): Promise<IDataStore> {
2044
1937
  const internalId = uuid();
2045
1938
  return channelToDataStore(
2046
- await this._createDataStore(pkg, false /* isRoot */, internalId),
1939
+ await this._createDataStore(pkg, internalId),
2047
1940
  internalId,
2048
1941
  this,
2049
1942
  this.dataStores,
2050
1943
  this.mc.logger);
2051
1944
  }
2052
1945
 
2053
- /**
2054
- * Creates a root datastore directly with a user generated id and attaches it to storage.
2055
- * It is vulnerable to name collisions and should not be used.
2056
- *
2057
- * This method will be removed. See #6465.
2058
- */
2059
- private async createRootDataStoreLegacy(pkg: string | string[], rootDataStoreId: string): Promise<IFluidRouter> {
2060
- const fluidDataStore = await this._createDataStore(pkg, true /* isRoot */, rootDataStoreId);
2061
- // back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel. For
2062
- // older versions, we still have to call bindToContext.
2063
- if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
2064
- fluidDataStore.makeVisibleAndAttachGraph();
2065
- } else {
2066
- fluidDataStore.bindToContext();
2067
- }
2068
- return fluidDataStore;
2069
- }
2070
-
2071
- /**
2072
- * @deprecated - will be removed in an upcoming release. See #9660.
2073
- */
2074
- public async createRootDataStore(pkg: string | string[], rootDataStoreId: string): Promise<IFluidRouter> {
2075
- if (rootDataStoreId.includes("/")) {
2076
- throw new UsageError(`Id cannot contain slashes: '${rootDataStoreId}'`);
2077
- }
2078
- return this._aliasingEnabled === true ?
2079
- this.createAndAliasDataStore(pkg, rootDataStoreId) :
2080
- this.createRootDataStoreLegacy(pkg, rootDataStoreId);
2081
- }
2082
-
2083
- /**
2084
- * Creates a data store then attempts to alias it.
2085
- * If aliasing fails, it will raise an exception.
2086
- *
2087
- * This method will be removed. See #6465.
2088
- *
2089
- * @param pkg - Package name of the data store
2090
- * @param alias - Alias to be assigned to the data store
2091
- * @param props - Properties for the data store
2092
- * @returns - An aliased data store which can can be found / loaded by alias.
2093
- */
2094
- private async createAndAliasDataStore(pkg: string | string[], alias: string, props?: any): Promise<IDataStore> {
2095
- const internalId = uuid();
2096
- const dataStore = await this._createDataStore(pkg, false /* isRoot */, internalId, props);
2097
- const aliasedDataStore = channelToDataStore(dataStore, internalId, this, this.dataStores, this.mc.logger);
2098
- const result = await aliasedDataStore.trySetAlias(alias);
2099
- if (result !== "Success") {
2100
- throw new GenericError(
2101
- "dataStoreAliasFailure",
2102
- undefined /* error */,
2103
- {
2104
- alias: {
2105
- value: alias,
2106
- tag: TelemetryDataTag.UserData,
2107
- },
2108
- internalId: {
2109
- value: internalId,
2110
- tag: TelemetryDataTag.PackageData,
2111
- },
2112
- aliasResult: result,
2113
- });
2114
- }
2115
-
2116
- return aliasedDataStore;
2117
- }
2118
-
2119
1946
  public createDetachedRootDataStore(
2120
1947
  pkg: Readonly<string[]>,
2121
1948
  rootDataStoreId: string): IFluidDataStoreContextDetached {
@@ -2129,55 +1956,23 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2129
1956
  return this.dataStores.createDetachedDataStoreCore(pkg, false);
2130
1957
  }
2131
1958
 
2132
- /**
2133
- * Creates a possibly root datastore directly with a possibly user generated id and attaches it to storage.
2134
- * It is vulnerable to name collisions if both aforementioned conditions are true, and should not be used.
2135
- *
2136
- * This method will be removed. See #6465.
2137
- */
2138
- private async _createDataStoreWithPropsLegacy(
1959
+ public async _createDataStoreWithProps(
2139
1960
  pkg: string | string[],
2140
1961
  props?: any,
2141
1962
  id = uuid(),
2142
- isRoot = false,
2143
1963
  ): Promise<IDataStore> {
2144
1964
  const fluidDataStore = await this.dataStores._createFluidDataStoreContext(
2145
- Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props).realize();
2146
- if (isRoot) {
2147
- // back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel.
2148
- // For older versions, we still have to call bindToContext.
2149
- if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
2150
- fluidDataStore.makeVisibleAndAttachGraph();
2151
- } else {
2152
- fluidDataStore.bindToContext();
2153
- }
2154
- this.logger.sendTelemetryEvent({
2155
- eventName: "Root datastore with props",
2156
- hasProps: props !== undefined,
2157
- });
2158
- }
1965
+ Array.isArray(pkg) ? pkg : [pkg], id, props).realize();
2159
1966
  return channelToDataStore(fluidDataStore, id, this, this.dataStores, this.mc.logger);
2160
1967
  }
2161
1968
 
2162
- public async _createDataStoreWithProps(
2163
- pkg: string | string[],
2164
- props?: any,
2165
- id = uuid(),
2166
- isRoot = false,
2167
- ): Promise<IDataStore> {
2168
- return this._aliasingEnabled === true && isRoot ?
2169
- this.createAndAliasDataStore(pkg, id, props) :
2170
- this._createDataStoreWithPropsLegacy(pkg, props, id, isRoot);
2171
- }
2172
-
2173
1969
  private async _createDataStore(
2174
1970
  pkg: string | string[],
2175
- isRoot: boolean,
2176
1971
  id = uuid(),
2177
1972
  props?: any,
2178
1973
  ): Promise<IFluidDataStoreChannel> {
2179
1974
  return this.dataStores
2180
- ._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props)
1975
+ ._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, props)
2181
1976
  .realize();
2182
1977
  }
2183
1978
 
@@ -2263,7 +2058,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2263
2058
  this.emit("attached");
2264
2059
  }
2265
2060
 
2266
- if (attachState === AttachState.Attached && !this.pendingStateManager.hasPendingMessages()) {
2061
+ if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
2267
2062
  this.updateDocumentDirtyState(false);
2268
2063
  }
2269
2064
  this.dataStores.setAttachState(attachState);
@@ -2283,10 +2078,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2283
2078
  }
2284
2079
 
2285
2080
  const summarizeResult = this.dataStores.createSummary(telemetryContext);
2286
- if (!this.disableIsolatedChannels) {
2287
- // Wrap data store summaries in .channels subtree.
2288
- wrapSummaryInChannelsTree(summarizeResult);
2289
- }
2081
+ // Wrap data store summaries in .channels subtree.
2082
+ wrapSummaryInChannelsTree(summarizeResult);
2083
+
2290
2084
  this.addContainerStateToSummary(
2291
2085
  summarizeResult,
2292
2086
  true /* fullTree */,
@@ -2312,13 +2106,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2312
2106
  telemetryContext?: ITelemetryContext,
2313
2107
  ): Promise<ISummarizeInternalResult> {
2314
2108
  const summarizeResult = await this.dataStores.summarize(fullTree, trackState, telemetryContext);
2315
- let pathPartsForChildren: string[] | undefined;
2316
2109
 
2317
- if (!this.disableIsolatedChannels) {
2318
- // Wrap data store summaries in .channels subtree.
2319
- wrapSummaryInChannelsTree(summarizeResult);
2320
- pathPartsForChildren = [channelsTreeName];
2321
- }
2110
+ // Wrap data store summaries in .channels subtree.
2111
+ wrapSummaryInChannelsTree(summarizeResult);
2112
+ const pathPartsForChildren = [channelsTreeName];
2113
+
2322
2114
  this.addContainerStateToSummary(summarizeResult, fullTree, trackState, telemetryContext);
2323
2115
  return {
2324
2116
  ...summarizeResult,
@@ -2488,7 +2280,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2488
2280
 
2489
2281
  /**
2490
2282
  * Runs garbage collection and updates the reference / used state of the nodes in the container.
2491
- * @returns the statistics of the garbage collection run.
2283
+ * @returns the statistics of the garbage collection run; undefined if GC did not run.
2492
2284
  */
2493
2285
  public async collectGarbage(
2494
2286
  options: {
@@ -2499,7 +2291,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2499
2291
  /** True to generate full GC data */
2500
2292
  fullGC?: boolean;
2501
2293
  },
2502
- ): Promise<IGCStats> {
2294
+ ): Promise<IGCStats | undefined> {
2503
2295
  return this.garbageCollector.collectGarbage(options);
2504
2296
  }
2505
2297
 
@@ -2534,6 +2326,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2534
2326
  },
2535
2327
  );
2536
2328
 
2329
+ assert(this.emptyBatch, 0x3d1 /* Can't trigger summary in the middle of a batch */);
2330
+
2537
2331
  let latestSnapshotVersionId: string | undefined;
2538
2332
  if (refreshLatestAck) {
2539
2333
  const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(
@@ -2563,15 +2357,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2563
2357
  const summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
2564
2358
  const minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
2565
2359
  const message = `Summary @${summaryRefSeqNum}:${this.deltaManager.minimumSequenceNumber}`;
2566
-
2567
- // We should be here is we haven't processed be here. If we are of if the last message's sequence number
2568
- // doesn't match the last processed sequence number, log an error.
2569
- if (summaryRefSeqNum !== this.deltaManager.lastMessage?.sequenceNumber) {
2570
- summaryNumberLogger.sendErrorEvent({
2571
- eventName: "LastSequenceMismatch",
2572
- error: message,
2573
- });
2574
- }
2360
+ const lastAck = this.summaryCollection.latestAck;
2575
2361
 
2576
2362
  this.summarizerNode.startSummary(summaryRefSeqNum, summaryNumberLogger);
2577
2363
 
@@ -2601,6 +2387,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2601
2387
  error: `lastSequenceNumber changed before uploading to storage. ${this.deltaManager.lastSequenceNumber} !== ${summaryRefSeqNum}`,
2602
2388
  };
2603
2389
  }
2390
+ assert(summaryRefSeqNum === this.deltaManager.lastMessage?.sequenceNumber,
2391
+ 0x395 /* it's one and the same thing */);
2392
+
2393
+ if (lastAck !== this.summaryCollection.latestAck) {
2394
+ return {
2395
+ continue: false,
2396
+ // eslint-disable-next-line max-len
2397
+ error: `Last summary changed while summarizing. ${this.summaryCollection.latestAck} !== ${lastAck}`,
2398
+ };
2399
+ }
2604
2400
  return { continue: true };
2605
2401
  };
2606
2402
 
@@ -2621,7 +2417,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2621
2417
  const forcedFullTree = this.garbageCollector.summaryStateNeedsReset;
2622
2418
  try {
2623
2419
  summarizeResult = await this.summarize({
2624
- fullTree: fullTree || forcedFullTree,
2420
+ fullTree: fullTree ?? forcedFullTree,
2625
2421
  trackState: true,
2626
2422
  summaryLogger: summaryNumberLogger,
2627
2423
  runGC: this.garbageCollector.shouldRunGC,
@@ -2642,7 +2438,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2642
2438
  // Counting dataStores and handles
2643
2439
  // Because handles are unchanged dataStores in the current logic,
2644
2440
  // summarized dataStore count is total dataStore count minus handle count
2645
- const dataStoreTree = this.disableIsolatedChannels ? summaryTree : summaryTree.tree[channelsTreeName];
2441
+ const dataStoreTree = summaryTree.tree[channelsTreeName];
2646
2442
 
2647
2443
  assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
2648
2444
  const handleCount = Object.values(dataStoreTree.tree).filter(
@@ -2657,8 +2453,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2657
2453
  gcStateUpdatedDataStoreCount: summarizeResult.gcStats?.updatedDataStoreCount,
2658
2454
  gcBlobNodeCount: gcSummaryTreeStats?.blobNodeCount,
2659
2455
  gcTotalBlobsSize: gcSummaryTreeStats?.totalBlobSize,
2660
- opsSizesSinceLastSummary: this.opTracker.opsSizeAccumulator,
2661
- nonSystemOpsSinceLastSummary: this.opTracker.nonSystemOpCount,
2662
2456
  summaryNumber,
2663
2457
  ...partialStats,
2664
2458
  };
@@ -2681,7 +2475,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2681
2475
  // submitting the summaryOp then we can't rely on summaryAck. So in case we have
2682
2476
  // latestSnapshotVersionId from storage and it does not match with the lastAck ackHandle, then use
2683
2477
  // the one fetched from storage as parent as that is the latest.
2684
- const lastAck = this.summaryCollection.latestAck;
2685
2478
  let summaryContext: ISummaryContext;
2686
2479
  if (lastAck?.summaryAck.contents.handle !== latestSnapshotVersionId
2687
2480
  && latestSnapshotVersionId !== undefined) {
@@ -2732,7 +2525,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2732
2525
 
2733
2526
  let clientSequenceNumber: number;
2734
2527
  try {
2735
- clientSequenceNumber = this.submitSystemMessage(MessageType.Summarize, summaryMessage);
2528
+ clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
2736
2529
  } catch (error) {
2737
2530
  return { stage: "upload", ...uploadData, error };
2738
2531
  }
@@ -2745,7 +2538,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2745
2538
  } as const;
2746
2539
 
2747
2540
  this.summarizerNode.completeSummary(handle);
2748
- this.opTracker.reset();
2749
2541
  return submitData;
2750
2542
  } finally {
2751
2543
  // Cleanup wip summary in case of failure
@@ -2792,7 +2584,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2792
2584
  }
2793
2585
  }
2794
2586
 
2587
+ private hasPendingMessages() {
2588
+ return this.pendingStateManager.hasPendingMessages() || !this.emptyBatch;
2589
+ }
2590
+
2795
2591
  private updateDocumentDirtyState(dirty: boolean) {
2592
+ if (this.attachState !== AttachState.Attached) {
2593
+ assert(dirty, 0x3d2 /* Non-attached container is dirty */);
2594
+ } else {
2595
+ // Other way is not true = see this.isContainerMessageDirtyable()
2596
+ assert(!dirty || this.hasPendingMessages(),
2597
+ 0x3d3 /* if doc is dirty, there has to be pending ops */);
2598
+ }
2599
+
2796
2600
  if (this.dirtyContainer === dirty) {
2797
2601
  return;
2798
2602
  }
@@ -2831,160 +2635,117 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2831
2635
 
2832
2636
  private submit(
2833
2637
  type: ContainerMessageType,
2834
- content: any,
2638
+ contents: any,
2835
2639
  localOpMetadata: unknown = undefined,
2836
- opMetadata: Record<string, unknown> | undefined = undefined,
2640
+ metadata: Record<string, unknown> | undefined = undefined,
2837
2641
  ): void {
2838
2642
  this.verifyNotClosed();
2839
2643
 
2840
2644
  // There should be no ops in detached container state!
2841
2645
  assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
2842
2646
 
2843
- let clientSequenceNumber: number = -1;
2844
- let opMetadataInternal = opMetadata;
2647
+ const deserializedContent: ContainerRuntimeMessage = { type, contents };
2648
+ const serializedContent = JSON.stringify(deserializedContent);
2845
2649
 
2846
- if (this.canSendOps()) {
2847
- const serializedContent = JSON.stringify(content);
2848
- const maxOpSize = this.context.deltaManager.maxMessageSize;
2849
-
2850
- // If in TurnBased flush mode we will trigger a flush at the next turn break
2851
- if (this.flushMode === FlushMode.TurnBased && !this.needsFlush) {
2852
- opMetadataInternal = {
2853
- ...opMetadata,
2854
- batch: true,
2855
- };
2856
- this.needsFlush = true;
2857
-
2858
- // Use Promise.resolve().then() to queue a microtask to detect the end of the turn and force a flush.
2859
- if (!this.flushTrigger) {
2860
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
2861
- Promise.resolve().then(() => {
2862
- this.flushTrigger = false;
2863
- this.flush();
2864
- });
2865
- }
2866
- }
2867
-
2868
- clientSequenceNumber = this.submitMaybeChunkedMessages(
2869
- type,
2870
- content,
2871
- serializedContent,
2872
- maxOpSize,
2873
- this._flushMode === FlushMode.TurnBased,
2874
- opMetadataInternal);
2650
+ if (this.deltaManager.readOnlyInfo.readonly) {
2651
+ this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
2875
2652
  }
2876
2653
 
2877
- // Let the PendingStateManager know that a message was submitted.
2878
- this.pendingStateManager.onSubmitMessage(
2879
- type,
2880
- clientSequenceNumber,
2881
- this.deltaManager.lastSequenceNumber,
2882
- content,
2654
+ const message: BatchMessage = {
2655
+ contents: serializedContent,
2656
+ deserializedContent,
2657
+ metadata,
2883
2658
  localOpMetadata,
2884
- opMetadataInternal,
2885
- );
2886
- if (this.isContainerMessageDirtyable(type, content)) {
2887
- this.updateDocumentDirtyState(true);
2888
- }
2889
- }
2659
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
2660
+ };
2890
2661
 
2891
- private submitMaybeChunkedMessages(
2892
- type: ContainerMessageType,
2893
- content: any,
2894
- serializedContent: string,
2895
- serverMaxOpSize: number,
2896
- batch: boolean,
2897
- opMetadataInternal: unknown = undefined,
2898
- ): number {
2899
- if (this._maxOpSizeInBytes >= 0) {
2900
- // Chunking disabled
2901
- if (!serializedContent || serializedContent.length <= this._maxOpSizeInBytes) {
2902
- return this.submitRuntimeMessage(type, content, batch, opMetadataInternal);
2662
+ try {
2663
+ // If this is attach message for new data store, and we are in a batch, send this op out of order
2664
+ // Is it safe:
2665
+ // Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
2666
+ // They become visible only when aliased, or handle to some sub-element of newly created datastore
2667
+ // is stored in some DDS, i.e. only after some other op.
2668
+ // Why:
2669
+ // Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
2670
+ // stores are created, causing issues like relay service throttling (too many ops) and catastrophic
2671
+ // failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
2672
+ // these issues.
2673
+ // Cons:
2674
+ // 1. With large batches, relay service may throttle clients. Clients may disconnect while throttled.
2675
+ // This change creates new possibility of a lot of newly created data stores never being referenced
2676
+ // because client died before it had a change to submit the rest of the ops. This will create more
2677
+ // garbage that needs to be collected leveraging GC (Garbage Collection) feature.
2678
+ // 2. Sending ops out of order means they are excluded from rollback functionality. This is not an issue
2679
+ // today as rollback can't undo creation of data store. To some extent not sending them is a bigger
2680
+ // issue than sending.
2681
+ // Please note that this does not change file format, so it can be disabled in the future if this
2682
+ // optimization no longer makes sense (for example, batch compression may make it less appealing).
2683
+ if (type === ContainerMessageType.Attach &&
2684
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
2685
+ if (!this.pendingAttachBatch.push(message)) {
2686
+ // BatchManager has two limits - soft limit & hard limit. Soft limit is only engaged
2687
+ // when queue is not empty.
2688
+ // Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
2689
+ this.flushBatch(this.pendingAttachBatch.popBatch());
2690
+ if (!this.pendingAttachBatch.push(message)) {
2691
+ throw new GenericError(
2692
+ "BatchTooLarge",
2693
+ /* error */ undefined,
2694
+ {
2695
+ opSize: message.contents.length,
2696
+ count: this.pendingAttachBatch.length,
2697
+ limit: this.pendingAttachBatch.limit,
2698
+ });
2699
+ }
2700
+ }
2701
+ } else {
2702
+ if (!this.pendingBatch.push(message)) {
2703
+ throw new GenericError(
2704
+ "BatchTooLarge",
2705
+ /* error */ undefined,
2706
+ {
2707
+ opSize: message.contents.length,
2708
+ count: this.pendingBatch.length,
2709
+ limit: this.pendingBatch.limit,
2710
+ });
2711
+ }
2903
2712
  }
2904
2713
 
2905
- // When chunking is disabled, we ignore the server max message size
2906
- // and if the content length is larger than the client configured message size
2907
- // instead of splitting the content, we will fail by explicitly close the container
2908
- this.closeFn(new GenericError(
2909
- "OpTooLarge",
2910
- /* error */ undefined,
2911
- {
2912
- length: {
2913
- value: serializedContent.length,
2914
- tag: TelemetryDataTag.PackageData,
2915
- },
2916
- limit: {
2917
- value: this._maxOpSizeInBytes,
2918
- tag: TelemetryDataTag.PackageData,
2919
- },
2920
- }));
2921
- return -1;
2922
- }
2923
-
2924
- // Chunking enabled, fallback on the server's max message size
2925
- // and split the content accordingly
2926
- if (!serializedContent || serializedContent.length <= serverMaxOpSize) {
2927
- return this.submitRuntimeMessage(type, content, batch, opMetadataInternal);
2714
+ if (this._flushMode !== FlushMode.TurnBased) {
2715
+ this.flush();
2716
+ } else if (!this.flushTrigger) {
2717
+ this.flushTrigger = true;
2718
+ // Queue a microtask to detect the end of the turn and force a flush.
2719
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
2720
+ Promise.resolve().then(() => {
2721
+ this.flushTrigger = false;
2722
+ this.flush();
2723
+ });
2724
+ }
2725
+ } catch (error) {
2726
+ this.closeFn(error as GenericError);
2727
+ throw error;
2928
2728
  }
2929
2729
 
2930
- return this.submitChunkedMessage(type, serializedContent, serverMaxOpSize);
2931
- }
2932
-
2933
- private submitChunkedMessage(type: ContainerMessageType, content: string, maxOpSize: number): number {
2934
- const contentLength = content.length;
2935
- const chunkN = Math.floor((contentLength - 1) / maxOpSize) + 1;
2936
- let offset = 0;
2937
- let clientSequenceNumber: number = 0;
2938
- for (let i = 1; i <= chunkN; i = i + 1) {
2939
- const chunkedOp: IChunkedOp = {
2940
- chunkId: i,
2941
- contents: content.substr(offset, maxOpSize),
2942
- originalType: type,
2943
- totalChunks: chunkN,
2944
- };
2945
- offset += maxOpSize;
2946
- clientSequenceNumber = this.submitRuntimeMessage(
2947
- ContainerMessageType.ChunkedOp,
2948
- chunkedOp,
2949
- false);
2730
+ if (this.isContainerMessageDirtyable(type, contents)) {
2731
+ this.updateDocumentDirtyState(true);
2950
2732
  }
2951
- return clientSequenceNumber;
2952
2733
  }
2953
2734
 
2954
- private submitSystemMessage(
2955
- type: MessageType,
2956
- contents: any) {
2735
+ private submitSummaryMessage(contents: ISummaryContent) {
2957
2736
  this.verifyNotClosed();
2958
2737
  assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
2959
2738
 
2960
2739
  // System message should not be sent in the middle of the batch.
2961
- // That said, we can preserve existing behavior by not flushing existing buffer.
2962
- // That might be not what caller hopes to get, but we can look deeper if telemetry tells us it's a problem.
2963
- const middleOfBatch = this.flushMode === FlushMode.TurnBased && this.needsFlush;
2964
- if (middleOfBatch) {
2965
- this.mc.logger.sendErrorEvent({ eventName: "submitSystemMessageError", type });
2966
- }
2967
-
2968
- return this.context.submitFn(
2969
- type,
2970
- contents,
2971
- middleOfBatch);
2972
- }
2973
-
2974
- private submitRuntimeMessage(
2975
- type: ContainerMessageType,
2976
- contents: any,
2977
- batch: boolean,
2978
- appData?: any,
2979
- ) {
2980
- this.verifyNotClosed();
2981
- assert(this.connected, 0x259 /* "Container disconnected when trying to submit system message" */);
2982
- const payload: ContainerRuntimeMessage = { type, contents };
2983
- return this.context.submitFn(
2984
- MessageType.Operation,
2985
- payload,
2986
- batch,
2987
- appData);
2740
+ assert(this.emptyBatch, 0x3d4 /* System op in the middle of a batch */);
2741
+
2742
+ // back-compat: ADO #1385: Make this call unconditional in the future
2743
+ return this.context.submitSummaryFn !== undefined
2744
+ ? this.context.submitSummaryFn(contents)
2745
+ : this.context.submitFn(
2746
+ MessageType.Summarize,
2747
+ contents,
2748
+ false);
2988
2749
  }
2989
2750
 
2990
2751
  /**
@@ -3022,7 +2783,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3022
2783
  case ContainerMessageType.ChunkedOp:
3023
2784
  throw new Error(`chunkedOp not expected here`);
3024
2785
  case ContainerMessageType.BlobAttach:
3025
- this.submit(type, content, localOpMetadata, opMetadata);
2786
+ this.blobManager.reSubmit(opMetadata);
3026
2787
  break;
3027
2788
  case ContainerMessageType.Rejoin:
3028
2789
  this.submit(type, content);
@@ -3056,20 +2817,24 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3056
2817
  summaryLogger: ITelemetryLogger,
3057
2818
  ) {
3058
2819
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
3059
- const { snapshotTree } = await this.fetchSnapshotFromStorage(
3060
- ackHandle,
3061
- summaryLogger,
3062
- {
3063
- eventName: "RefreshLatestSummaryGetSnapshot",
2820
+ // The call to fetch the snapshot is very expensive and not always needed.
2821
+ // It should only be done by the summarizerNode, if required.
2822
+ const snapshotTreeFetcher = async () => {
2823
+ const fetchResult = await this.fetchSnapshotFromStorage(
3064
2824
  ackHandle,
3065
- summaryRefSeq,
3066
- fetchLatest: false,
3067
- },
3068
- );
2825
+ summaryLogger,
2826
+ {
2827
+ eventName: "RefreshLatestSummaryGetSnapshot",
2828
+ ackHandle,
2829
+ summaryRefSeq,
2830
+ fetchLatest: false,
2831
+ });
2832
+ return fetchResult.snapshotTree;
2833
+ };
3069
2834
  const result = await this.summarizerNode.refreshLatestSummary(
3070
2835
  proposalHandle,
3071
2836
  summaryRefSeq,
3072
- async () => snapshotTree,
2837
+ snapshotTreeFetcher,
3073
2838
  readAndParseBlob,
3074
2839
  summaryLogger,
3075
2840
  );
@@ -3088,9 +2853,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3088
2853
  summaryLogger: ITelemetryLogger,
3089
2854
  ): Promise<{ latestSnapshotRefSeq: number; latestSnapshotVersionId: string | undefined; }> {
3090
2855
  const { snapshotTree, versionId } = await this.fetchSnapshotFromStorage(null, summaryLogger, {
3091
- eventName: "RefreshLatestSummaryGetSnapshot",
3092
- fetchLatest: true,
3093
- },
2856
+ eventName: "RefreshLatestSummaryGetSnapshot",
2857
+ fetchLatest: true,
2858
+ },
2859
+ FetchSource.noCache,
3094
2860
  );
3095
2861
 
3096
2862
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
@@ -3114,6 +2880,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3114
2880
  versionId: string | null,
3115
2881
  logger: ITelemetryLogger,
3116
2882
  event: ITelemetryGenericEvent,
2883
+ fetchSource?: FetchSource,
3117
2884
  ): Promise<{ snapshotTree: ISnapshotTree; versionId: string; }> {
3118
2885
  return PerformanceEvent.timedExecAsync(
3119
2886
  logger, event, async (perfEvent: {
@@ -3125,7 +2892,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3125
2892
  const stats: { getVersionDuration?: number; getSnapshotDuration?: number; } = {};
3126
2893
  const trace = Trace.start();
3127
2894
 
3128
- const versions = await this.storage.getVersions(versionId, 1);
2895
+ const versions = await this.storage.getVersions(
2896
+ versionId, 1, "refreshLatestSummaryAckFromServer", fetchSource);
3129
2897
  assert(!!versions && !!versions[0], 0x137 /* "Failed to get version from storage" */);
3130
2898
  stats.getVersionDuration = trace.trace().duration;
3131
2899
 
@@ -3153,15 +2921,21 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3153
2921
  this.baseSnapshotBlobs = await SerializedSnapshotStorage.serializeTree(this.context.baseSnapshot, this.storage);
3154
2922
  }
3155
2923
 
3156
- public getPendingLocalState(): IPendingRuntimeState {
2924
+ public getPendingLocalState(): unknown {
3157
2925
  if (!(this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad)) {
3158
2926
  throw new UsageError("can't get state when offline load disabled");
3159
2927
  }
3160
2928
 
2929
+ // Flush pending batch.
2930
+ // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
2931
+ // to close current batch.
2932
+ this.flush();
2933
+
3161
2934
  const previousPendingState = this.context.pendingLocalState as IPendingRuntimeState | undefined;
3162
2935
  if (previousPendingState) {
3163
2936
  return {
3164
2937
  pending: this.pendingStateManager.getLocalState(),
2938
+ pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
3165
2939
  snapshotBlobs: previousPendingState.snapshotBlobs,
3166
2940
  baseSnapshot: previousPendingState.baseSnapshot,
3167
2941
  savedOps: this.savedOps,
@@ -3171,6 +2945,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3171
2945
  assert(!!this.baseSnapshotBlobs, 0x2e7 /* "Must serialize base snapshot blobs before getting runtime state" */);
3172
2946
  return {
3173
2947
  pending: this.pendingStateManager.getLocalState(),
2948
+ pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
3174
2949
  snapshotBlobs: this.baseSnapshotBlobs,
3175
2950
  baseSnapshot: this.context.baseSnapshot,
3176
2951
  savedOps: this.savedOps,
@@ -3245,6 +3020,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3245
3020
  // we may not have seen every sequence number (because of system ops) so apply everything once we
3246
3021
  // don't have any more saved ops
3247
3022
  await this.pendingStateManager.applyStashedOpsAt();
3023
+
3024
+ // If it's not the case, we should take it into account when calculating dirty state.
3025
+ assert(this.context.attachState === AttachState.Attached,
3026
+ 0x3d5 /* this function is called for attached containers only */);
3027
+ if (!this.hasPendingMessages()) {
3028
+ this.updateDocumentDirtyState(false);
3029
+ }
3030
+ }
3031
+
3032
+ private validateSummaryHeuristicConfiguration(configuration: ISummaryConfigurationHeuristics) {
3033
+ // eslint-disable-next-line no-restricted-syntax
3034
+ for (const prop in configuration) {
3035
+ if (typeof configuration[prop] === "number" && configuration[prop] < 0) {
3036
+ throw new UsageError(`Summary heuristic configuration property "${prop}" cannot be less than 0`);
3037
+ }
3038
+ }
3248
3039
  }
3249
3040
  }
3250
3041