@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
@@ -0,0 +1,139 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { ITelemetryProperties } from "@fluidframework/common-definitions";
7
+ import { ICriticalContainerError } from "@fluidframework/container-definitions";
8
+ import {
9
+ IConfigProvider,
10
+ IFluidErrorBase,
11
+ LoggingError,
12
+ MonitoringContext,
13
+ } from "@fluidframework/telemetry-utils";
14
+ import { oneDayMs } from "./garbageCollection";
15
+
16
+ /**
17
+ * Feature Gate Key -
18
+ * How many days between closing the container from this error (avoids locking user out of their file altogether)
19
+ */
20
+ export const skipClosureForXDaysKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.SkipClosureForXDays";
21
+
22
+ /**
23
+ * LocalStorage key (NOT via feature gate / monitoring context)
24
+ * A map from docId to info about the last time we closed due to this error
25
+ */
26
+ export const closuresMapLocalStorageKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection.Closures";
27
+
28
+ /**
29
+ * Feature gate key to enable closing the container if SweepReady objects are used.
30
+ * Value should contain keywords "interactiveClient" and/or "summarizer" to enable detection in each container type
31
+ */
32
+ const sweepReadyUsageDetectionSetting = {
33
+ read(config: IConfigProvider) {
34
+ const sweepReadyUsageDetectionKey = "Fluid.GarbageCollection.Dogfood.SweepReadyUsageDetection";
35
+ const value = config.getString(sweepReadyUsageDetectionKey);
36
+ if (value === undefined) {
37
+ return { interactiveClient: false, summarizer: false };
38
+ }
39
+ return {
40
+ interactiveClient: value.includes("interactiveClient"),
41
+ summarizer: value.includes("summarizer"),
42
+ };
43
+ },
44
+ };
45
+
46
+ /**
47
+ * Error class raised when a SweepReady object is used, indicating a bug in how
48
+ * references are managed in the container by the application, or a bug in how
49
+ * GC tracks those references.
50
+ *
51
+ * There's a chance for false positives when this error is raised by an Interactive Container,
52
+ * since only the Summarizer has the latest truth about unreferenced node tracking
53
+ */
54
+ export class SweepReadyUsageError extends LoggingError implements IFluidErrorBase {
55
+ /** This errorType will be in temporary use (until Sweep is fully implemented) so don't add to any errorType type */
56
+ public errorType: string = "unreferencedObjectUsedAfterGarbageCollected";
57
+ }
58
+
59
+ /**
60
+ * This class encapsulates the logic around what to do when a SweepReady object is used.
61
+ * There are several tactics we plan to use in Dogfood environments to aid diagnosis of these cases:
62
+ * - Closing the interactive container when either the interactive or summarizer client detects this kind of violation
63
+ * (via sweepReadyUsageDetectionSetting above)
64
+ * - Throttling the frequency of these crashes via a "Skip Closure Period" per container per device
65
+ * (via skipClosureForXDaysKey above. Uses localStorage and closuresMapLocalStorageKey to implement this behavior)
66
+ */
67
+ export class SweepReadyUsageDetectionHandler {
68
+ private readonly localStorage: Pick<Storage, "getItem" | "setItem">;
69
+
70
+ constructor(
71
+ private readonly uniqueContainerKey: string,
72
+ private readonly mc: MonitoringContext,
73
+ private readonly closeFn: (error?: ICriticalContainerError) => void,
74
+ localStorageOverride?: Pick<Storage, "getItem" | "setItem">,
75
+ ) {
76
+ const noopStorage = { getItem: () => null, setItem: () => {} };
77
+ // localStorage is not defined in Node environment, so fall back to noopStorage if needed.
78
+ this.localStorage = localStorageOverride ?? globalThis.localStorage ?? noopStorage;
79
+
80
+ if (this.localStorage === noopStorage) {
81
+ // This means the Skip Closure Period logic will not work.
82
+ this.mc.logger.sendTelemetryEvent({ eventName: "SweepReadyUsageDetectionHandlerNoopStorage" });
83
+ }
84
+ }
85
+
86
+ /**
87
+ * If SweepReady Usage Detection is enabled, close the interactive container.
88
+ * If the SkipClosureForXDays setting is set, don't close the container more than once in that period.
89
+ *
90
+ * Once Sweep is fully implemented, this will be removed since the objects will be gone
91
+ * and errors will arise elsewhere in the runtime
92
+ */
93
+ public usageDetectedInInteractiveClient(errorProps: ITelemetryProperties) {
94
+ if (!sweepReadyUsageDetectionSetting.read(this.mc.config).interactiveClient) {
95
+ return;
96
+ }
97
+
98
+ // Default stance is we close every time - this reflects the severity of SweepReady Object Usage.
99
+ // However, we may choose to "throttle" the closures by setting the SkipClosureForXDays setting,
100
+ // which will only allow the container to close once during that period, to avoid locking users out.
101
+ let shouldClose: boolean = true;
102
+ let pastClosuresMap: Record<string, { lastCloseTime: number; } | undefined> = {};
103
+ let lastCloseTime: number | undefined;
104
+ const skipClosureForXDays = this.mc.config.getNumber(skipClosureForXDaysKey);
105
+ if (skipClosureForXDays !== undefined) {
106
+ // Read pastClosuresMap from localStorage then extract the lastCloseTime from the map
107
+ try {
108
+ const rawValue = this.localStorage.getItem(closuresMapLocalStorageKey);
109
+ const parsedValue = rawValue === null ? {} : JSON.parse(rawValue);
110
+ if (typeof parsedValue === "object") {
111
+ pastClosuresMap = parsedValue;
112
+ }
113
+ } catch (e) {
114
+ }
115
+ lastCloseTime = pastClosuresMap[this.uniqueContainerKey]?.lastCloseTime;
116
+
117
+ // Don't close if we did already within the Skip Closure Period
118
+ if (lastCloseTime !== undefined && Date.now() < lastCloseTime + skipClosureForXDays * oneDayMs) {
119
+ shouldClose = false;
120
+ }
121
+ }
122
+
123
+ const error = new SweepReadyUsageError(
124
+ "SweepReady object used in Non-Summarizer Client",
125
+ { errorDetails: JSON.stringify({ ...errorProps, lastCloseTime, skipClosureForXDays }) },
126
+ );
127
+ if (shouldClose) {
128
+ // Update closures map in localStorage before closing
129
+ // Note there is a race condition between different tabs updating localStorage and overwriting
130
+ // each others' updates. If so, some tab will crash again. Just reload one at a time to get unstuck
131
+ pastClosuresMap[this.uniqueContainerKey] = { lastCloseTime: Date.now() };
132
+ this.localStorage.setItem(closuresMapLocalStorageKey, JSON.stringify(pastClosuresMap));
133
+
134
+ this.closeFn(error);
135
+ } else {
136
+ this.mc.logger.sendErrorEvent({ eventName: "SweepReadyObject_UsageAllowed" }, error);
137
+ }
138
+ }
139
+ }
package/src/index.ts CHANGED
@@ -14,12 +14,10 @@ export {
14
14
  ISummaryConfigurationDisableSummarizer,
15
15
  ISummaryConfigurationDisableHeuristics,
16
16
  IContainerRuntimeOptions,
17
- IPendingRuntimeState,
18
17
  IRootSummaryTreeWithStats,
19
18
  isRuntimeMessage,
20
19
  RuntimeMessage,
21
20
  unpackRuntimeMessage,
22
- ScheduleManager,
23
21
  agentSchedulerId,
24
22
  ContainerRuntime,
25
23
  RuntimeHeaders,
@@ -41,6 +39,7 @@ export {
41
39
  IPendingMessage,
42
40
  IPendingState,
43
41
  } from "./pendingStateManager";
42
+ export { ScheduleManager } from "./scheduleManager";
44
43
  export { Summarizer } from "./summarizer";
45
44
  export {
46
45
  EnqueueSummarizeResult,
@@ -0,0 +1,19 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { ISequencedDocumentMessage, ISequencedDocumentSystemMessage } from "@fluidframework/protocol-definitions";
7
+
8
+ export const opSize = (op: ISequencedDocumentMessage): number => {
9
+ // Some messages may already have string contents,
10
+ // so stringifying them again will add inaccurate overhead.
11
+ const content = typeof op.contents === "string" ?
12
+ op.contents :
13
+ JSON.stringify(op.contents) ?? "";
14
+ const data = opHasData(op) ? op.data : "";
15
+ return content.length + data.length;
16
+ };
17
+
18
+ const opHasData = (op: ISequencedDocumentMessage): op is ISequencedDocumentSystemMessage =>
19
+ (op as ISequencedDocumentSystemMessage).data !== undefined;
@@ -208,11 +208,17 @@ export interface IOrderedClientElectionEvents extends IEvent {
208
208
  export interface ISerializedElection {
209
209
  /** Sequence number at the time of the latest election. */
210
210
  readonly electionSequenceNumber: number;
211
- /** Most recently elected client id. This is either:
211
+
212
+ /**
213
+ * Most recently elected client id. This is either:
214
+ *
212
215
  * 1. the interactive elected parent client, in which case electedClientId === electedParentId,
213
- * and the SummaryManager on the elected client will spawn a summarizer client, or
214
- * 2. the non-interactive summarizer client itself. */
216
+ * and the SummaryManager on the elected client will spawn a summarizer client, or
217
+ *
218
+ * 2. the non-interactive summarizer client itself.
219
+ */
215
220
  readonly electedClientId: string | undefined;
221
+
216
222
  /** Most recently elected parent client id. This is always an interactive client. */
217
223
  readonly electedParentId: string | undefined;
218
224
  }
@@ -221,10 +227,15 @@ export interface ISerializedElection {
221
227
  export interface IOrderedClientElection extends IEventProvider<IOrderedClientElectionEvents> {
222
228
  /** Count of eligible clients in the collection. */
223
229
  readonly eligibleCount: number;
224
- /** Currently elected client. This is either:
230
+
231
+ /**
232
+ * Currently elected client. This is either:
233
+ *
225
234
  * 1. the interactive elected parent client, in which case electedClientId === electedParentId,
226
- * and the SummaryManager on the elected client will spawn a summarizer client, or
227
- * 2. the non-interactive summarizer client itself. */
235
+ * and the SummaryManager on the elected client will spawn a summarizer client, or
236
+ *
237
+ * 2. the non-interactive summarizer client itself.
238
+ */
228
239
  readonly electedClient: ITrackedClient | undefined;
229
240
  /** Currently elected parent client. This is always an interactive client. */
230
241
  readonly electedParent: ITrackedClient | undefined;
@@ -283,13 +294,20 @@ export class OrderedClientElection
283
294
  * electedClient leaves the quorum.
284
295
  *
285
296
  * A typical sequence looks like this:
297
+ *
286
298
  * i. Begin by electing A. electedParent === A, electedClient === A.
299
+ *
287
300
  * ii. SummaryManager running on A spawns a summarizer client, A'. electedParent === A, electedClient === A'
301
+ *
288
302
  * iii. A' stops producing summaries. A new parent client, B, is elected. electedParent === B, electedClient === A'
303
+ *
289
304
  * iv. SummaryManager running on A detects the change to electedParent and tells the summarizer to stop, but A'
290
- * is in mid-summarization. No new summarizer is spawned, as electedParent !== electedClient.
305
+ * is in mid-summarization. No new summarizer is spawned, as electedParent !== electedClient.
306
+ *
291
307
  * v. A' completes its summary, and the summarizer and backing client are torn down.
308
+ *
292
309
  * vi. A' leaves the quorum, and B takes its place as electedClient. electedParent === B, electedClient === B
310
+ *
293
311
  * vii. SummaryManager running on B spawns a summarizer client, B'. electedParent === B, electedClient === B'
294
312
  */
295
313
  public get electedClient() {
@@ -357,7 +375,8 @@ export class OrderedClientElection
357
375
  }
358
376
  }
359
377
 
360
- /** Tries changing the elected client, raising an event if it is different.
378
+ /**
379
+ * Tries changing the elected client, raising an event if it is different.
361
380
  * Note that this function does no eligibility or suitability checks. If we get here, then
362
381
  * we will set _electedClient, and we will set _electedParent if this is an interactive client.
363
382
  */
@@ -467,7 +486,8 @@ export class OrderedClientElection
467
486
  return this.orderedClientCollection.getAllClients().filter(this.isEligibleFn);
468
487
  }
469
488
 
470
- /** Advance election to the next-oldest client. This is called if the current parent is leaving the quorum,
489
+ /**
490
+ * Advance election to the next-oldest client. This is called if the current parent is leaving the quorum,
471
491
  * or if the current summarizer is not responsive and we want to stop it and spawn a new one.
472
492
  */
473
493
  public incrementElectedClient(sequenceNumber: number): void {
@@ -482,7 +502,8 @@ export class OrderedClientElection
482
502
  }
483
503
  }
484
504
 
485
- /** (Re-)start election with the oldest client in the quorum. This is called if we need to summarize
505
+ /**
506
+ * (Re-)start election with the oldest client in the quorum. This is called if we need to summarize
486
507
  * and no client has been elected.
487
508
  */
488
509
  public resetElectedClient(sequenceNumber: number): void {
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "1.2.7";
9
+ export const pkgVersion = "2.0.0-dev.1.3.0.96595";
@@ -11,9 +11,9 @@ import {
11
11
  ISequencedDocumentMessage,
12
12
  } from "@fluidframework/protocol-definitions";
13
13
  import { FlushMode } from "@fluidframework/runtime-definitions";
14
- import { wrapError } from "@fluidframework/telemetry-utils";
15
14
  import Deque from "double-ended-queue";
16
15
  import { ContainerMessageType } from "./containerRuntime";
16
+ import { pkgVersion } from "./packageVersion";
17
17
 
18
18
  /**
19
19
  * This represents a message that has been submitted and is added to the pending queue when `submit` is called on the
@@ -68,10 +68,6 @@ export interface IRuntimeStateHandler{
68
68
  content: any,
69
69
  localOpMetadata: unknown,
70
70
  opMetadata: Record<string, unknown> | undefined): void;
71
- rollback(
72
- type: ContainerMessageType,
73
- content: any,
74
- localOpMetadata: unknown): void;
75
71
  }
76
72
 
77
73
  /**
@@ -232,7 +228,11 @@ export class PendingStateManager implements IDisposable {
232
228
 
233
229
  // then we push onto pendingStates which will cause PendingStateManager to resubmit when we connect
234
230
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
235
- this.pendingStates.push(this.initialStates.shift()!);
231
+ const firstPendingState = this.initialStates.shift()!;
232
+ this.pendingStates.push(firstPendingState);
233
+ if (firstPendingState.type === "message") {
234
+ this._pendingMessagesCount++;
235
+ }
236
236
  }
237
237
  }
238
238
 
@@ -266,6 +266,7 @@ export class PendingStateManager implements IDisposable {
266
266
  }
267
267
 
268
268
  this._pendingMessagesCount--;
269
+ assert(this._pendingMessagesCount >= 0, 0x3d6 /* positive */);
269
270
 
270
271
  // Post-processing part - If we are processing a batch then this could be the last message in the batch.
271
272
  this.maybeProcessBatchEnd(message);
@@ -285,9 +286,11 @@ export class PendingStateManager implements IDisposable {
285
286
 
286
287
  /**
287
288
  * We are checking if the next message is the start of a batch. It can happen in the following scenarios:
289
+ *
288
290
  * 1. The FlushMode was set to TurnBased before this message was sent.
291
+ *
289
292
  * 2. The FlushMode was already TurnBased and a flush was called before this message was sent. This essentially
290
- * means that the flush marked the end of a previous batch and beginning of a new batch.
293
+ * means that the flush marked the end of a previous batch and beginning of a new batch.
291
294
  *
292
295
  * Keep reading pending states from the queue until we encounter a message. It's possible that the FlushMode was
293
296
  * updated a bunch of times without sending any messages.
@@ -368,8 +371,23 @@ export class PendingStateManager implements IDisposable {
368
371
  } else {
369
372
  // Get the batch metadata from the last message in the batch.
370
373
  const batchEndMetadata = message.metadata?.batch;
371
- assert(batchBeginMetadata === true, 0x16f /* "Did not receive batch begin metadata" */);
372
- assert(batchEndMetadata === false, 0x170 /* "Did not receive batch end metadata" */);
374
+ if (batchBeginMetadata !== true || batchEndMetadata !== false) {
375
+ this.stateHandler.close(DataProcessingError.create(
376
+ "Pending batch inconsistency", // Formerly known as asserts 0x16f and 0x170
377
+ "processPendingLocalMessage",
378
+ message,
379
+ {
380
+ runtimeVersion: pkgVersion,
381
+ batchClientId: this.pendingBatchBeginMessage.clientId,
382
+ clientId: this.stateHandler.clientId(),
383
+ hasBatchStart: batchBeginMetadata === true,
384
+ hasBatchEnd: batchEndMetadata === false,
385
+ messageType: message.type,
386
+ batchStartSequenceNumber: this.pendingBatchBeginMessage.clientSequenceNumber,
387
+ pendingMessagesCount: this.pendingMessagesCount,
388
+ nextPendingState: nextPendingState.type,
389
+ }));
390
+ }
373
391
  }
374
392
 
375
393
  // Clear the pending batch state now that we have processed the entire batch.
@@ -377,31 +395,6 @@ export class PendingStateManager implements IDisposable {
377
395
  this.isProcessingBatch = false;
378
396
  }
379
397
 
380
- /**
381
- * Capture the pending state at this point
382
- */
383
- public checkpoint() {
384
- const checkpointHead = this.pendingStates.peekBack();
385
- return {
386
- rollback: () => {
387
- try {
388
- while (this.pendingStates.peekBack() !== checkpointHead) {
389
- this.rollbackNextPendingState();
390
- }
391
- } catch (err) {
392
- const error = wrapError(err, (message) => {
393
- return DataProcessingError.create(
394
- `RollbackError: ${message}`,
395
- "checkpointRollback",
396
- undefined) as DataProcessingError;
397
- });
398
- this.stateHandler.close(error);
399
- throw error;
400
- }
401
- },
402
- };
403
- }
404
-
405
398
  /**
406
399
  * Returns the next pending state from the pending state queue.
407
400
  */
@@ -411,31 +404,6 @@ export class PendingStateManager implements IDisposable {
411
404
  return nextPendingState;
412
405
  }
413
406
 
414
- /**
415
- * Undo the last pending state
416
- */
417
- private rollbackNextPendingState() {
418
- const pendingStatesCount = this.pendingStates.length;
419
- if (pendingStatesCount === 0) {
420
- return;
421
- }
422
-
423
- this._pendingMessagesCount--;
424
-
425
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
426
- const pendingState = this.pendingStates.pop()!;
427
- switch (pendingState.type) {
428
- case "message":
429
- this.stateHandler.rollback(
430
- pendingState.messageType,
431
- pendingState.content,
432
- pendingState.localOpMetadata);
433
- break;
434
- default:
435
- throw new Error(`Can't rollback state ${pendingState.type}`);
436
- }
437
- }
438
-
439
407
  /**
440
408
  * Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
441
409
  * states in its queue. This includes setting the FlushMode and triggering resubmission of unacked ops.
@@ -6,6 +6,7 @@
6
6
  import { IDisposable, ITelemetryLogger } from "@fluidframework/common-definitions";
7
7
  import { assert, delay, Deferred, PromiseTimer } from "@fluidframework/common-utils";
8
8
  import { UsageError } from "@fluidframework/container-utils";
9
+ import { isRuntimeMessage } from "@fluidframework/driver-utils";
9
10
  import {
10
11
  ISequencedDocumentMessage,
11
12
  MessageType,
@@ -14,6 +15,7 @@ import { ChildLogger } from "@fluidframework/telemetry-utils";
14
15
  import {
15
16
  ISummaryConfiguration,
16
17
  } from "./containerRuntime";
18
+ import { opSize } from "./opProperties";
17
19
  import { SummarizeHeuristicRunner } from "./summarizerHeuristics";
18
20
  import {
19
21
  IEnqueueSummarizeOptions,
@@ -28,6 +30,7 @@ import {
28
30
  ISummaryCancellationToken,
29
31
  ISummarizeResults,
30
32
  ISummarizeTelemetryProperties,
33
+ ISummarizerRuntime,
31
34
  ISummarizeRunnerTelemetry,
32
35
  } from "./summarizerTypes";
33
36
  import { IClientSummaryWatcher, SummaryCollection } from "./summaryCollection";
@@ -58,6 +61,7 @@ export class RunningSummarizer implements IDisposable {
58
61
  summaryCollection: SummaryCollection,
59
62
  cancellationToken: ISummaryCancellationToken,
60
63
  stopSummarizerCallback: (reason: SummarizerStopReason) => void,
64
+ runtime: ISummarizerRuntime,
61
65
  ): Promise<RunningSummarizer> {
62
66
  const summarizer = new RunningSummarizer(
63
67
  logger,
@@ -68,12 +72,36 @@ export class RunningSummarizer implements IDisposable {
68
72
  raiseSummarizingError,
69
73
  summaryCollection,
70
74
  cancellationToken,
71
- stopSummarizerCallback);
75
+ stopSummarizerCallback,
76
+ runtime);
72
77
 
73
78
  await summarizer.waitStart();
74
79
 
75
- // Run the heuristics after starting
80
+ // Update heuristic counts
81
+ // By the time we get here, there are potentially ops missing from the heuristic summary counts
82
+ // Examples of where this could happen:
83
+ // 1. Op is processed during the time that we are initiating the RunningSummarizer instance but before we
84
+ // listen for the op events (will get missed by the handlers in the current workflow)
85
+ // 2. Op was sequenced after the last time we summarized (op sequence number > summarize ref sequence number)
86
+ const diff = runtime.deltaManager.lastSequenceNumber - (
87
+ heuristicData.lastSuccessfulSummary.refSequenceNumber
88
+ + heuristicData.numNonRuntimeOps
89
+ + heuristicData.numRuntimeOps);
90
+ heuristicData.hasMissingOpData = diff > 0;
91
+
92
+ if (heuristicData.hasMissingOpData) {
93
+ // Split the diff 50-50 and increment the counts appropriately
94
+ heuristicData.numNonRuntimeOps += Math.ceil(diff / 2);
95
+ heuristicData.numRuntimeOps += Math.floor(diff / 2);
96
+ }
97
+
98
+ // Update last seq number (in case the handlers haven't processed anything yet)
99
+ heuristicData.lastOpSequenceNumber = runtime.deltaManager.lastSequenceNumber;
100
+
101
+ // Start heuristics
102
+ summarizer.heuristicRunner?.start();
76
103
  summarizer.heuristicRunner?.run();
104
+
77
105
  return summarizer;
78
106
  }
79
107
 
@@ -95,6 +123,7 @@ export class RunningSummarizer implements IDisposable {
95
123
  } | undefined;
96
124
  private summarizeCount = 0;
97
125
  private totalSuccessfulAttempts = 0;
126
+ private initialized = false;
98
127
 
99
128
  private constructor(
100
129
  baseLogger: ITelemetryLogger,
@@ -106,6 +135,7 @@ export class RunningSummarizer implements IDisposable {
106
135
  private readonly summaryCollection: SummaryCollection,
107
136
  private readonly cancellationToken: ISummaryCancellationToken,
108
137
  private readonly stopSummarizerCallback: (reason: SummarizerStopReason) => void,
138
+ private readonly runtime: ISummarizerRuntime,
109
139
  ) {
110
140
  const telemetryProps: ISummarizeRunnerTelemetry = {
111
141
  summarizeCount: () => this.summarizeCount,
@@ -175,9 +205,13 @@ export class RunningSummarizer implements IDisposable {
175
205
  this.summaryWatcher,
176
206
  this.logger,
177
207
  );
208
+
209
+ // Listen for ops
210
+ this.runtime.deltaManager.on("op", (op) => { this.handleOp(op); });
178
211
  }
179
212
 
180
213
  public dispose(): void {
214
+ this.runtime.deltaManager.off("op", (op) => { this.handleOp(op); });
181
215
  this.summaryWatcher.dispose();
182
216
  this.heuristicRunner?.dispose();
183
217
  this.heuristicRunner = undefined;
@@ -199,30 +233,48 @@ export class RunningSummarizer implements IDisposable {
199
233
  ? this.logger
200
234
  : undefined;
201
235
 
202
- public handleSystemOp(op: ISequencedDocumentMessage) {
203
- switch (op.type) {
204
- case MessageType.ClientLeave:
205
- case MessageType.ClientJoin:
206
- case MessageType.Propose: {
207
- // Synchronously handle quorum ops like regular ops
208
- this.handleOp(undefined, op);
209
- return;
210
- }
211
- default: {
212
- return;
213
- }
214
- }
215
- }
236
+ /** We only want a single heuristic runner micro-task (will provide better optimized grouping of ops) */
237
+ private heuristicRunnerMicroTaskExists = false;
216
238
 
217
- public handleOp(error: any, { sequenceNumber, type, clientId, contents }: ISequencedDocumentMessage) {
218
- if (error !== undefined) {
219
- return;
239
+ public handleOp(op: ISequencedDocumentMessage) {
240
+ this.heuristicData.lastOpSequenceNumber = op.sequenceNumber;
241
+
242
+ if (isRuntimeMessage(op)) {
243
+ this.heuristicData.numRuntimeOps++;
244
+ } else {
245
+ this.heuristicData.numNonRuntimeOps++;
220
246
  }
221
- this.heuristicData.lastOpSequenceNumber = sequenceNumber;
247
+
248
+ this.heuristicData.totalOpsSize += opSize(op);
222
249
 
223
250
  // Check for enqueued on-demand summaries; Intentionally do nothing otherwise
224
- if (!this.tryRunEnqueuedSummary()) {
225
- this.heuristicRunner?.run();
251
+ if (this.initialized
252
+ && this.opCanTriggerSummary(op)
253
+ && !this.tryRunEnqueuedSummary()
254
+ && !this.heuristicRunnerMicroTaskExists) {
255
+ this.heuristicRunnerMicroTaskExists = true;
256
+ Promise.resolve().then(() => {
257
+ this.heuristicRunner?.run();
258
+ }).finally(() => {
259
+ this.heuristicRunnerMicroTaskExists = false;
260
+ });
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Can the given op trigger a summary?
266
+ * # Currently only prevents summaries for Summarize and SummaryAck ops
267
+ * @param op - op to check
268
+ * @returns true if this type of op can trigger a summary
269
+ */
270
+ private opCanTriggerSummary(op: ISequencedDocumentMessage): boolean {
271
+ switch (op.type) {
272
+ case MessageType.Summarize:
273
+ case MessageType.SummaryAck:
274
+ case MessageType.SummaryNack:
275
+ return false;
276
+ default:
277
+ return true;
226
278
  }
227
279
  }
228
280
 
@@ -274,6 +326,7 @@ export class RunningSummarizer implements IDisposable {
274
326
  summarySequenceNumber: waitStartResult.value.summaryOp.sequenceNumber,
275
327
  });
276
328
  }
329
+ this.initialized = true;
277
330
  }
278
331
 
279
332
  /**