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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/.eslintrc.js +8 -22
  2. package/.mocharc.js +12 -0
  3. package/dist/batchManager.d.ts +37 -0
  4. package/dist/batchManager.d.ts.map +1 -0
  5. package/dist/batchManager.js +73 -0
  6. package/dist/batchManager.js.map +1 -0
  7. package/dist/batchTracker.d.ts +1 -2
  8. package/dist/batchTracker.d.ts.map +1 -1
  9. package/dist/batchTracker.js +2 -3
  10. package/dist/batchTracker.js.map +1 -1
  11. package/dist/blobManager.d.ts +87 -25
  12. package/dist/blobManager.d.ts.map +1 -1
  13. package/dist/blobManager.js +317 -99
  14. package/dist/blobManager.js.map +1 -1
  15. package/dist/containerRuntime.d.ts +110 -125
  16. package/dist/containerRuntime.d.ts.map +1 -1
  17. package/dist/containerRuntime.js +360 -549
  18. package/dist/containerRuntime.js.map +1 -1
  19. package/dist/dataStore.js +29 -24
  20. package/dist/dataStore.js.map +1 -1
  21. package/dist/dataStoreContext.d.ts +20 -14
  22. package/dist/dataStoreContext.d.ts.map +1 -1
  23. package/dist/dataStoreContext.js +49 -58
  24. package/dist/dataStoreContext.js.map +1 -1
  25. package/dist/dataStores.d.ts +12 -5
  26. package/dist/dataStores.d.ts.map +1 -1
  27. package/dist/dataStores.js +21 -20
  28. package/dist/dataStores.js.map +1 -1
  29. package/dist/deltaScheduler.d.ts +6 -4
  30. package/dist/deltaScheduler.d.ts.map +1 -1
  31. package/dist/deltaScheduler.js +6 -4
  32. package/dist/deltaScheduler.js.map +1 -1
  33. package/dist/garbageCollection.d.ts +74 -14
  34. package/dist/garbageCollection.d.ts.map +1 -1
  35. package/dist/garbageCollection.js +248 -169
  36. package/dist/garbageCollection.js.map +1 -1
  37. package/dist/gcSweepReadyUsageDetection.d.ts +53 -0
  38. package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -0
  39. package/dist/gcSweepReadyUsageDetection.js +135 -0
  40. package/dist/gcSweepReadyUsageDetection.js.map +1 -0
  41. package/dist/index.d.ts +2 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/opProperties.d.ts +7 -0
  46. package/dist/opProperties.d.ts.map +1 -0
  47. package/dist/opProperties.js +20 -0
  48. package/dist/opProperties.js.map +1 -0
  49. package/dist/orderedClientElection.d.ts +28 -10
  50. package/dist/orderedClientElection.d.ts.map +1 -1
  51. package/dist/orderedClientElection.js +14 -4
  52. package/dist/orderedClientElection.js.map +1 -1
  53. package/dist/packageVersion.d.ts +1 -1
  54. package/dist/packageVersion.d.ts.map +1 -1
  55. package/dist/packageVersion.js +1 -1
  56. package/dist/packageVersion.js.map +1 -1
  57. package/dist/pendingStateManager.d.ts +0 -11
  58. package/dist/pendingStateManager.d.ts.map +1 -1
  59. package/dist/pendingStateManager.js +24 -46
  60. package/dist/pendingStateManager.js.map +1 -1
  61. package/dist/runningSummarizer.d.ts +14 -4
  62. package/dist/runningSummarizer.d.ts.map +1 -1
  63. package/dist/runningSummarizer.js +69 -27
  64. package/dist/runningSummarizer.js.map +1 -1
  65. package/dist/scheduleManager.d.ts +31 -0
  66. package/dist/scheduleManager.d.ts.map +1 -0
  67. package/dist/scheduleManager.js +243 -0
  68. package/dist/scheduleManager.js.map +1 -0
  69. package/dist/summarizer.d.ts +0 -2
  70. package/dist/summarizer.d.ts.map +1 -1
  71. package/dist/summarizer.js +1 -12
  72. package/dist/summarizer.js.map +1 -1
  73. package/dist/summarizerHeuristics.d.ts +26 -4
  74. package/dist/summarizerHeuristics.d.ts.map +1 -1
  75. package/dist/summarizerHeuristics.js +98 -18
  76. package/dist/summarizerHeuristics.js.map +1 -1
  77. package/dist/summarizerTypes.d.ts +45 -18
  78. package/dist/summarizerTypes.d.ts.map +1 -1
  79. package/dist/summarizerTypes.js +1 -1
  80. package/dist/summarizerTypes.js.map +1 -1
  81. package/dist/summaryCollection.d.ts +1 -0
  82. package/dist/summaryCollection.d.ts.map +1 -1
  83. package/dist/summaryCollection.js +31 -15
  84. package/dist/summaryCollection.js.map +1 -1
  85. package/dist/summaryFormat.d.ts +0 -5
  86. package/dist/summaryFormat.d.ts.map +1 -1
  87. package/dist/summaryFormat.js.map +1 -1
  88. package/dist/summaryGenerator.d.ts +1 -0
  89. package/dist/summaryGenerator.d.ts.map +1 -1
  90. package/dist/summaryGenerator.js +11 -9
  91. package/dist/summaryGenerator.js.map +1 -1
  92. package/dist/summaryManager.d.ts +2 -2
  93. package/dist/summaryManager.d.ts.map +1 -1
  94. package/dist/summaryManager.js +22 -7
  95. package/dist/summaryManager.js.map +1 -1
  96. package/lib/batchManager.d.ts +37 -0
  97. package/lib/batchManager.d.ts.map +1 -0
  98. package/lib/batchManager.js +69 -0
  99. package/lib/batchManager.js.map +1 -0
  100. package/lib/batchTracker.d.ts +1 -2
  101. package/lib/batchTracker.d.ts.map +1 -1
  102. package/lib/batchTracker.js +2 -3
  103. package/lib/batchTracker.js.map +1 -1
  104. package/lib/blobManager.d.ts +87 -25
  105. package/lib/blobManager.d.ts.map +1 -1
  106. package/lib/blobManager.js +319 -101
  107. package/lib/blobManager.js.map +1 -1
  108. package/lib/containerRuntime.d.ts +110 -125
  109. package/lib/containerRuntime.d.ts.map +1 -1
  110. package/lib/containerRuntime.js +366 -554
  111. package/lib/containerRuntime.js.map +1 -1
  112. package/lib/dataStore.js +29 -24
  113. package/lib/dataStore.js.map +1 -1
  114. package/lib/dataStoreContext.d.ts +20 -14
  115. package/lib/dataStoreContext.d.ts.map +1 -1
  116. package/lib/dataStoreContext.js +46 -55
  117. package/lib/dataStoreContext.js.map +1 -1
  118. package/lib/dataStores.d.ts +12 -5
  119. package/lib/dataStores.d.ts.map +1 -1
  120. package/lib/dataStores.js +21 -20
  121. package/lib/dataStores.js.map +1 -1
  122. package/lib/deltaScheduler.d.ts +6 -4
  123. package/lib/deltaScheduler.d.ts.map +1 -1
  124. package/lib/deltaScheduler.js +6 -4
  125. package/lib/deltaScheduler.js.map +1 -1
  126. package/lib/garbageCollection.d.ts +74 -14
  127. package/lib/garbageCollection.d.ts.map +1 -1
  128. package/lib/garbageCollection.js +237 -159
  129. package/lib/garbageCollection.js.map +1 -1
  130. package/lib/gcSweepReadyUsageDetection.d.ts +53 -0
  131. package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -0
  132. package/lib/gcSweepReadyUsageDetection.js +130 -0
  133. package/lib/gcSweepReadyUsageDetection.js.map +1 -0
  134. package/lib/index.d.ts +2 -1
  135. package/lib/index.d.ts.map +1 -1
  136. package/lib/index.js +2 -1
  137. package/lib/index.js.map +1 -1
  138. package/lib/opProperties.d.ts +7 -0
  139. package/lib/opProperties.d.ts.map +1 -0
  140. package/lib/opProperties.js +16 -0
  141. package/lib/opProperties.js.map +1 -0
  142. package/lib/orderedClientElection.d.ts +28 -10
  143. package/lib/orderedClientElection.d.ts.map +1 -1
  144. package/lib/orderedClientElection.js +14 -4
  145. package/lib/orderedClientElection.js.map +1 -1
  146. package/lib/packageVersion.d.ts +1 -1
  147. package/lib/packageVersion.d.ts.map +1 -1
  148. package/lib/packageVersion.js +1 -1
  149. package/lib/packageVersion.js.map +1 -1
  150. package/lib/pendingStateManager.d.ts +0 -11
  151. package/lib/pendingStateManager.d.ts.map +1 -1
  152. package/lib/pendingStateManager.js +24 -46
  153. package/lib/pendingStateManager.js.map +1 -1
  154. package/lib/runningSummarizer.d.ts +14 -4
  155. package/lib/runningSummarizer.d.ts.map +1 -1
  156. package/lib/runningSummarizer.js +69 -27
  157. package/lib/runningSummarizer.js.map +1 -1
  158. package/lib/scheduleManager.d.ts +31 -0
  159. package/lib/scheduleManager.d.ts.map +1 -0
  160. package/lib/scheduleManager.js +239 -0
  161. package/lib/scheduleManager.js.map +1 -0
  162. package/lib/summarizer.d.ts +0 -2
  163. package/lib/summarizer.d.ts.map +1 -1
  164. package/lib/summarizer.js +1 -12
  165. package/lib/summarizer.js.map +1 -1
  166. package/lib/summarizerHeuristics.d.ts +26 -4
  167. package/lib/summarizerHeuristics.d.ts.map +1 -1
  168. package/lib/summarizerHeuristics.js +98 -18
  169. package/lib/summarizerHeuristics.js.map +1 -1
  170. package/lib/summarizerTypes.d.ts +45 -18
  171. package/lib/summarizerTypes.d.ts.map +1 -1
  172. package/lib/summarizerTypes.js +1 -1
  173. package/lib/summarizerTypes.js.map +1 -1
  174. package/lib/summaryCollection.d.ts +1 -0
  175. package/lib/summaryCollection.d.ts.map +1 -1
  176. package/lib/summaryCollection.js +31 -15
  177. package/lib/summaryCollection.js.map +1 -1
  178. package/lib/summaryFormat.d.ts +0 -5
  179. package/lib/summaryFormat.d.ts.map +1 -1
  180. package/lib/summaryFormat.js.map +1 -1
  181. package/lib/summaryGenerator.d.ts +1 -0
  182. package/lib/summaryGenerator.d.ts.map +1 -1
  183. package/lib/summaryGenerator.js +11 -9
  184. package/lib/summaryGenerator.js.map +1 -1
  185. package/lib/summaryManager.d.ts +2 -2
  186. package/lib/summaryManager.d.ts.map +1 -1
  187. package/lib/summaryManager.js +22 -7
  188. package/lib/summaryManager.js.map +1 -1
  189. package/package.json +68 -25
  190. package/src/batchManager.ts +91 -0
  191. package/src/batchTracker.ts +2 -3
  192. package/src/blobManager.ts +385 -118
  193. package/src/containerRuntime.ts +523 -732
  194. package/src/dataStore.ts +49 -37
  195. package/src/dataStoreContext.ts +44 -56
  196. package/src/dataStores.ts +34 -30
  197. package/src/deltaScheduler.ts +6 -4
  198. package/src/garbageCollection.ts +296 -205
  199. package/src/gcSweepReadyUsageDetection.ts +147 -0
  200. package/src/index.ts +1 -2
  201. package/src/opProperties.ts +19 -0
  202. package/src/orderedClientElection.ts +31 -10
  203. package/src/packageVersion.ts +1 -1
  204. package/src/pendingStateManager.ts +27 -59
  205. package/src/runningSummarizer.ts +76 -23
  206. package/src/scheduleManager.ts +314 -0
  207. package/src/summarizer.ts +1 -18
  208. package/src/summarizerHeuristics.ts +136 -19
  209. package/src/summarizerTypes.ts +53 -18
  210. package/src/summaryCollection.ts +33 -18
  211. package/src/summaryFormat.ts +0 -6
  212. package/src/summaryGenerator.ts +40 -22
  213. package/src/summaryManager.ts +22 -7
  214. package/dist/opTelemetry.d.ts +0 -22
  215. package/dist/opTelemetry.d.ts.map +0 -1
  216. package/dist/opTelemetry.js +0 -59
  217. package/dist/opTelemetry.js.map +0 -1
  218. package/lib/opTelemetry.d.ts +0 -22
  219. package/lib/opTelemetry.d.ts.map +0 -1
  220. package/lib/opTelemetry.js +0 -55
  221. package/lib/opTelemetry.js.map +0 -1
  222. package/src/opTelemetry.ts +0 -71
@@ -0,0 +1,314 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import { EventEmitter } from "events";
6
+ import { IDeltaManager } from "@fluidframework/container-definitions";
7
+ import { IDocumentMessage, ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
8
+ import { ITelemetryLogger } from "@fluidframework/common-definitions";
9
+ import { ChildLogger } from "@fluidframework/telemetry-utils";
10
+ import { assert, performance } from "@fluidframework/common-utils";
11
+ import { isRuntimeMessage } from "@fluidframework/driver-utils";
12
+ import {
13
+ DataCorruptionError,
14
+ DataProcessingError,
15
+ extractSafePropertiesFromMessage,
16
+ } from "@fluidframework/container-utils";
17
+ import { DeltaScheduler } from "./deltaScheduler";
18
+ import { pkgVersion } from "./packageVersion";
19
+ import { latencyThreshold } from "./connectionTelemetry";
20
+
21
+ type IRuntimeMessageMetadata = undefined | {
22
+ batch?: boolean;
23
+ };
24
+
25
+ /**
26
+ * This class has the following responsibilities:
27
+ *
28
+ * 1. It tracks batches as we process ops and raises "batchBegin" and "batchEnd" events.
29
+ * As part of it, it validates batch correctness (i.e. no system ops in the middle of batch)
30
+ *
31
+ * 2. It creates instance of ScheduleManagerCore that ensures we never start processing ops from batch
32
+ * unless all ops of the batch are in.
33
+ */
34
+ export class ScheduleManager {
35
+ private readonly deltaScheduler: DeltaScheduler;
36
+ private batchClientId: string | undefined;
37
+ private hitError = false;
38
+
39
+ constructor(
40
+ private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
41
+ private readonly emitter: EventEmitter,
42
+ readonly getClientId: () => string | undefined,
43
+ private readonly logger: ITelemetryLogger,
44
+ ) {
45
+ this.deltaScheduler = new DeltaScheduler(
46
+ this.deltaManager,
47
+ ChildLogger.create(this.logger, "DeltaScheduler"),
48
+ );
49
+ void new ScheduleManagerCore(deltaManager, getClientId, logger);
50
+ }
51
+
52
+ public beforeOpProcessing(message: ISequencedDocumentMessage) {
53
+ if (this.batchClientId !== message.clientId) {
54
+ assert(this.batchClientId === undefined,
55
+ 0x2a2 /* "Batch is interrupted by other client op. Should be caught by trackPending()" */);
56
+
57
+ // This could be the beginning of a new batch or an individual message.
58
+ this.emitter.emit("batchBegin", message);
59
+ this.deltaScheduler.batchBegin(message);
60
+
61
+ const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
62
+ this.batchClientId = batch ? message.clientId : undefined;
63
+ }
64
+ }
65
+
66
+ public afterOpProcessing(error: any | undefined, message: ISequencedDocumentMessage) {
67
+ // If this is no longer true, we need to revisit what we do where we set this.hitError.
68
+ assert(!this.hitError, 0x2a3 /* "container should be closed on any error" */);
69
+
70
+ if (error) {
71
+ // We assume here that loader will close container and stop processing all future ops.
72
+ // This is implicit dependency. If this flow changes, this code might no longer be correct.
73
+ this.hitError = true;
74
+ this.batchClientId = undefined;
75
+ this.emitter.emit("batchEnd", error, message);
76
+ this.deltaScheduler.batchEnd(message);
77
+ return;
78
+ }
79
+
80
+ const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
81
+ // If no batchClientId has been set then we're in an individual batch. Else, if we get
82
+ // batch end metadata, this is end of the current batch.
83
+ if (this.batchClientId === undefined || batch === false) {
84
+ this.batchClientId = undefined;
85
+ this.emitter.emit("batchEnd", undefined, message);
86
+ this.deltaScheduler.batchEnd(message);
87
+ return;
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * This class controls pausing and resuming of inbound queue to ensure that we never
94
+ * start processing ops in a batch IF we do not have all ops in the batch.
95
+ */
96
+ class ScheduleManagerCore {
97
+ private pauseSequenceNumber: number | undefined;
98
+ private currentBatchClientId: string | undefined;
99
+ private localPaused = false;
100
+ private timePaused = 0;
101
+ private batchCount = 0;
102
+
103
+ constructor(
104
+ private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
105
+ private readonly getClientId: () => string | undefined,
106
+ private readonly logger: ITelemetryLogger,
107
+ ) {
108
+ // Listen for delta manager sends and add batch metadata to messages
109
+ this.deltaManager.on("prepareSend", (messages: IDocumentMessage[]) => {
110
+ if (messages.length === 0) {
111
+ return;
112
+ }
113
+
114
+ // First message will have the batch flag set to true if doing a batched send
115
+ const firstMessageMetadata = messages[0].metadata as IRuntimeMessageMetadata;
116
+ if (!firstMessageMetadata?.batch) {
117
+ return;
118
+ }
119
+
120
+ // If the batch contains only a single op, clear the batch flag.
121
+ if (messages.length === 1) {
122
+ delete firstMessageMetadata.batch;
123
+ return;
124
+ }
125
+
126
+ // Set the batch flag to false on the last message to indicate the end of the send batch
127
+ const lastMessage = messages[messages.length - 1];
128
+ lastMessage.metadata = { ...lastMessage.metadata, batch: false };
129
+ });
130
+
131
+ // Listen for updates and peek at the inbound
132
+ this.deltaManager.inbound.on(
133
+ "push",
134
+ (message: ISequencedDocumentMessage) => {
135
+ this.trackPending(message);
136
+ });
137
+
138
+ // Start with baseline - empty inbound queue.
139
+ assert(!this.localPaused, 0x293 /* "initial state" */);
140
+
141
+ const allPending = this.deltaManager.inbound.toArray();
142
+ for (const pending of allPending) {
143
+ this.trackPending(pending);
144
+ }
145
+
146
+ // We are intentionally directly listening to the "op" to inspect system ops as well.
147
+ // If we do not observe system ops, we are likely to hit 0x296 assert when system ops
148
+ // precedes start of incomplete batch.
149
+ this.deltaManager.on("op", (message) => this.afterOpProcessing(message.sequenceNumber));
150
+ }
151
+
152
+ /**
153
+ * The only public function in this class - called when we processed an op,
154
+ * to make decision if op processing should be paused or not after that.
155
+ */
156
+ public afterOpProcessing(sequenceNumber: number) {
157
+ assert(!this.localPaused, 0x294 /* "can't have op processing paused if we are processing an op" */);
158
+
159
+ // If the inbound queue is ever empty, nothing to do!
160
+ if (this.deltaManager.inbound.length === 0) {
161
+ assert(this.pauseSequenceNumber === undefined,
162
+ 0x295 /* "there should be no pending batch if we have no ops" */);
163
+ return;
164
+ }
165
+
166
+ // The queue is
167
+ // 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
168
+ // - here (processing ops until reaching start of incomplete batch)
169
+ // - in trackPending(), when queue was empty and start of batch showed up.
170
+ // 2. resumed when batch end comes in (in trackPending())
171
+
172
+ // do we have incomplete batch to worry about?
173
+ if (this.pauseSequenceNumber !== undefined) {
174
+ assert(sequenceNumber < this.pauseSequenceNumber,
175
+ 0x296 /* "we should never start processing incomplete batch!" */);
176
+ // If the next op is the start of incomplete batch, then we can't process it until it's fully in - pause!
177
+ if (sequenceNumber + 1 === this.pauseSequenceNumber) {
178
+ this.pauseQueue();
179
+ }
180
+ }
181
+ }
182
+
183
+ private pauseQueue() {
184
+ assert(!this.localPaused, 0x297 /* "always called from resumed state" */);
185
+ this.localPaused = true;
186
+ this.timePaused = performance.now();
187
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
188
+ this.deltaManager.inbound.pause();
189
+ }
190
+
191
+ private resumeQueue(startBatch: number, messageEndBatch: ISequencedDocumentMessage) {
192
+ const endBatch = messageEndBatch.sequenceNumber;
193
+ const duration = this.localPaused ? (performance.now() - this.timePaused) : undefined;
194
+
195
+ this.batchCount++;
196
+ if (this.batchCount % 1000 === 1) {
197
+ this.logger.sendTelemetryEvent({
198
+ eventName: "BatchStats",
199
+ sequenceNumber: endBatch,
200
+ length: endBatch - startBatch + 1,
201
+ msnDistance: endBatch - messageEndBatch.minimumSequenceNumber,
202
+ duration,
203
+ batchCount: this.batchCount,
204
+ interrupted: this.localPaused,
205
+ });
206
+ }
207
+
208
+ // Return early if no change in value
209
+ if (!this.localPaused) {
210
+ return;
211
+ }
212
+
213
+ this.localPaused = false;
214
+
215
+ // Random round number - we want to know when batch waiting paused op processing.
216
+ if (duration !== undefined && duration > latencyThreshold) {
217
+ this.logger.sendErrorEvent({
218
+ eventName: "MaxBatchWaitTimeExceeded",
219
+ duration,
220
+ sequenceNumber: endBatch,
221
+ length: endBatch - startBatch,
222
+ });
223
+ }
224
+ this.deltaManager.inbound.resume();
225
+ }
226
+
227
+ /**
228
+ * Called for each incoming op (i.e. inbound "push" notification)
229
+ */
230
+ private trackPending(message: ISequencedDocumentMessage) {
231
+ assert(this.deltaManager.inbound.length !== 0,
232
+ 0x298 /* "we have something in the queue that generates this event" */);
233
+
234
+ assert((this.currentBatchClientId === undefined) === (this.pauseSequenceNumber === undefined),
235
+ 0x299 /* "non-synchronized state" */);
236
+
237
+ const metadata = message.metadata as IRuntimeMessageMetadata;
238
+ const batchMetadata = metadata?.batch;
239
+
240
+ // Protocol messages are never part of a runtime batch of messages
241
+ if (!isRuntimeMessage(message)) {
242
+ // Protocol messages should never show up in the middle of the batch!
243
+ if (this.currentBatchClientId !== undefined) {
244
+ throw DataProcessingError.create(
245
+ "Received a system message during batch processing", // Formerly known as assert 0x29a
246
+ "trackPending",
247
+ message,
248
+ {
249
+ runtimeVersion: pkgVersion,
250
+ batchClientId: this.currentBatchClientId,
251
+ pauseSequenceNumber: this.pauseSequenceNumber,
252
+ localBatch: this.currentBatchClientId === this.getClientId(),
253
+ messageType: message.type,
254
+ });
255
+ }
256
+
257
+ assert(batchMetadata === undefined, 0x29b /* "system op in a batch?" */);
258
+ assert(!this.localPaused, 0x29c /* "we should be processing ops when there is no active batch" */);
259
+ return;
260
+ }
261
+
262
+ if (this.currentBatchClientId === undefined && batchMetadata === undefined) {
263
+ assert(!this.localPaused, 0x29d /* "we should be processing ops when there is no active batch" */);
264
+ return;
265
+ }
266
+
267
+ // If the client ID changes then we can move the pause point. If it stayed the same then we need to check.
268
+ // If batchMetadata is not undefined then if it's true we've begun a new batch - if false we've ended
269
+ // the previous one
270
+ if (this.currentBatchClientId !== undefined || batchMetadata === false) {
271
+ if (this.currentBatchClientId !== message.clientId) {
272
+ // "Batch not closed, yet message from another client!"
273
+ throw new DataCorruptionError(
274
+ "OpBatchIncomplete",
275
+ {
276
+ runtimeVersion: pkgVersion,
277
+ batchClientId: this.currentBatchClientId,
278
+ pauseSequenceNumber: this.pauseSequenceNumber,
279
+ localBatch: this.currentBatchClientId === this.getClientId(),
280
+ localMessage: message.clientId === this.getClientId(),
281
+ ...extractSafePropertiesFromMessage(message),
282
+ });
283
+ }
284
+ }
285
+
286
+ // The queue is
287
+ // 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
288
+ // - in afterOpProcessing() - processing ops until reaching start of incomplete batch
289
+ // - here (batchMetadata == false below), when queue was empty and start of batch showed up.
290
+ // 2. resumed when batch end comes in (batchMetadata === true case below)
291
+
292
+ if (batchMetadata) {
293
+ assert(this.currentBatchClientId === undefined, 0x29e /* "there can't be active batch" */);
294
+ assert(!this.localPaused, 0x29f /* "we should be processing ops when there is no active batch" */);
295
+ this.pauseSequenceNumber = message.sequenceNumber;
296
+ this.currentBatchClientId = message.clientId;
297
+ // Start of the batch
298
+ // Only pause processing if queue has no other ops!
299
+ // If there are any other ops in the queue, processing will be stopped when they are processed!
300
+ if (this.deltaManager.inbound.length === 1) {
301
+ this.pauseQueue();
302
+ }
303
+ } else if (batchMetadata === false) {
304
+ assert(this.pauseSequenceNumber !== undefined, 0x2a0 /* "batch presence was validated above" */);
305
+ // Batch is complete, we can process it!
306
+ this.resumeQueue(this.pauseSequenceNumber, message);
307
+ this.pauseSequenceNumber = undefined;
308
+ this.currentBatchClientId = undefined;
309
+ } else {
310
+ // Continuation of current batch. Do nothing
311
+ assert(this.currentBatchClientId !== undefined, 0x2a1 /* "logic error" */);
312
+ }
313
+ }
314
+ }
package/src/summarizer.ts CHANGED
@@ -23,9 +23,6 @@ import {
23
23
  IFluidHandle,
24
24
  IRequest,
25
25
  } from "@fluidframework/core-interfaces";
26
- import {
27
- ISequencedDocumentMessage,
28
- } from "@fluidframework/protocol-definitions";
29
26
  import { ISummaryConfiguration } from "./containerRuntime";
30
27
  import { ICancellableSummarizerController } from "./runWhileConnectedCoordinator";
31
28
  import { summarizerClientType } from "./summarizerClientElection";
@@ -76,8 +73,6 @@ export class Summarizer extends EventEmitter implements ISummarizer {
76
73
 
77
74
  private readonly logger: ITelemetryLogger;
78
75
  private runningSummarizer?: RunningSummarizer;
79
- private systemOpListener?: (op: ISequencedDocumentMessage) => void;
80
- private opListener?: (error: any, op: ISequencedDocumentMessage) => void;
81
76
  private _disposed: boolean = false;
82
77
  private starting: boolean = false;
83
78
 
@@ -283,6 +278,7 @@ export class Summarizer extends EventEmitter implements ISummarizer {
283
278
  this.summaryCollection,
284
279
  runCoordinator /* cancellationToken */,
285
280
  (reason) => runCoordinator.stop(reason), /* stopSummarizerCallback */
281
+ this.runtime,
286
282
  );
287
283
  this.runningSummarizer = runningSummarizer;
288
284
  this.starting = false;
@@ -293,13 +289,6 @@ export class Summarizer extends EventEmitter implements ISummarizer {
293
289
  this.logger.sendErrorEvent({ eventName: "HandleSummaryAckFatalError" }, error);
294
290
  });
295
291
 
296
- // Listen for ops
297
- this.systemOpListener = (op: ISequencedDocumentMessage) => runningSummarizer.handleSystemOp(op);
298
- this.runtime.deltaManager.inbound.on("op", this.systemOpListener);
299
-
300
- this.opListener = (error: any, op: ISequencedDocumentMessage) => runningSummarizer.handleOp(error, op);
301
- this.runtime.on("batchEnd", this.opListener);
302
-
303
292
  return runningSummarizer;
304
293
  }
305
294
 
@@ -318,12 +307,6 @@ export class Summarizer extends EventEmitter implements ISummarizer {
318
307
  this.runningSummarizer.dispose();
319
308
  this.runningSummarizer = undefined;
320
309
  }
321
- if (this.systemOpListener) {
322
- this.runtime.deltaManager.inbound.off("op", this.systemOpListener);
323
- }
324
- if (this.opListener) {
325
- this.runtime.removeListener("batchEnd", this.opListener);
326
- }
327
310
  }
328
311
 
329
312
  public readonly summarizeOnDemand: ISummarizer["summarizeOnDemand"] = (...args) => {
@@ -6,11 +6,11 @@
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
7
  import { Timer } from "@fluidframework/common-utils";
8
8
  import { ISummaryConfigurationHeuristics } from "./containerRuntime";
9
-
10
9
  import {
11
10
  ISummarizeHeuristicData,
12
11
  ISummarizeHeuristicRunner,
13
12
  ISummarizeAttempt,
13
+ ISummaryHeuristicStrategy,
14
14
  } from "./summarizerTypes";
15
15
  import { SummarizeReason } from "./summaryGenerator";
16
16
 
@@ -26,6 +26,30 @@ export class SummarizeHeuristicData implements ISummarizeHeuristicData {
26
26
  return this._lastSuccessfulSummary;
27
27
  }
28
28
 
29
+ public numNonRuntimeOps: number = 0;
30
+ public totalOpsSize: number = 0;
31
+ public hasMissingOpData: boolean = false;
32
+
33
+ /**
34
+ * Cumulative size in bytes of all the ops at the beginning of the summarization attempt.
35
+ * Is used to adjust totalOpsSize appropriately after successful summarization.
36
+ */
37
+ /** */
38
+ private totalOpsSizeBefore: number = 0;
39
+
40
+ /**
41
+ * Number of system ops at beginning of attempting to summarize.
42
+ * Is used to adjust numSystemOps appropriately after successful summarization.
43
+ */
44
+ private numSystemOpsBefore: number = 0;
45
+
46
+ public numRuntimeOps: number = 0;
47
+ /**
48
+ * Number of non-system ops at beginning of attempting to summarize.
49
+ * Is used to adjust numNonSystemOps appropriately after successful summarization.
50
+ */
51
+ private numNonSystemOpsBefore: number = 0;
52
+
29
53
  constructor(
30
54
  public lastOpSequenceNumber: number,
31
55
  /** Baseline attempt data used for comparisons with subsequent attempts/calculations. */
@@ -45,10 +69,23 @@ export class SummarizeHeuristicData implements ISummarizeHeuristicData {
45
69
  refSequenceNumber: refSequenceNumber ?? this.lastOpSequenceNumber,
46
70
  summaryTime: Date.now(),
47
71
  };
72
+
73
+ this.numSystemOpsBefore = this.numNonRuntimeOps;
74
+ this.numNonSystemOpsBefore = this.numRuntimeOps;
75
+ this.totalOpsSizeBefore = this.totalOpsSize;
48
76
  }
49
77
 
50
78
  public markLastAttemptAsSuccessful() {
51
79
  this._lastSuccessfulSummary = { ...this.lastAttempt };
80
+
81
+ this.numNonRuntimeOps -= this.numSystemOpsBefore;
82
+ this.numSystemOpsBefore = 0;
83
+
84
+ this.numRuntimeOps -= this.numNonSystemOpsBefore;
85
+ this.numNonSystemOpsBefore = 0;
86
+
87
+ this.totalOpsSize -= this.totalOpsSizeBefore;
88
+ this.totalOpsSizeBefore = 0;
52
89
  }
53
90
  }
54
91
 
@@ -56,42 +93,74 @@ export class SummarizeHeuristicData implements ISummarizeHeuristicData {
56
93
  * This class contains the heuristics for when to summarize.
57
94
  */
58
95
  export class SummarizeHeuristicRunner implements ISummarizeHeuristicRunner {
59
- private readonly idleTimer: Timer;
60
- private readonly minOpsForLastSummaryAttempt: number;
96
+ private readonly idleTimer: Timer | undefined;
97
+ private readonly runSummarize: (reason: SummarizeReason) => void;
61
98
 
62
99
  public constructor(
63
100
  private readonly heuristicData: ISummarizeHeuristicData,
64
101
  private readonly configuration: ISummaryConfigurationHeuristics,
65
- private readonly trySummarize: (reason: SummarizeReason) => void,
102
+ trySummarize: (reason: SummarizeReason) => void,
66
103
  private readonly logger: ITelemetryLogger,
104
+ private readonly summarizeStrategies: ISummaryHeuristicStrategy[] = getDefaultSummaryHeuristicStrategies(),
67
105
  ) {
68
106
  this.idleTimer = new Timer(
69
- this.configuration.idleTime,
70
- () => this.trySummarize("idle"));
71
- this.minOpsForLastSummaryAttempt = this.configuration.minOpsForLastSummaryAttempt;
107
+ this.idleTime,
108
+ () => this.runSummarize("idle"));
109
+
110
+ this.runSummarize = (reason: SummarizeReason) => {
111
+ this.idleTimer?.clear();
112
+
113
+ // We shouldn't attempt a summary if there are no new processed ops
114
+ const opsSinceLastAck = this.opsSinceLastAck;
115
+ if (opsSinceLastAck > 0) {
116
+ trySummarize(reason);
117
+ }
118
+ };
119
+ }
120
+
121
+ public get idleTime(): number {
122
+ if (this.configuration.idleTime !== undefined) {
123
+ return this.configuration.idleTime;
124
+ }
125
+ const maxIdleTime = this.configuration.maxIdleTime;
126
+ const minIdleTime = this.configuration.minIdleTime;
127
+ const weightedNumOfOps = getWeightedNumberOfOps(
128
+ this.heuristicData.numRuntimeOps,
129
+ this.heuristicData.numNonRuntimeOps,
130
+ this.configuration.runtimeOpWeight,
131
+ this.configuration.nonRuntimeOpWeight,
132
+ );
133
+ const pToMaxOps = weightedNumOfOps * 1.0 / this.configuration.maxOps;
134
+
135
+ if (pToMaxOps >= 1) {
136
+ return minIdleTime;
137
+ }
138
+
139
+ // Return a ratioed idle time based on the percentage of ops
140
+ return maxIdleTime - ((maxIdleTime - minIdleTime) * pToMaxOps);
72
141
  }
73
142
 
74
143
  public get opsSinceLastAck(): number {
75
144
  return this.heuristicData.lastOpSequenceNumber - this.heuristicData.lastSuccessfulSummary.refSequenceNumber;
76
145
  }
77
146
 
147
+ public start() {
148
+ this.idleTimer?.start(this.idleTime);
149
+ }
150
+
78
151
  public run() {
79
- const timeSinceLastSummary = Date.now() - this.heuristicData.lastSuccessfulSummary.summaryTime;
80
- const opsSinceLastAck = this.opsSinceLastAck;
81
- if (timeSinceLastSummary > this.configuration.maxTime) {
82
- this.idleTimer.clear();
83
- this.trySummarize("maxTime");
84
- } else if (opsSinceLastAck > this.configuration.maxOps) {
85
- this.idleTimer.clear();
86
- this.trySummarize("maxOps");
87
- } else {
88
- this.idleTimer.restart();
152
+ for (const strategy of this.summarizeStrategies) {
153
+ if (strategy.shouldRunSummary(this.configuration, this.heuristicData)) {
154
+ return this.runSummarize(strategy.summarizeReason);
155
+ }
89
156
  }
157
+
158
+ this.idleTimer?.restart(this.idleTime);
90
159
  }
91
160
 
92
161
  public shouldRunLastSummary(): boolean {
93
162
  const opsSinceLastAck = this.opsSinceLastAck;
94
- const minOpsForLastSummaryAttempt = this.minOpsForLastSummaryAttempt;
163
+ const minOpsForLastSummaryAttempt = this.configuration.minOpsForLastSummaryAttempt;
95
164
 
96
165
  this.logger.sendTelemetryEvent({
97
166
  eventName: "ShouldRunLastSummary",
@@ -103,6 +172,54 @@ export class SummarizeHeuristicRunner implements ISummarizeHeuristicRunner {
103
172
  }
104
173
 
105
174
  public dispose() {
106
- this.idleTimer.clear();
175
+ this.idleTimer?.clear();
176
+ }
177
+ }
178
+
179
+ /** Strategy used to run a summary when it's been a while since our last successful summary */
180
+ class MaxTimeSummaryHeuristicStrategy implements ISummaryHeuristicStrategy {
181
+ public readonly summarizeReason: Readonly<SummarizeReason> = "maxTime";
182
+
183
+ public shouldRunSummary(
184
+ configuration: ISummaryConfigurationHeuristics,
185
+ heuristicData: ISummarizeHeuristicData,
186
+ ): boolean {
187
+ const timeSinceLastSummary = Date.now() - heuristicData.lastSuccessfulSummary.summaryTime;
188
+ return timeSinceLastSummary > configuration.maxTime;
107
189
  }
108
190
  }
191
+
192
+ function getWeightedNumberOfOps(
193
+ runtimeOpCount: number,
194
+ nonRuntimeOpCount: number,
195
+ runtimeOpWeight: number,
196
+ nonRuntimeOpWeight: number,
197
+ ): number {
198
+ return (runtimeOpWeight * runtimeOpCount)
199
+ + (nonRuntimeOpWeight * nonRuntimeOpCount);
200
+ }
201
+
202
+ /** Strategy used to do a weighted analysis on the ops we've processed since the last successful summary */
203
+ class WeightedOpsSummaryHeuristicStrategy implements ISummaryHeuristicStrategy {
204
+ public readonly summarizeReason: Readonly<SummarizeReason> = "maxOps";
205
+
206
+ public shouldRunSummary(
207
+ configuration: ISummaryConfigurationHeuristics,
208
+ heuristicData: ISummarizeHeuristicData,
209
+ ): boolean {
210
+ const weightedNumOfOps = getWeightedNumberOfOps(
211
+ heuristicData.numRuntimeOps,
212
+ heuristicData.numNonRuntimeOps,
213
+ configuration.runtimeOpWeight,
214
+ configuration.nonRuntimeOpWeight,
215
+ );
216
+ return weightedNumOfOps > configuration.maxOps;
217
+ }
218
+ }
219
+
220
+ function getDefaultSummaryHeuristicStrategies() {
221
+ return [
222
+ new MaxTimeSummaryHeuristicStrategy(),
223
+ new WeightedOpsSummaryHeuristicStrategy(),
224
+ ];
225
+ }