@fluidframework/container-runtime 1.2.7 → 2.0.0-dev.1.3.0.96595

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 (221) hide show
  1. package/.mocharc.js +12 -0
  2. package/dist/batchManager.d.ts +37 -0
  3. package/dist/batchManager.d.ts.map +1 -0
  4. package/dist/batchManager.js +73 -0
  5. package/dist/batchManager.js.map +1 -0
  6. package/dist/batchTracker.d.ts +1 -2
  7. package/dist/batchTracker.d.ts.map +1 -1
  8. package/dist/batchTracker.js +2 -3
  9. package/dist/batchTracker.js.map +1 -1
  10. package/dist/blobManager.d.ts +87 -25
  11. package/dist/blobManager.d.ts.map +1 -1
  12. package/dist/blobManager.js +317 -99
  13. package/dist/blobManager.js.map +1 -1
  14. package/dist/containerRuntime.d.ts +109 -124
  15. package/dist/containerRuntime.d.ts.map +1 -1
  16. package/dist/containerRuntime.js +349 -542
  17. package/dist/containerRuntime.js.map +1 -1
  18. package/dist/dataStore.js +29 -24
  19. package/dist/dataStore.js.map +1 -1
  20. package/dist/dataStoreContext.d.ts +20 -14
  21. package/dist/dataStoreContext.d.ts.map +1 -1
  22. package/dist/dataStoreContext.js +49 -58
  23. package/dist/dataStoreContext.js.map +1 -1
  24. package/dist/dataStores.d.ts +12 -5
  25. package/dist/dataStores.d.ts.map +1 -1
  26. package/dist/dataStores.js +21 -20
  27. package/dist/dataStores.js.map +1 -1
  28. package/dist/deltaScheduler.d.ts +6 -4
  29. package/dist/deltaScheduler.d.ts.map +1 -1
  30. package/dist/deltaScheduler.js +6 -4
  31. package/dist/deltaScheduler.js.map +1 -1
  32. package/dist/garbageCollection.d.ts +74 -14
  33. package/dist/garbageCollection.d.ts.map +1 -1
  34. package/dist/garbageCollection.js +249 -170
  35. package/dist/garbageCollection.js.map +1 -1
  36. package/dist/gcSweepReadyUsageDetection.d.ts +53 -0
  37. package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -0
  38. package/dist/gcSweepReadyUsageDetection.js +126 -0
  39. package/dist/gcSweepReadyUsageDetection.js.map +1 -0
  40. package/dist/index.d.ts +2 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -2
  43. package/dist/index.js.map +1 -1
  44. package/dist/opProperties.d.ts +7 -0
  45. package/dist/opProperties.d.ts.map +1 -0
  46. package/dist/opProperties.js +20 -0
  47. package/dist/opProperties.js.map +1 -0
  48. package/dist/orderedClientElection.d.ts +28 -10
  49. package/dist/orderedClientElection.d.ts.map +1 -1
  50. package/dist/orderedClientElection.js +14 -4
  51. package/dist/orderedClientElection.js.map +1 -1
  52. package/dist/packageVersion.d.ts +1 -1
  53. package/dist/packageVersion.d.ts.map +1 -1
  54. package/dist/packageVersion.js +1 -1
  55. package/dist/packageVersion.js.map +1 -1
  56. package/dist/pendingStateManager.d.ts +0 -11
  57. package/dist/pendingStateManager.d.ts.map +1 -1
  58. package/dist/pendingStateManager.js +24 -46
  59. package/dist/pendingStateManager.js.map +1 -1
  60. package/dist/runningSummarizer.d.ts +14 -4
  61. package/dist/runningSummarizer.d.ts.map +1 -1
  62. package/dist/runningSummarizer.js +68 -26
  63. package/dist/runningSummarizer.js.map +1 -1
  64. package/dist/scheduleManager.d.ts +31 -0
  65. package/dist/scheduleManager.d.ts.map +1 -0
  66. package/dist/scheduleManager.js +243 -0
  67. package/dist/scheduleManager.js.map +1 -0
  68. package/dist/summarizer.d.ts +0 -2
  69. package/dist/summarizer.d.ts.map +1 -1
  70. package/dist/summarizer.js +1 -12
  71. package/dist/summarizer.js.map +1 -1
  72. package/dist/summarizerHeuristics.d.ts +26 -4
  73. package/dist/summarizerHeuristics.d.ts.map +1 -1
  74. package/dist/summarizerHeuristics.js +95 -18
  75. package/dist/summarizerHeuristics.js.map +1 -1
  76. package/dist/summarizerTypes.d.ts +45 -18
  77. package/dist/summarizerTypes.d.ts.map +1 -1
  78. package/dist/summarizerTypes.js +1 -1
  79. package/dist/summarizerTypes.js.map +1 -1
  80. package/dist/summaryCollection.d.ts +1 -0
  81. package/dist/summaryCollection.d.ts.map +1 -1
  82. package/dist/summaryCollection.js +31 -15
  83. package/dist/summaryCollection.js.map +1 -1
  84. package/dist/summaryFormat.d.ts +0 -5
  85. package/dist/summaryFormat.d.ts.map +1 -1
  86. package/dist/summaryFormat.js.map +1 -1
  87. package/dist/summaryGenerator.d.ts +1 -0
  88. package/dist/summaryGenerator.d.ts.map +1 -1
  89. package/dist/summaryGenerator.js +11 -9
  90. package/dist/summaryGenerator.js.map +1 -1
  91. package/dist/summaryManager.d.ts +2 -2
  92. package/dist/summaryManager.d.ts.map +1 -1
  93. package/dist/summaryManager.js +22 -7
  94. package/dist/summaryManager.js.map +1 -1
  95. package/lib/batchManager.d.ts +37 -0
  96. package/lib/batchManager.d.ts.map +1 -0
  97. package/lib/batchManager.js +69 -0
  98. package/lib/batchManager.js.map +1 -0
  99. package/lib/batchTracker.d.ts +1 -2
  100. package/lib/batchTracker.d.ts.map +1 -1
  101. package/lib/batchTracker.js +2 -3
  102. package/lib/batchTracker.js.map +1 -1
  103. package/lib/blobManager.d.ts +87 -25
  104. package/lib/blobManager.d.ts.map +1 -1
  105. package/lib/blobManager.js +319 -101
  106. package/lib/blobManager.js.map +1 -1
  107. package/lib/containerRuntime.d.ts +109 -124
  108. package/lib/containerRuntime.d.ts.map +1 -1
  109. package/lib/containerRuntime.js +355 -547
  110. package/lib/containerRuntime.js.map +1 -1
  111. package/lib/dataStore.js +29 -24
  112. package/lib/dataStore.js.map +1 -1
  113. package/lib/dataStoreContext.d.ts +20 -14
  114. package/lib/dataStoreContext.d.ts.map +1 -1
  115. package/lib/dataStoreContext.js +46 -55
  116. package/lib/dataStoreContext.js.map +1 -1
  117. package/lib/dataStores.d.ts +12 -5
  118. package/lib/dataStores.d.ts.map +1 -1
  119. package/lib/dataStores.js +21 -20
  120. package/lib/dataStores.js.map +1 -1
  121. package/lib/deltaScheduler.d.ts +6 -4
  122. package/lib/deltaScheduler.d.ts.map +1 -1
  123. package/lib/deltaScheduler.js +6 -4
  124. package/lib/deltaScheduler.js.map +1 -1
  125. package/lib/garbageCollection.d.ts +74 -14
  126. package/lib/garbageCollection.d.ts.map +1 -1
  127. package/lib/garbageCollection.js +238 -160
  128. package/lib/garbageCollection.js.map +1 -1
  129. package/lib/gcSweepReadyUsageDetection.d.ts +53 -0
  130. package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -0
  131. package/lib/gcSweepReadyUsageDetection.js +121 -0
  132. package/lib/gcSweepReadyUsageDetection.js.map +1 -0
  133. package/lib/index.d.ts +2 -1
  134. package/lib/index.d.ts.map +1 -1
  135. package/lib/index.js +2 -1
  136. package/lib/index.js.map +1 -1
  137. package/lib/opProperties.d.ts +7 -0
  138. package/lib/opProperties.d.ts.map +1 -0
  139. package/lib/opProperties.js +16 -0
  140. package/lib/opProperties.js.map +1 -0
  141. package/lib/orderedClientElection.d.ts +28 -10
  142. package/lib/orderedClientElection.d.ts.map +1 -1
  143. package/lib/orderedClientElection.js +14 -4
  144. package/lib/orderedClientElection.js.map +1 -1
  145. package/lib/packageVersion.d.ts +1 -1
  146. package/lib/packageVersion.d.ts.map +1 -1
  147. package/lib/packageVersion.js +1 -1
  148. package/lib/packageVersion.js.map +1 -1
  149. package/lib/pendingStateManager.d.ts +0 -11
  150. package/lib/pendingStateManager.d.ts.map +1 -1
  151. package/lib/pendingStateManager.js +24 -46
  152. package/lib/pendingStateManager.js.map +1 -1
  153. package/lib/runningSummarizer.d.ts +14 -4
  154. package/lib/runningSummarizer.d.ts.map +1 -1
  155. package/lib/runningSummarizer.js +68 -26
  156. package/lib/runningSummarizer.js.map +1 -1
  157. package/lib/scheduleManager.d.ts +31 -0
  158. package/lib/scheduleManager.d.ts.map +1 -0
  159. package/lib/scheduleManager.js +239 -0
  160. package/lib/scheduleManager.js.map +1 -0
  161. package/lib/summarizer.d.ts +0 -2
  162. package/lib/summarizer.d.ts.map +1 -1
  163. package/lib/summarizer.js +1 -12
  164. package/lib/summarizer.js.map +1 -1
  165. package/lib/summarizerHeuristics.d.ts +26 -4
  166. package/lib/summarizerHeuristics.d.ts.map +1 -1
  167. package/lib/summarizerHeuristics.js +95 -18
  168. package/lib/summarizerHeuristics.js.map +1 -1
  169. package/lib/summarizerTypes.d.ts +45 -18
  170. package/lib/summarizerTypes.d.ts.map +1 -1
  171. package/lib/summarizerTypes.js +1 -1
  172. package/lib/summarizerTypes.js.map +1 -1
  173. package/lib/summaryCollection.d.ts +1 -0
  174. package/lib/summaryCollection.d.ts.map +1 -1
  175. package/lib/summaryCollection.js +31 -15
  176. package/lib/summaryCollection.js.map +1 -1
  177. package/lib/summaryFormat.d.ts +0 -5
  178. package/lib/summaryFormat.d.ts.map +1 -1
  179. package/lib/summaryFormat.js.map +1 -1
  180. package/lib/summaryGenerator.d.ts +1 -0
  181. package/lib/summaryGenerator.d.ts.map +1 -1
  182. package/lib/summaryGenerator.js +11 -9
  183. package/lib/summaryGenerator.js.map +1 -1
  184. package/lib/summaryManager.d.ts +2 -2
  185. package/lib/summaryManager.d.ts.map +1 -1
  186. package/lib/summaryManager.js +22 -7
  187. package/lib/summaryManager.js.map +1 -1
  188. package/package.json +65 -24
  189. package/src/batchManager.ts +91 -0
  190. package/src/batchTracker.ts +2 -3
  191. package/src/blobManager.ts +385 -118
  192. package/src/containerRuntime.ts +529 -740
  193. package/src/dataStore.ts +49 -37
  194. package/src/dataStoreContext.ts +44 -56
  195. package/src/dataStores.ts +34 -30
  196. package/src/deltaScheduler.ts +6 -4
  197. package/src/garbageCollection.ts +297 -206
  198. package/src/gcSweepReadyUsageDetection.ts +139 -0
  199. package/src/index.ts +1 -2
  200. package/src/opProperties.ts +19 -0
  201. package/src/orderedClientElection.ts +31 -10
  202. package/src/packageVersion.ts +1 -1
  203. package/src/pendingStateManager.ts +27 -59
  204. package/src/runningSummarizer.ts +75 -22
  205. package/src/scheduleManager.ts +314 -0
  206. package/src/summarizer.ts +1 -18
  207. package/src/summarizerHeuristics.ts +133 -19
  208. package/src/summarizerTypes.ts +53 -18
  209. package/src/summaryCollection.ts +33 -18
  210. package/src/summaryFormat.ts +0 -6
  211. package/src/summaryGenerator.ts +40 -22
  212. package/src/summaryManager.ts +22 -7
  213. package/dist/opTelemetry.d.ts +0 -22
  214. package/dist/opTelemetry.d.ts.map +0 -1
  215. package/dist/opTelemetry.js +0 -59
  216. package/dist/opTelemetry.js.map +0 -1
  217. package/lib/opTelemetry.d.ts +0 -22
  218. package/lib/opTelemetry.d.ts.map +0 -1
  219. package/lib/opTelemetry.js +0 -55
  220. package/lib/opTelemetry.js.map +0 -1
  221. 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
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,57 @@ export type ISummaryConfiguration =
260
297
  export const DefaultSummaryConfiguration: ISummaryConfiguration = {
261
298
  state: "enabled",
262
299
 
263
- idleTime: 5000 * 3,
300
+ idleTime: 15 * 1000, // 15 secs.
301
+
302
+ minIdleTime: 0,
264
303
 
265
- maxTime: 5000 * 12,
304
+ maxIdleTime: 30 * 1000, // 30 secs.
266
305
 
267
- maxOps: 100, // Summarize if 100 ops received since last snapshot.
306
+ maxTime: 60 * 1000, // 1 min.
307
+
308
+ maxOps: 100, // Summarize if 100 weighted ops received since last snapshot.
268
309
 
269
310
  minOpsForLastSummaryAttempt: 10,
270
311
 
271
- maxAckWaitTime: 6 * 10 * 1000, // 6 min.
312
+ maxAckWaitTime: 10 * 60 * 1000, // 10 mins.
272
313
 
273
314
  maxOpsSinceLastSummary: 7000,
274
315
 
275
- initialSummarizerDelayMs: 5000, // 5 secs.
316
+ initialSummarizerDelayMs: 5 * 1000, // 5 secs.
276
317
 
277
318
  summarizerClientElection: false,
319
+
320
+ nonRuntimeOpWeight: 0.1,
321
+
322
+ runtimeOpWeight: 1.0,
278
323
  };
279
324
 
280
325
  export interface IGCRuntimeOptions {
281
326
  /**
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.
327
+ * Flag that if true, will enable running garbage collection (GC) for a new container.
328
+ *
329
+ * GC has mark phase and sweep phase. In mark phase, unreferenced objects are identified
330
+ * and marked as such in the summary. This option enables the mark phase.
285
331
  * In sweep phase, unreferenced objects are eventually deleted from the container if they meet certain conditions.
286
332
  * Sweep phase can be enabled via the "sweepAllowed" option.
287
- * Note: This setting becomes part of the container's summary and cannot be changed.
333
+ *
334
+ * Note: This setting is persisted in the container's summary and cannot be changed.
288
335
  */
289
336
  gcAllowed?: boolean;
290
337
 
291
338
  /**
292
- * Flag that if true, enables GC's sweep phase which will eventually delete unreferenced objects from the container.
339
+ * Flag that if true, enables GC's sweep phase for a new container.
340
+ *
341
+ * This will allow GC to eventually delete unreferenced objects from the container.
293
342
  * 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.
343
+ *
344
+ * Note: This setting is persisted in the container's summary and cannot be changed.
295
345
  */
296
346
  sweepAllowed?: boolean;
297
347
 
298
348
  /**
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.
349
+ * Flag that if true, will disable garbage collection for the session.
350
+ * Can be used to disable running GC on containers where it is allowed via the gcAllowed option.
301
351
  */
302
352
  disableGC?: boolean;
303
353
 
@@ -307,6 +357,13 @@ export interface IGCRuntimeOptions {
307
357
  */
308
358
  runFullGC?: boolean;
309
359
 
360
+ /**
361
+ * Maximum session duration for a new container. If not present, a default value will be used.
362
+ *
363
+ * Note: This setting is persisted in the container's summary and cannot be changed.
364
+ */
365
+ sessionExpiryTimeoutMs?: number;
366
+
310
367
  /**
311
368
  * Allows additional GC options to be passed.
312
369
  */
@@ -318,39 +375,46 @@ export interface ISummaryRuntimeOptions {
318
375
  /** Override summary configurations set by the server. */
319
376
  summaryConfigOverrides?: ISummaryConfiguration;
320
377
 
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
378
  /**
327
- * @deprecated - use `summaryConfigOverrides.initialSummarizerDelayMs` instead.
328
- * Delay before first attempt to spawn summarizing container.
329
- */
379
+ * Delay before first attempt to spawn summarizing container.
380
+ *
381
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
382
+ * {@link ISummaryBaseConfiguration.initialSummarizerDelayMs} instead.
383
+ */
330
384
  initialSummarizerDelayMs?: number;
331
385
 
332
386
  /**
333
- * @deprecated - use `summaryConfigOverrides.disableSummaries` instead.
334
387
  * Flag that disables summaries if it is set to true.
388
+ *
389
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
390
+ * {@link ISummaryConfigurationDisableSummarizer.state} instead.
335
391
  */
336
392
  disableSummaries?: boolean;
337
393
 
338
394
  /**
339
- * @deprecated - use `summaryConfigOverrides.maxOpsSinceLastSummary` instead.
340
- * Defaults to 7000 ops
395
+ * @defaultValue 7000 operations (ops)
396
+ *
397
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
398
+ * {@link ISummaryBaseConfiguration.maxOpsSinceLastSummary} instead.
341
399
  */
342
400
  maxOpsSinceLastSummary?: number;
343
401
 
344
402
  /**
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
- */
403
+ * Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
404
+ *
405
+ * @defaultValue `false` (disabled) and must be explicitly set to true to enable.
406
+ *
407
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
408
+ * {@link ISummaryBaseConfiguration.summarizerClientElection} instead.
409
+ */
349
410
  summarizerClientElection?: boolean;
350
411
 
351
412
  /**
352
- * @deprecated - use `summaryConfigOverrides.state = "DisableHeuristics"` instead.
353
- * Options that control the running summarizer behavior. */
413
+ * Options that control the running summarizer behavior.
414
+ *
415
+ * @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
416
+ * `{@link ISummaryConfiguration.state} = "DisableHeuristics"` instead.
417
+ * */
354
418
  summarizerOptions?: Readonly<Partial<ISummarizerOptions>>;
355
419
  }
356
420
 
@@ -369,12 +433,6 @@ export interface IContainerRuntimeOptions {
369
433
  * 3. "bypass" will skip the check entirely. This is not recommended.
370
434
  */
371
435
  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
436
  /**
379
437
  * Sets the flush mode for the runtime. In Immediate flush mode the runtime will immediately
380
438
  * send all operations to the driver layer, while in TurnBased the operations will be buffered
@@ -388,10 +446,6 @@ export interface IContainerRuntimeOptions {
388
446
  readonly enableOfflineLoad?: boolean;
389
447
  }
390
448
 
391
- type IRuntimeMessageMetadata = undefined | {
392
- batch?: boolean;
393
- };
394
-
395
449
  /**
396
450
  * The summary tree returned by the root node. It adds state relevant to the root of the tree.
397
451
  */
@@ -431,11 +485,15 @@ interface OldContainerContextWithLogger extends Omit<IContainerContext, "taggedL
431
485
  * instantiated runtime in a new instance of the container, so it can load to the
432
486
  * same state
433
487
  */
434
- export interface IPendingRuntimeState {
488
+ interface IPendingRuntimeState {
435
489
  /**
436
490
  * Pending ops from PendingStateManager
437
491
  */
438
492
  pending?: IPendingLocalState;
493
+ /**
494
+ * Pending blobs from BlobManager
495
+ */
496
+ pendingAttachmentBlobs?: IPendingBlobs;
439
497
  /**
440
498
  * A base snapshot at a sequence number prior to the first pending op
441
499
  */
@@ -453,26 +511,13 @@ export interface IPendingRuntimeState {
453
511
  savedOps: ISequencedDocumentMessage[];
454
512
  }
455
513
 
456
- const useDataStoreAliasingKey = "Fluid.ContainerRuntime.UseDataStoreAliasing";
457
514
  const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
458
515
 
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
516
  const defaultFlushMode = FlushMode.TurnBased;
475
517
 
518
+ /**
519
+ * @deprecated - use ContainerRuntimeMessage instead
520
+ */
476
521
  export enum RuntimeMessage {
477
522
  FluidDataStoreOp = "component",
478
523
  Attach = "attach",
@@ -483,6 +528,9 @@ export enum RuntimeMessage {
483
528
  Operation = "op",
484
529
  }
485
530
 
531
+ /**
532
+ * @deprecated - please use version in driver-utils
533
+ */
486
534
  export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
487
535
  if ((Object.values(RuntimeMessage) as string[]).includes(message.type)) {
488
536
  return true;
@@ -490,6 +538,15 @@ export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
490
538
  return false;
491
539
  }
492
540
 
541
+ /**
542
+ * Unpacks runtime messages
543
+ *
544
+ * @remarks This API makes no promises regarding backward-compatability. This is internal API.
545
+ * @param message - message (as it observed in storage / service)
546
+ * @returns unpacked runtime message
547
+ *
548
+ * @internal
549
+ */
493
550
  export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
494
551
  if (message.type === MessageType.Operation) {
495
552
  // legacy op format?
@@ -502,287 +559,13 @@ export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
502
559
  message.type = innerContents.type;
503
560
  message.contents = innerContents.contents;
504
561
  }
505
- assert(isUnpackedRuntimeMessage(message), 0x122 /* "Message to unpack is not proper runtime message" */);
562
+ return true;
506
563
  } else {
507
564
  // Legacy format, but it's already "unpacked",
508
565
  // i.e. message.type is actually ContainerMessageType.
566
+ // Or it's non-runtime message.
509
567
  // 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
- }
568
+ return false;
786
569
  }
787
570
  }
788
571
 
@@ -852,7 +635,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
852
635
  summaryOptions = {},
853
636
  gcOptions = {},
854
637
  loadSequenceNumberVerification = "close",
855
- useDataStoreAliasing = false,
856
638
  flushMode = defaultFlushMode,
857
639
  enableOfflineLoad = false,
858
640
  } = runtimeOptions;
@@ -928,7 +710,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
928
710
  summaryOptions,
929
711
  gcOptions,
930
712
  loadSequenceNumberVerification,
931
- useDataStoreAliasing,
932
713
  flushMode,
933
714
  enableOfflineLoad,
934
715
  },
@@ -1018,15 +799,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1018
799
  private readonly summaryCollection: SummaryCollection;
1019
800
 
1020
801
  private readonly summarizerNode: IRootSummarizerNodeWithGC;
1021
- private readonly _aliasingEnabled: boolean;
1022
- private readonly _maxOpSizeInBytes: number;
1023
802
 
1024
803
  private readonly maxConsecutiveReconnects: number;
1025
- private readonly defaultMaxConsecutiveReconnects = 15;
804
+ private readonly defaultMaxConsecutiveReconnects = 7;
1026
805
 
1027
806
  private _orderSequentiallyCalls: number = 0;
1028
807
  private _flushMode: FlushMode;
1029
- private needsFlush = false;
1030
808
  private flushTrigger = false;
1031
809
 
1032
810
  private _connected: boolean;
@@ -1036,6 +814,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1036
814
 
1037
815
  private consecutiveReconnects = 0;
1038
816
 
817
+ /**
818
+ * Used to delay transition to "connected" state while we upload
819
+ * attachment blobs that were added while disconnected
820
+ */
821
+ private delayConnectClientId?: string;
822
+
1039
823
  public get connected(): boolean {
1040
824
  return this._connected;
1041
825
  }
@@ -1069,6 +853,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1069
853
  private readonly scheduleManager: ScheduleManager;
1070
854
  private readonly blobManager: BlobManager;
1071
855
  private readonly pendingStateManager: PendingStateManager;
856
+
857
+ // Provide lower soft limit - we want to have some number of ops to get efficiency in compression & bandwidth usage,
858
+ // but at the same time we want to send these ops sooner, to reduce overall latency of processing a batch.
859
+ // So there is some ballance here, that depends on compression algorithm and its efficiency working with smaller
860
+ // payloads. That number represents final (compressed) bits (once compression is implemented).
861
+ private readonly pendingAttachBatch = new BatchManager(64 * 1024);
862
+ private readonly pendingBatch = new BatchManager();
863
+
1072
864
  private readonly garbageCollector: IGarbageCollector;
1073
865
 
1074
866
  // Local copy of incomplete received chunks.
@@ -1076,15 +868,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1076
868
 
1077
869
  private readonly dataStores: DataStores;
1078
870
 
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
871
  /** The last message processed at the time of the last summary. */
1086
872
  private messageAtLastSummary: ISummaryMetadataMessage | undefined;
1087
873
 
874
+ private get emptyBatch() {
875
+ return this.pendingBatch.empty && this.pendingAttachBatch.empty;
876
+ }
877
+
1088
878
  private get summarizer(): Summarizer {
1089
879
  assert(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
1090
880
  return this._summarizer;
@@ -1120,11 +910,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1120
910
  if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
1121
911
  return true;
1122
912
  }
1123
- if (this.summaryConfiguration.state !== "disabled") {
1124
- return this.summaryConfiguration.summarizerClientElection === true;
1125
- } else {
1126
- return false;
1127
- }
913
+ return this.summaryConfiguration.state !== "disabled"
914
+ ? this.summaryConfiguration.summarizerClientElection === true
915
+ : false;
1128
916
  }
1129
917
  private readonly maxOpsSinceLastSummary: number;
1130
918
  private getMaxOpsSinceLastSummary(): number {
@@ -1133,11 +921,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1133
921
  if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
1134
922
  return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
1135
923
  }
1136
- if (this.summaryConfiguration.state !== "disabled") {
1137
- return this.summaryConfiguration.maxOpsSinceLastSummary;
1138
- } else {
1139
- return 0;
1140
- }
924
+ return this.summaryConfiguration.state !== "disabled"
925
+ ? this.summaryConfiguration.maxOpsSinceLastSummary
926
+ : 0;
1141
927
  }
1142
928
 
1143
929
  private readonly initialSummarizerDelayMs: number;
@@ -1147,11 +933,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1147
933
  if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
1148
934
  return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
1149
935
  }
1150
- if (this.summaryConfiguration.state !== "disabled") {
1151
- return this.summaryConfiguration.initialSummarizerDelayMs;
1152
- } else {
1153
- return 0;
1154
- }
936
+ return this.summaryConfiguration.state !== "disabled"
937
+ ? this.summaryConfiguration.initialSummarizerDelayMs
938
+ : 0;
1155
939
  }
1156
940
 
1157
941
  private readonly createContainerMetadata: ICreateContainerMetadata;
@@ -1160,7 +944,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1160
944
  * a summary is generated.
1161
945
  */
1162
946
  private nextSummaryNumber: number;
1163
- private readonly opTracker: OpTracker;
1164
947
 
1165
948
  private constructor(
1166
949
  private readonly context: IContainerContext,
@@ -1186,9 +969,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1186
969
  super();
1187
970
  this.messageAtLastSummary = metadata?.message;
1188
971
 
1189
- // Default to false (enabled).
1190
- this.disableIsolatedChannels = this.runtimeOptions.summaryOptions.disableIsolatedChannels ?? false;
1191
-
1192
972
  this._connected = this.context.connected;
1193
973
  this.chunkMap = new Map<string, string[]>(chunks);
1194
974
 
@@ -1197,17 +977,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1197
977
  this.mc = loggerToMonitoringContext(
1198
978
  ChildLogger.create(this.logger, "ContainerRuntime"));
1199
979
 
980
+ if (this.summaryConfiguration.state === "enabled") {
981
+ this.validateSummaryHeuristicConfiguration(this.summaryConfiguration);
982
+ }
983
+
1200
984
  this.summariesDisabled = this.isSummariesDisabled();
1201
985
  this.heuristicsDisabled = this.isHeuristicsDisabled();
1202
986
  this.summarizerClientElectionEnabled = this.isSummarizerClientElectionEnabled();
1203
987
  this.maxOpsSinceLastSummary = this.getMaxOpsSinceLastSummary();
1204
988
  this.initialSummarizerDelayMs = this.getInitialSummarizerDelayMs();
1205
989
 
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
990
  this.maxConsecutiveReconnects =
1212
991
  this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? this.defaultMaxConsecutiveReconnects;
1213
992
 
@@ -1227,6 +1006,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1227
1006
  getNodePackagePath: async (nodePath: string) => this.getGCNodePackagePath(nodePath),
1228
1007
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
1229
1008
  readAndParseBlob: async <T>(id: string) => readAndParse<T>(this.storage, id),
1009
+ getContainerDiagnosticId: () => this.context.id,
1010
+ activeConnection: () => this.deltaManager.active,
1230
1011
  });
1231
1012
 
1232
1013
  const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
@@ -1288,15 +1069,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1288
1069
  this.handleContext,
1289
1070
  blobManagerSnapshot,
1290
1071
  () => this.storage,
1291
- (blobId: string) => this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId }),
1072
+ (blobId, localId) => {
1073
+ if (!this.disposed) {
1074
+ this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId, localId });
1075
+ }
1076
+ },
1292
1077
  (blobPath: string) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"),
1293
1078
  this,
1294
- this.logger,
1079
+ pendingRuntimeState?.pendingAttachmentBlobs,
1295
1080
  );
1296
1081
 
1297
1082
  this.scheduleManager = new ScheduleManager(
1298
1083
  context.deltaManager,
1299
1084
  this,
1085
+ () => this.clientId,
1300
1086
  ChildLogger.create(this.logger, "ScheduleManager"),
1301
1087
  );
1302
1088
 
@@ -1311,7 +1097,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1311
1097
  flush: this.flush.bind(this),
1312
1098
  flushMode: () => this.flushMode,
1313
1099
  reSubmit: this.reSubmit.bind(this),
1314
- rollback: this.rollback.bind(this),
1315
1100
  setFlushMode: (mode) => this.setFlushMode(mode),
1316
1101
  },
1317
1102
  this._flushMode,
@@ -1442,9 +1227,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1442
1227
  createContainerRuntimeVersion: metadata?.createContainerRuntimeVersion,
1443
1228
  createContainerTimestamp: metadata?.createContainerTimestamp,
1444
1229
  };
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;
1230
+ // summaryNumber was renamed from summaryCount. For older docs that haven't been opened for a long time,
1231
+ // the count is reset to 0.
1232
+ loadSummaryNumber = metadata?.summaryNumber ?? 0;
1448
1233
  } else {
1449
1234
  this.createContainerMetadata = {
1450
1235
  createContainerRuntimeVersion: pkgVersion,
@@ -1466,7 +1251,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1466
1251
 
1467
1252
  ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
1468
1253
  BindBatchTracker(this, this.logger);
1469
- this.opTracker = new OpTracker(this.deltaManager, this.mc.config.getBoolean(disableOpTrackingKey) === true);
1470
1254
  }
1471
1255
 
1472
1256
  public dispose(error?: Error): void {
@@ -1546,16 +1330,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1546
1330
  }
1547
1331
 
1548
1332
  if (id === BlobManager.basePath && requestParser.isLeaf(2)) {
1549
- const handle = await this.blobManager.getBlob(requestParser.pathParts[1]);
1550
- if (handle) {
1551
- return {
1333
+ const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
1334
+ return blob
1335
+ ? {
1552
1336
  status: 200,
1553
1337
  mimeType: "fluid/object",
1554
- value: handle.get(),
1555
- };
1556
- } else {
1557
- return create404Response(request);
1558
- }
1338
+ value: blob,
1339
+ } : create404Response(request);
1559
1340
  } else if (requestParser.pathParts.length > 0) {
1560
1341
  const dataStore = await this.getDataStoreFromRequest(id, request);
1561
1342
  const subRequest = requestParser.createSubRequest(1);
@@ -1573,7 +1354,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1573
1354
  }
1574
1355
 
1575
1356
  private internalId(maybeAlias: string): string {
1576
- return this.dataStores.aliases().get(maybeAlias) ?? maybeAlias;
1357
+ return this.dataStores.aliases.get(maybeAlias) ?? maybeAlias;
1577
1358
  }
1578
1359
 
1579
1360
  private async getDataStoreFromRequest(id: string, request: IRequest): Promise<IFluidRouter> {
@@ -1581,6 +1362,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1581
1362
  ? request.headers?.[RuntimeHeaders.wait]
1582
1363
  : true;
1583
1364
 
1365
+ await this.dataStores.waitIfPendingAlias(id);
1584
1366
  const internalId = this.internalId(id);
1585
1367
  const dataStoreContext = await this.dataStores.getDataStore(internalId, wait);
1586
1368
 
@@ -1620,12 +1402,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1620
1402
  private addMetadataToSummary(summaryTree: ISummaryTreeWithStats) {
1621
1403
  const metadata: IContainerRuntimeMetadata = {
1622
1404
  ...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
1405
  // Increment the summary number for the next summary that will be generated.
1626
1406
  summaryNumber: this.nextSummaryNumber++,
1627
1407
  summaryFormatVersion: 1,
1628
- disableIsolatedChannels: this.disableIsolatedChannels || undefined,
1629
1408
  ...this.garbageCollector.getMetadata(),
1630
1409
  // The last message processed at the time of summary. If there are no new messages, use the message from the
1631
1410
  // last summary.
@@ -1647,7 +1426,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1647
1426
  addBlobToSummary(summaryTree, chunksBlobName, content);
1648
1427
  }
1649
1428
 
1650
- const dataStoreAliases = this.dataStores.aliases();
1429
+ const dataStoreAliases = this.dataStores.aliases;
1651
1430
  if (dataStoreAliases.size > 0) {
1652
1431
  addBlobToSummary(summaryTree, aliasBlobName, JSON.stringify([...dataStoreAliases]));
1653
1432
  }
@@ -1683,7 +1462,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1683
1462
  return true;
1684
1463
  }
1685
1464
 
1686
- if (!this.pendingStateManager.hasPendingMessages()) {
1465
+ if (!this.hasPendingMessages()) {
1687
1466
  // If there are no pending messages, we can always reconnect
1688
1467
  this.resetReconnectCount();
1689
1468
  return true;
@@ -1757,34 +1536,72 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1757
1536
  }
1758
1537
 
1759
1538
  public setConnectionState(connected: boolean, clientId?: string) {
1539
+ if (connected === false && this.delayConnectClientId !== undefined) {
1540
+ this.delayConnectClientId = undefined;
1541
+ this.mc.logger.sendTelemetryEvent({
1542
+ eventName: "UnsuccessfulConnectedTransition",
1543
+ });
1544
+ // Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
1545
+ return;
1546
+ }
1547
+
1548
+ // If attachment blobs were added while disconnected, we need to delay
1549
+ // propagation of the "connected" event until we have uploaded them to
1550
+ // ensure we don't submit ops referencing a blob that has not been uploaded
1551
+ const connecting = connected && !this._connected && !this.deltaManager.readOnlyInfo.readonly;
1552
+ if (connecting && this.blobManager.hasPendingOfflineUploads) {
1553
+ assert(!this.delayConnectClientId,
1554
+ 0x392 /* Connect event delay must be canceled before subsequent connect event */);
1555
+ assert(!!clientId, 0x393 /* Must have clientId when connecting */);
1556
+ this.delayConnectClientId = clientId;
1557
+ this.blobManager.onConnected().then(() => {
1558
+ // make sure we didn't reconnect before the promise resolved
1559
+ if (this.delayConnectClientId === clientId && !this.disposed) {
1560
+ this.delayConnectClientId = undefined;
1561
+ this.setConnectionStateCore(connected, clientId);
1562
+ }
1563
+ }, (error) => this.closeFn(error));
1564
+ return;
1565
+ }
1566
+
1567
+ this.setConnectionStateCore(connected, clientId);
1568
+ }
1569
+
1570
+ private setConnectionStateCore(connected: boolean, clientId?: string) {
1571
+ assert(!this.delayConnectClientId,
1572
+ 0x394 /* connect event delay must be cleared before propagating connect event */);
1760
1573
  this.verifyNotClosed();
1761
1574
 
1762
1575
  // There might be no change of state due to Container calling this API after loading runtime.
1763
1576
  const changeOfState = this._connected !== connected;
1764
- const reconnection = changeOfState && connected;
1577
+ const reconnection = changeOfState && !connected;
1765
1578
  this._connected = connected;
1766
1579
 
1767
1580
  if (!connected) {
1768
1581
  this._perfSignalData.signalsLost = 0;
1769
1582
  this._perfSignalData.signalTimestamp = 0;
1770
1583
  this._perfSignalData.trackingSignalSequenceNumber = undefined;
1584
+ } else {
1585
+ assert(this.attachState === AttachState.Attached,
1586
+ 0x3cd /* Connection is possible only if container exists in storage */);
1771
1587
  }
1772
1588
 
1589
+ // Fail while disconnected
1773
1590
  if (reconnection) {
1774
1591
  this.consecutiveReconnects++;
1775
1592
 
1776
1593
  if (!this.shouldContinueReconnecting()) {
1777
1594
  this.closeFn(
1778
- // pre-0.58 error message: MaxReconnectsWithNoProgress
1779
1595
  DataProcessingError.create(
1780
- "Runtime detected too many reconnects with no progress syncing local ops",
1596
+ // eslint-disable-next-line max-len
1597
+ "Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)",
1781
1598
  "setConnectionState",
1782
1599
  undefined,
1783
- {
1784
- dataLoss: 1,
1785
- attempts: this.consecutiveReconnects,
1786
- pendingMessages: this.pendingStateManager.pendingMessagesCount,
1787
- }));
1600
+ {
1601
+ dataLoss: 1,
1602
+ attempts: this.consecutiveReconnects,
1603
+ pendingMessages: this.pendingStateManager.pendingMessagesCount,
1604
+ }));
1788
1605
  return;
1789
1606
  }
1790
1607
  }
@@ -1794,6 +1611,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1794
1611
  }
1795
1612
 
1796
1613
  this.dataStores.setConnectionState(connected, clientId);
1614
+ this.garbageCollector.setConnectionState(connected, clientId);
1797
1615
 
1798
1616
  raiseConnectedEvent(this.mc.logger, this, connected, clientId);
1799
1617
  }
@@ -1801,49 +1619,50 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1801
1619
  public process(messageArg: ISequencedDocumentMessage, local: boolean) {
1802
1620
  this.verifyNotClosed();
1803
1621
 
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
1622
  // Do shallow copy of message, as methods below will modify it.
1814
1623
  // There might be multiple container instances receiving same message
1815
1624
  // We do not need to make deep copy, as each layer will just replace message.content itself,
1816
1625
  // but would not modify contents details
1817
1626
  let message = { ...messageArg };
1818
1627
 
1628
+ // back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
1629
+ // System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
1630
+ // Old ops may contain empty string (I assume noops).
1631
+ if (typeof message.contents === "string" && message.contents !== "") {
1632
+ message.contents = JSON.parse(message.contents);
1633
+ }
1634
+
1635
+ // Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
1636
+ // This format was not shipped to production workflows.
1637
+ const runtimeMessage = unpackRuntimeMessage(message);
1638
+
1639
+ if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
1640
+ this.savedOps.push(messageArg);
1641
+ }
1642
+
1819
1643
  // Surround the actual processing of the operation with messages to the schedule manager indicating
1820
1644
  // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
1821
1645
  // messages once a batch has been fully processed.
1822
1646
  this.scheduleManager.beforeOpProcessing(message);
1823
1647
 
1824
1648
  try {
1825
- message = unpackRuntimeMessage(message);
1826
-
1827
1649
  // Chunk processing must come first given that we will transform the message to the unchunked version
1828
1650
  // once all pieces are available
1829
1651
  message = this.processRemoteChunkedMessage(message);
1830
1652
 
1831
1653
  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
- }
1654
+ if (local && runtimeMessage) {
1655
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1838
1656
  }
1839
1657
 
1840
1658
  // If there are no more pending messages after processing a local message,
1841
1659
  // the document is no longer dirty.
1842
- if (!this.pendingStateManager.hasPendingMessages()) {
1660
+ if (!this.hasPendingMessages()) {
1843
1661
  this.updateDocumentDirtyState(false);
1844
1662
  }
1845
1663
 
1846
- switch (message.type) {
1664
+ const type = message.type as ContainerMessageType;
1665
+ switch (type) {
1847
1666
  case ContainerMessageType.Attach:
1848
1667
  this.dataStores.processAttachMessage(message, local);
1849
1668
  break;
@@ -1854,13 +1673,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1854
1673
  this.dataStores.processFluidDataStoreOp(message, local, localOpMetadata);
1855
1674
  break;
1856
1675
  case ContainerMessageType.BlobAttach:
1857
- assert(message?.metadata?.blobId, 0x12a /* "Missing blob id on metadata" */);
1858
- this.blobManager.processBlobAttachOp(message.metadata.blobId, local);
1676
+ this.blobManager.processBlobAttachOp(message, local);
1677
+ break;
1678
+ case ContainerMessageType.ChunkedOp:
1679
+ case ContainerMessageType.Rejoin:
1859
1680
  break;
1860
1681
  default:
1682
+ assert(!runtimeMessage, 0x3ce /* Runtime message of unknown type */);
1683
+ }
1684
+
1685
+ // For back-compat, notify only about runtime messages for now.
1686
+ if (runtimeMessage) {
1687
+ this.emit("op", message, runtimeMessage);
1861
1688
  }
1862
1689
 
1863
- this.emit("op", message);
1864
1690
  this.scheduleManager.afterOpProcessing(undefined, message);
1865
1691
 
1866
1692
  if (local) {
@@ -1937,6 +1763,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1937
1763
  }
1938
1764
 
1939
1765
  public async getRootDataStore(id: string, wait = true): Promise<IFluidRouter> {
1766
+ return this.getRootDataStoreChannel(id, wait);
1767
+ }
1768
+
1769
+ private async getRootDataStoreChannel(id: string, wait = true): Promise<IFluidDataStoreChannel> {
1770
+ await this.dataStores.waitIfPendingAlias(id);
1940
1771
  const internalId = this.internalId(id);
1941
1772
  const context = await this.dataStores.getDataStore(internalId, wait);
1942
1773
  assert(await context.isRoot(), 0x12b /* "did not get root data store" */);
@@ -1969,30 +1800,76 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1969
1800
  assert(this._orderSequentiallyCalls === 0,
1970
1801
  0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
1971
1802
 
1972
- if (!this.deltaSender) {
1973
- return;
1974
- }
1803
+ this.flushBatch(this.pendingAttachBatch.popBatch());
1804
+ this.flushBatch(this.pendingBatch.popBatch());
1975
1805
 
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();
1806
+ assert(this.emptyBatch, 0x3cf /* reentrancy */);
1807
+ }
1981
1808
 
1982
- // If flush has already been called then exit early
1983
- if (!this.needsFlush) {
1984
- return;
1809
+ protected flushBatch(batch: BatchMessage[]): void {
1810
+ const length = batch.length;
1811
+
1812
+ if (length > 1) {
1813
+ batch[0].metadata = { ...batch[0].metadata, batch: true };
1814
+ batch[length - 1].metadata = { ...batch[length - 1].metadata, batch: false };
1815
+
1816
+ // This assert fires for the following reason (there might be more cases like that):
1817
+ // AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
1818
+ // i.e. in the middle of op processing!
1819
+ // Sending ops while processing ops is not good idea - it's not defined when
1820
+ // referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
1821
+ // If we send ops in response to processing multiple ops, then we for sure hit this assert!
1822
+ // Tracked via ADO #1834
1823
+ // assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
1824
+ // "Batch should be generated synchronously, without processing ops in the middle!");
1985
1825
  }
1986
1826
 
1987
- this.needsFlush = false;
1827
+ let clientSequenceNumber: number = -1;
1988
1828
 
1989
1829
  // Did we disconnect in the middle of turn-based batch?
1990
1830
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
1991
- if (!this.canSendOps()) {
1992
- return;
1831
+ if (this.canSendOps()) {
1832
+ if (this.context.submitBatchFn !== undefined) {
1833
+ const batchToSend: IBatchMessage[] = [];
1834
+ for (const message of batch) {
1835
+ batchToSend.push({ contents: message.contents, metadata: message.metadata });
1836
+ }
1837
+ // returns clientSequenceNumber of last message in a batch
1838
+ clientSequenceNumber = this.context.submitBatchFn(batchToSend);
1839
+ } else {
1840
+ // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
1841
+ // version that has support for batches (submitBatchFn)
1842
+ for (const message of batch) {
1843
+ clientSequenceNumber = this.context.submitFn(
1844
+ MessageType.Operation,
1845
+ message.deserializedContent,
1846
+ true, // batch
1847
+ message.metadata);
1848
+ }
1849
+
1850
+ this.deltaSender.flush();
1851
+ }
1852
+
1853
+ // Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
1854
+ clientSequenceNumber -= batch.length - 1;
1855
+ assert(clientSequenceNumber >= 0, 0x3d0 /* clientSequenceNumber can't be negative */);
1993
1856
  }
1994
1857
 
1995
- return this.deltaSender.flush();
1858
+ // Let the PendingStateManager know that a message was submitted.
1859
+ // In future, need to shift toward keeping batch as a whole!
1860
+ for (const message of batch) {
1861
+ this.pendingStateManager.onSubmitMessage(
1862
+ message.deserializedContent.type,
1863
+ clientSequenceNumber,
1864
+ message.referenceSequenceNumber,
1865
+ message.deserializedContent.contents,
1866
+ message.localOpMetadata,
1867
+ message.metadata,
1868
+ );
1869
+ clientSequenceNumber++;
1870
+ }
1871
+
1872
+ this.pendingStateManager.onFlush();
1996
1873
  }
1997
1874
 
1998
1875
  public orderSequentially(callback: () => void): void {
@@ -2018,9 +1895,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2018
1895
  }
2019
1896
 
2020
1897
  private trackOrderSequentiallyCalls(callback: () => void): void {
2021
- let checkpoint: { rollback: () => void; } | undefined;
1898
+ let checkpoint: { rollback: (action: (message: BatchMessage) => void) => void; } | undefined;
2022
1899
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
2023
- checkpoint = this.pendingStateManager.checkpoint();
1900
+ // Note: we are not touching this.pendingAttachBatch here, for two reasons:
1901
+ // 1. It would not help, as we flush attach ops as they become available.
1902
+ // 2. There is no way to undo process of data store creation.
1903
+ checkpoint = this.pendingBatch.checkpoint();
2024
1904
  }
2025
1905
 
2026
1906
  try {
@@ -2029,7 +1909,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2029
1909
  } catch (error) {
2030
1910
  if (checkpoint) {
2031
1911
  // This will throw and close the container if rollback fails
2032
- checkpoint.rollback();
1912
+ try {
1913
+ checkpoint.rollback((message: BatchMessage) =>
1914
+ this.rollback(
1915
+ message.deserializedContent.type,
1916
+ message.deserializedContent.contents,
1917
+ message.localOpMetadata));
1918
+ } catch (err) {
1919
+ const error2 = wrapError(err, (message) => {
1920
+ return DataProcessingError.create(
1921
+ `RollbackError: ${message}`,
1922
+ "checkpointRollback",
1923
+ undefined) as DataProcessingError;
1924
+ });
1925
+ this.closeFn(error2);
1926
+ throw error2;
1927
+ }
2033
1928
  } else {
2034
1929
  // pre-0.58 error message: orderSequentiallyCallbackException
2035
1930
  this.closeFn(new GenericError("orderSequentially callback exception", error));
@@ -2043,79 +1938,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2043
1938
  public async createDataStore(pkg: string | string[]): Promise<IDataStore> {
2044
1939
  const internalId = uuid();
2045
1940
  return channelToDataStore(
2046
- await this._createDataStore(pkg, false /* isRoot */, internalId),
1941
+ await this._createDataStore(pkg, internalId),
2047
1942
  internalId,
2048
1943
  this,
2049
1944
  this.dataStores,
2050
1945
  this.mc.logger);
2051
1946
  }
2052
1947
 
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
1948
  public createDetachedRootDataStore(
2120
1949
  pkg: Readonly<string[]>,
2121
1950
  rootDataStoreId: string): IFluidDataStoreContextDetached {
@@ -2129,55 +1958,23 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2129
1958
  return this.dataStores.createDetachedDataStoreCore(pkg, false);
2130
1959
  }
2131
1960
 
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(
1961
+ public async _createDataStoreWithProps(
2139
1962
  pkg: string | string[],
2140
1963
  props?: any,
2141
1964
  id = uuid(),
2142
- isRoot = false,
2143
1965
  ): Promise<IDataStore> {
2144
1966
  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
- }
1967
+ Array.isArray(pkg) ? pkg : [pkg], id, props).realize();
2159
1968
  return channelToDataStore(fluidDataStore, id, this, this.dataStores, this.mc.logger);
2160
1969
  }
2161
1970
 
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
1971
  private async _createDataStore(
2174
1972
  pkg: string | string[],
2175
- isRoot: boolean,
2176
1973
  id = uuid(),
2177
1974
  props?: any,
2178
1975
  ): Promise<IFluidDataStoreChannel> {
2179
1976
  return this.dataStores
2180
- ._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props)
1977
+ ._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, props)
2181
1978
  .realize();
2182
1979
  }
2183
1980
 
@@ -2263,7 +2060,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2263
2060
  this.emit("attached");
2264
2061
  }
2265
2062
 
2266
- if (attachState === AttachState.Attached && !this.pendingStateManager.hasPendingMessages()) {
2063
+ if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
2267
2064
  this.updateDocumentDirtyState(false);
2268
2065
  }
2269
2066
  this.dataStores.setAttachState(attachState);
@@ -2283,10 +2080,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2283
2080
  }
2284
2081
 
2285
2082
  const summarizeResult = this.dataStores.createSummary(telemetryContext);
2286
- if (!this.disableIsolatedChannels) {
2287
- // Wrap data store summaries in .channels subtree.
2288
- wrapSummaryInChannelsTree(summarizeResult);
2289
- }
2083
+ // Wrap data store summaries in .channels subtree.
2084
+ wrapSummaryInChannelsTree(summarizeResult);
2085
+
2290
2086
  this.addContainerStateToSummary(
2291
2087
  summarizeResult,
2292
2088
  true /* fullTree */,
@@ -2312,13 +2108,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2312
2108
  telemetryContext?: ITelemetryContext,
2313
2109
  ): Promise<ISummarizeInternalResult> {
2314
2110
  const summarizeResult = await this.dataStores.summarize(fullTree, trackState, telemetryContext);
2315
- let pathPartsForChildren: string[] | undefined;
2316
2111
 
2317
- if (!this.disableIsolatedChannels) {
2318
- // Wrap data store summaries in .channels subtree.
2319
- wrapSummaryInChannelsTree(summarizeResult);
2320
- pathPartsForChildren = [channelsTreeName];
2321
- }
2112
+ // Wrap data store summaries in .channels subtree.
2113
+ wrapSummaryInChannelsTree(summarizeResult);
2114
+ const pathPartsForChildren = [channelsTreeName];
2115
+
2322
2116
  this.addContainerStateToSummary(summarizeResult, fullTree, trackState, telemetryContext);
2323
2117
  return {
2324
2118
  ...summarizeResult,
@@ -2488,7 +2282,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2488
2282
 
2489
2283
  /**
2490
2284
  * Runs garbage collection and updates the reference / used state of the nodes in the container.
2491
- * @returns the statistics of the garbage collection run.
2285
+ * @returns the statistics of the garbage collection run; undefined if GC did not run.
2492
2286
  */
2493
2287
  public async collectGarbage(
2494
2288
  options: {
@@ -2499,7 +2293,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2499
2293
  /** True to generate full GC data */
2500
2294
  fullGC?: boolean;
2501
2295
  },
2502
- ): Promise<IGCStats> {
2296
+ ): Promise<IGCStats | undefined> {
2503
2297
  return this.garbageCollector.collectGarbage(options);
2504
2298
  }
2505
2299
 
@@ -2534,6 +2328,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2534
2328
  },
2535
2329
  );
2536
2330
 
2331
+ assert(this.emptyBatch, 0x3d1 /* Can't trigger summary in the middle of a batch */);
2332
+
2537
2333
  let latestSnapshotVersionId: string | undefined;
2538
2334
  if (refreshLatestAck) {
2539
2335
  const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(
@@ -2563,15 +2359,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2563
2359
  const summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
2564
2360
  const minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
2565
2361
  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
- }
2362
+ const lastAck = this.summaryCollection.latestAck;
2575
2363
 
2576
2364
  this.summarizerNode.startSummary(summaryRefSeqNum, summaryNumberLogger);
2577
2365
 
@@ -2601,6 +2389,16 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2601
2389
  error: `lastSequenceNumber changed before uploading to storage. ${this.deltaManager.lastSequenceNumber} !== ${summaryRefSeqNum}`,
2602
2390
  };
2603
2391
  }
2392
+ assert(summaryRefSeqNum === this.deltaManager.lastMessage?.sequenceNumber,
2393
+ 0x395 /* it's one and the same thing */);
2394
+
2395
+ if (lastAck !== this.summaryCollection.latestAck) {
2396
+ return {
2397
+ continue: false,
2398
+ // eslint-disable-next-line max-len
2399
+ error: `Last summary changed while summarizing. ${this.summaryCollection.latestAck} !== ${lastAck}`,
2400
+ };
2401
+ }
2604
2402
  return { continue: true };
2605
2403
  };
2606
2404
 
@@ -2621,7 +2419,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2621
2419
  const forcedFullTree = this.garbageCollector.summaryStateNeedsReset;
2622
2420
  try {
2623
2421
  summarizeResult = await this.summarize({
2624
- fullTree: fullTree || forcedFullTree,
2422
+ fullTree: fullTree ?? forcedFullTree,
2625
2423
  trackState: true,
2626
2424
  summaryLogger: summaryNumberLogger,
2627
2425
  runGC: this.garbageCollector.shouldRunGC,
@@ -2642,7 +2440,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2642
2440
  // Counting dataStores and handles
2643
2441
  // Because handles are unchanged dataStores in the current logic,
2644
2442
  // summarized dataStore count is total dataStore count minus handle count
2645
- const dataStoreTree = this.disableIsolatedChannels ? summaryTree : summaryTree.tree[channelsTreeName];
2443
+ const dataStoreTree = summaryTree.tree[channelsTreeName];
2646
2444
 
2647
2445
  assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
2648
2446
  const handleCount = Object.values(dataStoreTree.tree).filter(
@@ -2657,8 +2455,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2657
2455
  gcStateUpdatedDataStoreCount: summarizeResult.gcStats?.updatedDataStoreCount,
2658
2456
  gcBlobNodeCount: gcSummaryTreeStats?.blobNodeCount,
2659
2457
  gcTotalBlobsSize: gcSummaryTreeStats?.totalBlobSize,
2660
- opsSizesSinceLastSummary: this.opTracker.opsSizeAccumulator,
2661
- nonSystemOpsSinceLastSummary: this.opTracker.nonSystemOpCount,
2662
2458
  summaryNumber,
2663
2459
  ...partialStats,
2664
2460
  };
@@ -2681,7 +2477,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2681
2477
  // submitting the summaryOp then we can't rely on summaryAck. So in case we have
2682
2478
  // latestSnapshotVersionId from storage and it does not match with the lastAck ackHandle, then use
2683
2479
  // the one fetched from storage as parent as that is the latest.
2684
- const lastAck = this.summaryCollection.latestAck;
2685
2480
  let summaryContext: ISummaryContext;
2686
2481
  if (lastAck?.summaryAck.contents.handle !== latestSnapshotVersionId
2687
2482
  && latestSnapshotVersionId !== undefined) {
@@ -2732,7 +2527,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2732
2527
 
2733
2528
  let clientSequenceNumber: number;
2734
2529
  try {
2735
- clientSequenceNumber = this.submitSystemMessage(MessageType.Summarize, summaryMessage);
2530
+ clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
2736
2531
  } catch (error) {
2737
2532
  return { stage: "upload", ...uploadData, error };
2738
2533
  }
@@ -2745,7 +2540,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2745
2540
  } as const;
2746
2541
 
2747
2542
  this.summarizerNode.completeSummary(handle);
2748
- this.opTracker.reset();
2749
2543
  return submitData;
2750
2544
  } finally {
2751
2545
  // Cleanup wip summary in case of failure
@@ -2792,7 +2586,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2792
2586
  }
2793
2587
  }
2794
2588
 
2589
+ private hasPendingMessages() {
2590
+ return this.pendingStateManager.hasPendingMessages() || !this.emptyBatch;
2591
+ }
2592
+
2795
2593
  private updateDocumentDirtyState(dirty: boolean) {
2594
+ if (this.attachState !== AttachState.Attached) {
2595
+ assert(dirty, 0x3d2 /* Non-attached container is dirty */);
2596
+ } else {
2597
+ // Other way is not true = see this.isContainerMessageDirtyable()
2598
+ assert(!dirty || this.hasPendingMessages(),
2599
+ 0x3d3 /* if doc is dirty, there has to be pending ops */);
2600
+ }
2601
+
2796
2602
  if (this.dirtyContainer === dirty) {
2797
2603
  return;
2798
2604
  }
@@ -2831,160 +2637,117 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2831
2637
 
2832
2638
  private submit(
2833
2639
  type: ContainerMessageType,
2834
- content: any,
2640
+ contents: any,
2835
2641
  localOpMetadata: unknown = undefined,
2836
- opMetadata: Record<string, unknown> | undefined = undefined,
2642
+ metadata: Record<string, unknown> | undefined = undefined,
2837
2643
  ): void {
2838
2644
  this.verifyNotClosed();
2839
2645
 
2840
2646
  // There should be no ops in detached container state!
2841
2647
  assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
2842
2648
 
2843
- let clientSequenceNumber: number = -1;
2844
- let opMetadataInternal = opMetadata;
2649
+ const deserializedContent: ContainerRuntimeMessage = { type, contents };
2650
+ const serializedContent = JSON.stringify(deserializedContent);
2845
2651
 
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);
2652
+ if (this.deltaManager.readOnlyInfo.readonly) {
2653
+ this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
2875
2654
  }
2876
2655
 
2877
- // Let the PendingStateManager know that a message was submitted.
2878
- this.pendingStateManager.onSubmitMessage(
2879
- type,
2880
- clientSequenceNumber,
2881
- this.deltaManager.lastSequenceNumber,
2882
- content,
2656
+ const message: BatchMessage = {
2657
+ contents: serializedContent,
2658
+ deserializedContent,
2659
+ metadata,
2883
2660
  localOpMetadata,
2884
- opMetadataInternal,
2885
- );
2886
- if (this.isContainerMessageDirtyable(type, content)) {
2887
- this.updateDocumentDirtyState(true);
2888
- }
2889
- }
2661
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
2662
+ };
2890
2663
 
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);
2664
+ try {
2665
+ // If this is attach message for new data store, and we are in a batch, send this op out of order
2666
+ // Is it safe:
2667
+ // Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
2668
+ // They become visible only when aliased, or handle to some sub-element of newly created datastore
2669
+ // is stored in some DDS, i.e. only after some other op.
2670
+ // Why:
2671
+ // Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
2672
+ // stores are created, causing issues like relay service throttling (too many ops) and catastrophic
2673
+ // failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
2674
+ // these issues.
2675
+ // Cons:
2676
+ // 1. With large batches, relay service may throttle clients. Clients may disconnect while throttled.
2677
+ // This change creates new possibility of a lot of newly created data stores never being referenced
2678
+ // because client died before it had a change to submit the rest of the ops. This will create more
2679
+ // garbage that needs to be collected leveraging GC (Garbage Collection) feature.
2680
+ // 2. Sending ops out of order means they are excluded from rollback functionality. This is not an issue
2681
+ // today as rollback can't undo creation of data store. To some extent not sending them is a bigger
2682
+ // issue than sending.
2683
+ // Please note that this does not change file format, so it can be disabled in the future if this
2684
+ // optimization no longer makes sense (for example, batch compression may make it less appealing).
2685
+ if (type === ContainerMessageType.Attach &&
2686
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
2687
+ if (!this.pendingAttachBatch.push(message)) {
2688
+ // BatchManager has two limits - soft limit & hard limit. Soft limit is only engaged
2689
+ // when queue is not empty.
2690
+ // Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
2691
+ this.flushBatch(this.pendingAttachBatch.popBatch());
2692
+ if (!this.pendingAttachBatch.push(message)) {
2693
+ throw new GenericError(
2694
+ "BatchTooLarge",
2695
+ /* error */ undefined,
2696
+ {
2697
+ opSize: message.contents.length,
2698
+ count: this.pendingAttachBatch.length,
2699
+ limit: this.pendingAttachBatch.limit,
2700
+ });
2701
+ }
2702
+ }
2703
+ } else {
2704
+ if (!this.pendingBatch.push(message)) {
2705
+ throw new GenericError(
2706
+ "BatchTooLarge",
2707
+ /* error */ undefined,
2708
+ {
2709
+ opSize: message.contents.length,
2710
+ count: this.pendingBatch.length,
2711
+ limit: this.pendingBatch.limit,
2712
+ });
2713
+ }
2903
2714
  }
2904
2715
 
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);
2716
+ if (this._flushMode !== FlushMode.TurnBased) {
2717
+ this.flush();
2718
+ } else if (!this.flushTrigger) {
2719
+ this.flushTrigger = true;
2720
+ // Queue a microtask to detect the end of the turn and force a flush.
2721
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
2722
+ Promise.resolve().then(() => {
2723
+ this.flushTrigger = false;
2724
+ this.flush();
2725
+ });
2726
+ }
2727
+ } catch (error) {
2728
+ this.closeFn(error as GenericError);
2729
+ throw error;
2928
2730
  }
2929
2731
 
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);
2732
+ if (this.isContainerMessageDirtyable(type, contents)) {
2733
+ this.updateDocumentDirtyState(true);
2950
2734
  }
2951
- return clientSequenceNumber;
2952
2735
  }
2953
2736
 
2954
- private submitSystemMessage(
2955
- type: MessageType,
2956
- contents: any) {
2737
+ private submitSummaryMessage(contents: ISummaryContent) {
2957
2738
  this.verifyNotClosed();
2958
2739
  assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
2959
2740
 
2960
2741
  // 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);
2742
+ assert(this.emptyBatch, 0x3d4 /* System op in the middle of a batch */);
2743
+
2744
+ // back-compat: ADO #1385: Make this call unconditional in the future
2745
+ return this.context.submitSummaryFn !== undefined
2746
+ ? this.context.submitSummaryFn(contents)
2747
+ : this.context.submitFn(
2748
+ MessageType.Summarize,
2749
+ contents,
2750
+ false);
2988
2751
  }
2989
2752
 
2990
2753
  /**
@@ -3022,7 +2785,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3022
2785
  case ContainerMessageType.ChunkedOp:
3023
2786
  throw new Error(`chunkedOp not expected here`);
3024
2787
  case ContainerMessageType.BlobAttach:
3025
- this.submit(type, content, localOpMetadata, opMetadata);
2788
+ this.blobManager.reSubmit(opMetadata);
3026
2789
  break;
3027
2790
  case ContainerMessageType.Rejoin:
3028
2791
  this.submit(type, content);
@@ -3060,25 +2823,25 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3060
2823
  // It should only be done by the summarizerNode, if required.
3061
2824
  const snapshotTreeFetcher = async () => {
3062
2825
  const fetchResult = await this.fetchSnapshotFromStorage(
3063
- ackHandle,
3064
- summaryLogger,
3065
- {
3066
- eventName: "RefreshLatestSummaryGetSnapshot",
3067
- ackHandle,
3068
- summaryRefSeq,
3069
- fetchLatest: false,
3070
- });
3071
- return fetchResult.snapshotTree;
3072
- };
3073
- const result = await this.summarizerNode.refreshLatestSummary(
3074
- proposalHandle,
3075
- summaryRefSeq,
3076
- snapshotTreeFetcher,
3077
- readAndParseBlob,
3078
- summaryLogger,
3079
- );
3080
-
3081
- // Notify the garbage collector so it can update its latest summary state.
2826
+ ackHandle,
2827
+ summaryLogger,
2828
+ {
2829
+ eventName: "RefreshLatestSummaryGetSnapshot",
2830
+ ackHandle,
2831
+ summaryRefSeq,
2832
+ fetchLatest: false,
2833
+ });
2834
+ return fetchResult.snapshotTree;
2835
+ };
2836
+ const result = await this.summarizerNode.refreshLatestSummary(
2837
+ proposalHandle,
2838
+ summaryRefSeq,
2839
+ snapshotTreeFetcher,
2840
+ readAndParseBlob,
2841
+ summaryLogger,
2842
+ );
2843
+
2844
+ // Notify the garbage collector so it can update its latest summary state.
3082
2845
  await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
3083
2846
  }
3084
2847
 
@@ -3092,9 +2855,10 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3092
2855
  summaryLogger: ITelemetryLogger,
3093
2856
  ): Promise<{ latestSnapshotRefSeq: number; latestSnapshotVersionId: string | undefined; }> {
3094
2857
  const { snapshotTree, versionId } = await this.fetchSnapshotFromStorage(null, summaryLogger, {
3095
- eventName: "RefreshLatestSummaryGetSnapshot",
3096
- fetchLatest: true,
3097
- },
2858
+ eventName: "RefreshLatestSummaryGetSnapshot",
2859
+ fetchLatest: true,
2860
+ },
2861
+ FetchSource.noCache,
3098
2862
  );
3099
2863
 
3100
2864
  const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
@@ -3118,6 +2882,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3118
2882
  versionId: string | null,
3119
2883
  logger: ITelemetryLogger,
3120
2884
  event: ITelemetryGenericEvent,
2885
+ fetchSource?: FetchSource,
3121
2886
  ): Promise<{ snapshotTree: ISnapshotTree; versionId: string; }> {
3122
2887
  return PerformanceEvent.timedExecAsync(
3123
2888
  logger, event, async (perfEvent: {
@@ -3129,7 +2894,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3129
2894
  const stats: { getVersionDuration?: number; getSnapshotDuration?: number; } = {};
3130
2895
  const trace = Trace.start();
3131
2896
 
3132
- const versions = await this.storage.getVersions(versionId, 1);
2897
+ const versions = await this.storage.getVersions(
2898
+ versionId, 1, "refreshLatestSummaryAckFromServer", fetchSource);
3133
2899
  assert(!!versions && !!versions[0], 0x137 /* "Failed to get version from storage" */);
3134
2900
  stats.getVersionDuration = trace.trace().duration;
3135
2901
 
@@ -3157,15 +2923,21 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3157
2923
  this.baseSnapshotBlobs = await SerializedSnapshotStorage.serializeTree(this.context.baseSnapshot, this.storage);
3158
2924
  }
3159
2925
 
3160
- public getPendingLocalState(): IPendingRuntimeState {
2926
+ public getPendingLocalState(): unknown {
3161
2927
  if (!(this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad)) {
3162
2928
  throw new UsageError("can't get state when offline load disabled");
3163
2929
  }
3164
2930
 
2931
+ // Flush pending batch.
2932
+ // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
2933
+ // to close current batch.
2934
+ this.flush();
2935
+
3165
2936
  const previousPendingState = this.context.pendingLocalState as IPendingRuntimeState | undefined;
3166
2937
  if (previousPendingState) {
3167
2938
  return {
3168
2939
  pending: this.pendingStateManager.getLocalState(),
2940
+ pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
3169
2941
  snapshotBlobs: previousPendingState.snapshotBlobs,
3170
2942
  baseSnapshot: previousPendingState.baseSnapshot,
3171
2943
  savedOps: this.savedOps,
@@ -3175,6 +2947,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3175
2947
  assert(!!this.baseSnapshotBlobs, 0x2e7 /* "Must serialize base snapshot blobs before getting runtime state" */);
3176
2948
  return {
3177
2949
  pending: this.pendingStateManager.getLocalState(),
2950
+ pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
3178
2951
  snapshotBlobs: this.baseSnapshotBlobs,
3179
2952
  baseSnapshot: this.context.baseSnapshot,
3180
2953
  savedOps: this.savedOps,
@@ -3249,6 +3022,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
3249
3022
  // we may not have seen every sequence number (because of system ops) so apply everything once we
3250
3023
  // don't have any more saved ops
3251
3024
  await this.pendingStateManager.applyStashedOpsAt();
3025
+
3026
+ // If it's not the case, we should take it into account when calculating dirty state.
3027
+ assert(this.context.attachState === AttachState.Attached,
3028
+ 0x3d5 /* this function is called for attached containers only */);
3029
+ if (!this.hasPendingMessages()) {
3030
+ this.updateDocumentDirtyState(false);
3031
+ }
3032
+ }
3033
+
3034
+ private validateSummaryHeuristicConfiguration(configuration: ISummaryConfigurationHeuristics) {
3035
+ // eslint-disable-next-line no-restricted-syntax
3036
+ for (const prop in configuration) {
3037
+ if (typeof configuration[prop] === "number" && configuration[prop] < 0) {
3038
+ throw new UsageError(`Summary heuristic configuration property "${prop}" cannot be less than 0`);
3039
+ }
3040
+ }
3252
3041
  }
3253
3042
  }
3254
3043