@fluidframework/container-runtime 2.0.0-internal.1.0.0.82693 → 2.0.0-internal.1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/.mocharc.js +12 -0
  2. package/dist/batchTracker.js +1 -1
  3. package/dist/batchTracker.js.map +1 -1
  4. package/dist/blobManager.d.ts +7 -1
  5. package/dist/blobManager.d.ts.map +1 -1
  6. package/dist/blobManager.js +34 -17
  7. package/dist/blobManager.js.map +1 -1
  8. package/dist/containerRuntime.d.ts +3 -104
  9. package/dist/containerRuntime.d.ts.map +1 -1
  10. package/dist/containerRuntime.js +83 -395
  11. package/dist/containerRuntime.js.map +1 -1
  12. package/dist/dataStore.d.ts +1 -1
  13. package/dist/dataStore.d.ts.map +1 -1
  14. package/dist/dataStore.js +2 -3
  15. package/dist/dataStore.js.map +1 -1
  16. package/dist/dataStoreContext.d.ts +3 -5
  17. package/dist/dataStoreContext.d.ts.map +1 -1
  18. package/dist/dataStoreContext.js +13 -23
  19. package/dist/dataStoreContext.js.map +1 -1
  20. package/dist/dataStores.d.ts +1 -1
  21. package/dist/dataStores.d.ts.map +1 -1
  22. package/dist/dataStores.js +3 -8
  23. package/dist/dataStores.js.map +1 -1
  24. package/dist/garbageCollection.d.ts +37 -6
  25. package/dist/garbageCollection.d.ts.map +1 -1
  26. package/dist/garbageCollection.js +61 -65
  27. package/dist/garbageCollection.js.map +1 -1
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/packageVersion.d.ts +1 -1
  33. package/dist/packageVersion.d.ts.map +1 -1
  34. package/dist/packageVersion.js +1 -1
  35. package/dist/packageVersion.js.map +1 -1
  36. package/dist/pendingStateManager.d.ts.map +1 -1
  37. package/dist/pendingStateManager.js +15 -2
  38. package/dist/pendingStateManager.js.map +1 -1
  39. package/dist/runningSummarizer.d.ts +14 -0
  40. package/dist/runningSummarizer.d.ts.map +1 -1
  41. package/dist/runningSummarizer.js +25 -0
  42. package/dist/runningSummarizer.js.map +1 -1
  43. package/dist/scheduleManager.d.ts +28 -0
  44. package/dist/scheduleManager.d.ts.map +1 -0
  45. package/dist/scheduleManager.js +235 -0
  46. package/dist/scheduleManager.js.map +1 -0
  47. package/dist/summarizer.d.ts.map +1 -1
  48. package/dist/summarizer.js +33 -3
  49. package/dist/summarizer.js.map +1 -1
  50. package/dist/summaryCollection.js +1 -1
  51. package/dist/summaryCollection.js.map +1 -1
  52. package/dist/summaryGenerator.js +1 -1
  53. package/dist/summaryGenerator.js.map +1 -1
  54. package/dist/summaryManager.d.ts.map +1 -1
  55. package/dist/summaryManager.js +20 -5
  56. package/dist/summaryManager.js.map +1 -1
  57. package/lib/batchTracker.js +1 -1
  58. package/lib/batchTracker.js.map +1 -1
  59. package/lib/blobManager.d.ts +7 -1
  60. package/lib/blobManager.d.ts.map +1 -1
  61. package/lib/blobManager.js +35 -18
  62. package/lib/blobManager.js.map +1 -1
  63. package/lib/containerRuntime.d.ts +3 -104
  64. package/lib/containerRuntime.d.ts.map +1 -1
  65. package/lib/containerRuntime.js +84 -395
  66. package/lib/containerRuntime.js.map +1 -1
  67. package/lib/dataStore.d.ts +1 -1
  68. package/lib/dataStore.d.ts.map +1 -1
  69. package/lib/dataStore.js +2 -3
  70. package/lib/dataStore.js.map +1 -1
  71. package/lib/dataStoreContext.d.ts +3 -5
  72. package/lib/dataStoreContext.d.ts.map +1 -1
  73. package/lib/dataStoreContext.js +13 -23
  74. package/lib/dataStoreContext.js.map +1 -1
  75. package/lib/dataStores.d.ts +1 -1
  76. package/lib/dataStores.d.ts.map +1 -1
  77. package/lib/dataStores.js +3 -8
  78. package/lib/dataStores.js.map +1 -1
  79. package/lib/garbageCollection.d.ts +37 -6
  80. package/lib/garbageCollection.d.ts.map +1 -1
  81. package/lib/garbageCollection.js +47 -52
  82. package/lib/garbageCollection.js.map +1 -1
  83. package/lib/index.d.ts +2 -1
  84. package/lib/index.d.ts.map +1 -1
  85. package/lib/index.js +2 -1
  86. package/lib/index.js.map +1 -1
  87. package/lib/packageVersion.d.ts +1 -1
  88. package/lib/packageVersion.d.ts.map +1 -1
  89. package/lib/packageVersion.js +1 -1
  90. package/lib/packageVersion.js.map +1 -1
  91. package/lib/pendingStateManager.d.ts.map +1 -1
  92. package/lib/pendingStateManager.js +15 -2
  93. package/lib/pendingStateManager.js.map +1 -1
  94. package/lib/runningSummarizer.d.ts +14 -0
  95. package/lib/runningSummarizer.d.ts.map +1 -1
  96. package/lib/runningSummarizer.js +25 -0
  97. package/lib/runningSummarizer.js.map +1 -1
  98. package/lib/scheduleManager.d.ts +28 -0
  99. package/lib/scheduleManager.d.ts.map +1 -0
  100. package/lib/scheduleManager.js +231 -0
  101. package/lib/scheduleManager.js.map +1 -0
  102. package/lib/summarizer.d.ts.map +1 -1
  103. package/lib/summarizer.js +35 -5
  104. package/lib/summarizer.js.map +1 -1
  105. package/lib/summaryCollection.js +1 -1
  106. package/lib/summaryCollection.js.map +1 -1
  107. package/lib/summaryGenerator.js +1 -1
  108. package/lib/summaryGenerator.js.map +1 -1
  109. package/lib/summaryManager.d.ts.map +1 -1
  110. package/lib/summaryManager.js +20 -5
  111. package/lib/summaryManager.js.map +1 -1
  112. package/package.json +32 -19
  113. package/src/batchTracker.ts +1 -1
  114. package/src/blobManager.ts +43 -17
  115. package/src/containerRuntime.ts +113 -547
  116. package/src/dataStore.ts +1 -4
  117. package/src/dataStoreContext.ts +10 -25
  118. package/src/dataStores.ts +13 -19
  119. package/src/garbageCollection.ts +64 -69
  120. package/src/index.ts +1 -2
  121. package/src/packageVersion.ts +1 -1
  122. package/src/pendingStateManager.ts +18 -2
  123. package/src/runningSummarizer.ts +33 -1
  124. package/src/scheduleManager.ts +294 -0
  125. package/src/summarizer.ts +46 -10
  126. package/src/summaryCollection.ts +1 -1
  127. package/src/summaryGenerator.ts +1 -1
  128. package/src/summaryManager.ts +20 -5
@@ -0,0 +1,294 @@
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 { isUnpackedRuntimeMessage } from "@fluidframework/driver-utils";
12
+ import { DataCorruptionError, extractSafePropertiesFromMessage } from "@fluidframework/container-utils";
13
+ import { DeltaScheduler } from "./deltaScheduler";
14
+ import { pkgVersion } from "./packageVersion";
15
+ import { latencyThreshold } from "./connectionTelemetry";
16
+
17
+ type IRuntimeMessageMetadata = undefined | {
18
+ batch?: boolean;
19
+ };
20
+
21
+ /**
22
+ * This class has the following responsibilities:
23
+ * 1. It tracks batches as we process ops and raises "batchBegin" and "batchEnd" events.
24
+ * As part of it, it validates batch correctness (i.e. no system ops in the middle of batch)
25
+ * 2. It creates instance of ScheduleManagerCore that ensures we never start processing ops from batch
26
+ * unless all ops of the batch are in.
27
+ */
28
+ export class ScheduleManager {
29
+ private readonly deltaScheduler: DeltaScheduler;
30
+ private batchClientId: string | undefined;
31
+ private hitError = false;
32
+
33
+ constructor(
34
+ private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
35
+ private readonly emitter: EventEmitter,
36
+ private readonly logger: ITelemetryLogger,
37
+ ) {
38
+ this.deltaScheduler = new DeltaScheduler(
39
+ this.deltaManager,
40
+ ChildLogger.create(this.logger, "DeltaScheduler"),
41
+ );
42
+ void new ScheduleManagerCore(deltaManager, logger);
43
+ }
44
+
45
+ public beforeOpProcessing(message: ISequencedDocumentMessage) {
46
+ if (this.batchClientId !== message.clientId) {
47
+ assert(this.batchClientId === undefined,
48
+ 0x2a2 /* "Batch is interrupted by other client op. Should be caught by trackPending()" */);
49
+
50
+ // This could be the beginning of a new batch or an individual message.
51
+ this.emitter.emit("batchBegin", message);
52
+ this.deltaScheduler.batchBegin(message);
53
+
54
+ const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
55
+ if (batch) {
56
+ this.batchClientId = message.clientId;
57
+ } else {
58
+ this.batchClientId = undefined;
59
+ }
60
+ }
61
+ }
62
+
63
+ public afterOpProcessing(error: any | undefined, message: ISequencedDocumentMessage) {
64
+ // If this is no longer true, we need to revisit what we do where we set this.hitError.
65
+ assert(!this.hitError, 0x2a3 /* "container should be closed on any error" */);
66
+
67
+ if (error) {
68
+ // We assume here that loader will close container and stop processing all future ops.
69
+ // This is implicit dependency. If this flow changes, this code might no longer be correct.
70
+ this.hitError = true;
71
+ this.batchClientId = undefined;
72
+ this.emitter.emit("batchEnd", error, message);
73
+ this.deltaScheduler.batchEnd(message);
74
+ return;
75
+ }
76
+
77
+ const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
78
+ // If no batchClientId has been set then we're in an individual batch. Else, if we get
79
+ // batch end metadata, this is end of the current batch.
80
+ if (this.batchClientId === undefined || batch === false) {
81
+ this.batchClientId = undefined;
82
+ this.emitter.emit("batchEnd", undefined, message);
83
+ this.deltaScheduler.batchEnd(message);
84
+ return;
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * This class controls pausing and resuming of inbound queue to ensure that we never
91
+ * start processing ops in a batch IF we do not have all ops in the batch.
92
+ */
93
+ class ScheduleManagerCore {
94
+ private pauseSequenceNumber: number | undefined;
95
+ private currentBatchClientId: string | undefined;
96
+ private localPaused = false;
97
+ private timePaused = 0;
98
+ private batchCount = 0;
99
+
100
+ constructor(
101
+ private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
102
+ private readonly logger: ITelemetryLogger,
103
+ ) {
104
+ // Listen for delta manager sends and add batch metadata to messages
105
+ this.deltaManager.on("prepareSend", (messages: IDocumentMessage[]) => {
106
+ if (messages.length === 0) {
107
+ return;
108
+ }
109
+
110
+ // First message will have the batch flag set to true if doing a batched send
111
+ const firstMessageMetadata = messages[0].metadata as IRuntimeMessageMetadata;
112
+ if (!firstMessageMetadata?.batch) {
113
+ return;
114
+ }
115
+
116
+ // If the batch contains only a single op, clear the batch flag.
117
+ if (messages.length === 1) {
118
+ delete firstMessageMetadata.batch;
119
+ return;
120
+ }
121
+
122
+ // Set the batch flag to false on the last message to indicate the end of the send batch
123
+ const lastMessage = messages[messages.length - 1];
124
+ lastMessage.metadata = { ...lastMessage.metadata, batch: false };
125
+ });
126
+
127
+ // Listen for updates and peek at the inbound
128
+ this.deltaManager.inbound.on(
129
+ "push",
130
+ (message: ISequencedDocumentMessage) => {
131
+ this.trackPending(message);
132
+ });
133
+
134
+ // Start with baseline - empty inbound queue.
135
+ assert(!this.localPaused, 0x293 /* "initial state" */);
136
+
137
+ const allPending = this.deltaManager.inbound.toArray();
138
+ for (const pending of allPending) {
139
+ this.trackPending(pending);
140
+ }
141
+
142
+ // We are intentionally directly listening to the "op" to inspect system ops as well.
143
+ // If we do not observe system ops, we are likely to hit 0x296 assert when system ops
144
+ // precedes start of incomplete batch.
145
+ this.deltaManager.on("op", (message) => this.afterOpProcessing(message.sequenceNumber));
146
+ }
147
+
148
+ /**
149
+ * The only public function in this class - called when we processed an op,
150
+ * to make decision if op processing should be paused or not after that.
151
+ */
152
+ public afterOpProcessing(sequenceNumber: number) {
153
+ assert(!this.localPaused, 0x294 /* "can't have op processing paused if we are processing an op" */);
154
+
155
+ // If the inbound queue is ever empty, nothing to do!
156
+ if (this.deltaManager.inbound.length === 0) {
157
+ assert(this.pauseSequenceNumber === undefined,
158
+ 0x295 /* "there should be no pending batch if we have no ops" */);
159
+ return;
160
+ }
161
+
162
+ // The queue is
163
+ // 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
164
+ // - here (processing ops until reaching start of incomplete batch)
165
+ // - in trackPending(), when queue was empty and start of batch showed up.
166
+ // 2. resumed when batch end comes in (in trackPending())
167
+
168
+ // do we have incomplete batch to worry about?
169
+ if (this.pauseSequenceNumber !== undefined) {
170
+ assert(sequenceNumber < this.pauseSequenceNumber,
171
+ 0x296 /* "we should never start processing incomplete batch!" */);
172
+ // If the next op is the start of incomplete batch, then we can't process it until it's fully in - pause!
173
+ if (sequenceNumber + 1 === this.pauseSequenceNumber) {
174
+ this.pauseQueue();
175
+ }
176
+ }
177
+ }
178
+
179
+ private pauseQueue() {
180
+ assert(!this.localPaused, 0x297 /* "always called from resumed state" */);
181
+ this.localPaused = true;
182
+ this.timePaused = performance.now();
183
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
184
+ this.deltaManager.inbound.pause();
185
+ }
186
+
187
+ private resumeQueue(startBatch: number, messageEndBatch: ISequencedDocumentMessage) {
188
+ const endBatch = messageEndBatch.sequenceNumber;
189
+ const duration = this.localPaused ? (performance.now() - this.timePaused) : undefined;
190
+
191
+ this.batchCount++;
192
+ if (this.batchCount % 1000 === 1) {
193
+ this.logger.sendTelemetryEvent({
194
+ eventName: "BatchStats",
195
+ sequenceNumber: endBatch,
196
+ length: endBatch - startBatch + 1,
197
+ msnDistance: endBatch - messageEndBatch.minimumSequenceNumber,
198
+ duration,
199
+ batchCount: this.batchCount,
200
+ interrupted: this.localPaused,
201
+ });
202
+ }
203
+
204
+ // Return early if no change in value
205
+ if (!this.localPaused) {
206
+ return;
207
+ }
208
+
209
+ this.localPaused = false;
210
+
211
+ // Random round number - we want to know when batch waiting paused op processing.
212
+ if (duration !== undefined && duration > latencyThreshold) {
213
+ this.logger.sendErrorEvent({
214
+ eventName: "MaxBatchWaitTimeExceeded",
215
+ duration,
216
+ sequenceNumber: endBatch,
217
+ length: endBatch - startBatch,
218
+ });
219
+ }
220
+ this.deltaManager.inbound.resume();
221
+ }
222
+
223
+ /**
224
+ * Called for each incoming op (i.e. inbound "push" notification)
225
+ */
226
+ private trackPending(message: ISequencedDocumentMessage) {
227
+ assert(this.deltaManager.inbound.length !== 0,
228
+ 0x298 /* "we have something in the queue that generates this event" */);
229
+
230
+ assert((this.currentBatchClientId === undefined) === (this.pauseSequenceNumber === undefined),
231
+ 0x299 /* "non-synchronized state" */);
232
+
233
+ const metadata = message.metadata as IRuntimeMessageMetadata;
234
+ const batchMetadata = metadata?.batch;
235
+
236
+ // Protocol messages are never part of a runtime batch of messages
237
+ if (!isUnpackedRuntimeMessage(message)) {
238
+ // Protocol messages should never show up in the middle of the batch!
239
+ assert(this.currentBatchClientId === undefined, 0x29a /* "System message in the middle of batch!" */);
240
+ assert(batchMetadata === undefined, 0x29b /* "system op in a batch?" */);
241
+ assert(!this.localPaused, 0x29c /* "we should be processing ops when there is no active batch" */);
242
+ return;
243
+ }
244
+
245
+ if (this.currentBatchClientId === undefined && batchMetadata === undefined) {
246
+ assert(!this.localPaused, 0x29d /* "we should be processing ops when there is no active batch" */);
247
+ return;
248
+ }
249
+
250
+ // If the client ID changes then we can move the pause point. If it stayed the same then we need to check.
251
+ // If batchMetadata is not undefined then if it's true we've begun a new batch - if false we've ended
252
+ // the previous one
253
+ if (this.currentBatchClientId !== undefined || batchMetadata === false) {
254
+ if (this.currentBatchClientId !== message.clientId) {
255
+ // "Batch not closed, yet message from another client!"
256
+ throw new DataCorruptionError(
257
+ "OpBatchIncomplete",
258
+ {
259
+ runtimeVersion: pkgVersion,
260
+ batchClientId: this.currentBatchClientId,
261
+ ...extractSafePropertiesFromMessage(message),
262
+ });
263
+ }
264
+ }
265
+
266
+ // The queue is
267
+ // 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
268
+ // - in afterOpProcessing() - processing ops until reaching start of incomplete batch
269
+ // - here (batchMetadata == false below), when queue was empty and start of batch showed up.
270
+ // 2. resumed when batch end comes in (batchMetadata === true case below)
271
+
272
+ if (batchMetadata) {
273
+ assert(this.currentBatchClientId === undefined, 0x29e /* "there can't be active batch" */);
274
+ assert(!this.localPaused, 0x29f /* "we should be processing ops when there is no active batch" */);
275
+ this.pauseSequenceNumber = message.sequenceNumber;
276
+ this.currentBatchClientId = message.clientId;
277
+ // Start of the batch
278
+ // Only pause processing if queue has no other ops!
279
+ // If there are any other ops in the queue, processing will be stopped when they are processed!
280
+ if (this.deltaManager.inbound.length === 1) {
281
+ this.pauseQueue();
282
+ }
283
+ } else if (batchMetadata === false) {
284
+ assert(this.pauseSequenceNumber !== undefined, 0x2a0 /* "batch presence was validated above" */);
285
+ // Batch is complete, we can process it!
286
+ this.resumeQueue(this.pauseSequenceNumber, message);
287
+ this.pauseSequenceNumber = undefined;
288
+ this.currentBatchClientId = undefined;
289
+ } else {
290
+ // Continuation of current batch. Do nothing
291
+ assert(this.currentBatchClientId !== undefined, 0x2a1 /* "logic error" */);
292
+ }
293
+ }
294
+ }
package/src/summarizer.ts CHANGED
@@ -8,9 +8,15 @@ import { Deferred } from "@fluidframework/common-utils";
8
8
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
9
9
  import { ILoader, LoaderHeader } from "@fluidframework/container-definitions";
10
10
  import { UsageError } from "@fluidframework/container-utils";
11
- import { DriverHeader } from "@fluidframework/driver-definitions";
11
+ import { DriverErrorType, DriverHeader } from "@fluidframework/driver-definitions";
12
12
  import { requestFluidObject } from "@fluidframework/runtime-utils";
13
- import { ChildLogger, IFluidErrorBase, LoggingError, wrapErrorAndLog } from "@fluidframework/telemetry-utils";
13
+ import {
14
+ ChildLogger,
15
+ IFluidErrorBase,
16
+ isFluidError,
17
+ LoggingError,
18
+ wrapErrorAndLog,
19
+ } from "@fluidframework/telemetry-utils";
14
20
  import {
15
21
  FluidObject,
16
22
  IFluidHandleContext,
@@ -20,7 +26,7 @@ import {
20
26
  import { ISummaryConfiguration } from "./containerRuntime";
21
27
  import { ICancellableSummarizerController } from "./runWhileConnectedCoordinator";
22
28
  import { summarizerClientType } from "./summarizerClientElection";
23
- import { SummaryCollection } from "./summaryCollection";
29
+ import { IAckedSummary, SummaryCollection } from "./summaryCollection";
24
30
  import { SummarizerHandle } from "./summarizerHandle";
25
31
  import { RunningSummarizer } from "./runningSummarizer";
26
32
  import {
@@ -361,22 +367,52 @@ export class Summarizer extends EventEmitter implements ISummarizer {
361
367
 
362
368
  private async handleSummaryAcks() {
363
369
  let refSequenceNumber = this.runtime.deltaManager.initialSequenceNumber;
370
+ let ack: IAckedSummary | undefined;
364
371
  while (this.runningSummarizer) {
365
372
  const summaryLogger = this.runningSummarizer.tryGetCorrelatedLogger(refSequenceNumber) ?? this.logger;
366
373
  try {
367
- const ack = await this.summaryCollection.waitSummaryAck(refSequenceNumber);
374
+ // Initialize ack with undefined if exception happens inside of waitSummaryAck on second iteration,
375
+ // we record undefined, not previous handles.
376
+ ack = undefined;
377
+ ack = await this.summaryCollection.waitSummaryAck(refSequenceNumber);
368
378
  refSequenceNumber = ack.summaryOp.referenceSequenceNumber;
369
-
370
- await this.internalsProvider.refreshLatestSummaryAck(
371
- ack.summaryOp.contents.handle,
372
- ack.summaryAck.contents.handle,
373
- refSequenceNumber,
374
- summaryLogger,
379
+ const summaryOpHandle = ack.summaryOp.contents.handle;
380
+ const summaryAckHandle = ack.summaryAck.contents.handle;
381
+ // Make sure we block any summarizer from being executed/enqueued while
382
+ // executing the refreshLatestSummaryAck.
383
+ // https://dev.azure.com/fluidframework/internal/_workitems/edit/779
384
+ await this.runningSummarizer.lockedRefreshSummaryAckAction(async () =>
385
+ this.internalsProvider.refreshLatestSummaryAck(
386
+ summaryOpHandle,
387
+ summaryAckHandle,
388
+ refSequenceNumber,
389
+ summaryLogger,
390
+ ).catch(async (error) => {
391
+ // If the error is 404, so maybe the fetched version no longer exists on server. We just
392
+ // ignore this error in that case, as that means we will have another summaryAck for the
393
+ // latest version with which we will refresh the state. However in case of single commit
394
+ // summary, we might me missing a summary ack, so in that case we are still fine as the
395
+ // code in `submitSummary` function in container runtime, will refresh the latest state
396
+ // by calling `refreshLatestSummaryAckFromServer` and we will be fine.
397
+ if (isFluidError(error)
398
+ && error.errorType === DriverErrorType.fileNotFoundOrAccessDeniedError) {
399
+ summaryLogger.sendTelemetryEvent({
400
+ eventName: "HandleSummaryAckErrorIgnored",
401
+ referenceSequenceNumber: refSequenceNumber,
402
+ proposalHandle: summaryOpHandle,
403
+ ackHandle: summaryAckHandle,
404
+ }, error);
405
+ } else {
406
+ throw error;
407
+ }
408
+ }),
375
409
  );
376
410
  } catch (error) {
377
411
  summaryLogger.sendErrorEvent({
378
412
  eventName: "HandleSummaryAckError",
379
413
  referenceSequenceNumber: refSequenceNumber,
414
+ handle: ack?.summaryOp?.contents?.handle,
415
+ ackHandle: ack?.summaryAck?.contents?.handle,
380
416
  }, error);
381
417
  }
382
418
  refSequenceNumber++;
@@ -366,7 +366,7 @@ export class SummaryCollection extends TypedEventEmitter<ISummaryCollectionOpEve
366
366
  // from. i.e. initialSequenceNumber > summarySequenceNumber.
367
367
  // We really don't care about it for now, since it is older than
368
368
  // the one we loaded from.
369
- if (seq >= this.deltaManager.initialSequenceNumber) {
369
+ if (seq > this.deltaManager.initialSequenceNumber) {
370
370
  // Potential causes for it to be later than our initialSequenceNumber
371
371
  // are that the summaryOp was nacked then acked, double-acked, or
372
372
  // the summarySequenceNumber is incorrect.
@@ -420,7 +420,7 @@ export class SummaryGenerator {
420
420
  nonRuntimeOpsSinceLastSummary: this.heuristicData.numNonRuntimeOps,
421
421
  };
422
422
 
423
- default: assert(true, "Unexpected summary stage");
423
+ default: assert(true, 0x397 /* Unexpected summary stage */);
424
424
  }
425
425
 
426
426
  return initialProps;
@@ -14,6 +14,7 @@ import {
14
14
  SummarizerStopReason,
15
15
  } from "./summarizerTypes";
16
16
  import { SummaryCollection } from "./summaryCollection";
17
+ import { Summarizer } from "./summarizer";
17
18
 
18
19
  const defaultInitialDelayMs = 5000;
19
20
  const defaultOpsToBypassInitialDelay = 4000;
@@ -198,8 +199,12 @@ export class SummaryManager implements IDisposable {
198
199
  // a summarizer to kick off lastSummary. Without that, we would not be able to summarize and get
199
200
  // document out of broken state if it has too many ops and ordering service keeps nacking main
200
201
  // container (and thus it goes into cycle of reconnects)
201
- if (startWithInitialDelay && this.getShouldSummarizeState().shouldSummarize === false) {
202
- return "early exit";
202
+ // If we can't run the LastSummary, simply return as to avoid paying the cost of launching
203
+ // the summarizer at all.
204
+ const shouldSummarizeStateEarlyStage = this.getShouldSummarizeState();
205
+ if (startWithInitialDelay &&
206
+ shouldSummarizeStateEarlyStage.shouldSummarize === false) {
207
+ return `early exit ${shouldSummarizeStateEarlyStage.stopReason}`;
203
208
  }
204
209
 
205
210
  // We transition to Running before requesting the summarizer, because after requesting we can't predict
@@ -213,11 +218,21 @@ export class SummaryManager implements IDisposable {
213
218
  this.summarizer = summarizer;
214
219
 
215
220
  // Re-validate that it need to be running. Due to asynchrony, it may be not the case anymore
221
+ // If we can't run the LastSummary, simply return as to avoid paying the cost of launching
222
+ // the summarizer at all.
216
223
  const shouldSummarizeState = this.getShouldSummarizeState();
217
224
  if (shouldSummarizeState.shouldSummarize === false) {
218
- this.state = SummaryManagerState.Starting;
219
- summarizer.stop(shouldSummarizeState.stopReason);
220
- return "early exit after starting summarizer";
225
+ // In order to allow the last summary to run, we not only need a stop reason that would
226
+ // allow it but also, startWithInitialDelay to be false (start the summarization immediately),
227
+ // which would happen when we have a high enough number of unsummarized ops.
228
+ if (startWithInitialDelay || !Summarizer.stopReasonCanRunLastSummary(shouldSummarizeState.stopReason)) {
229
+ this.state = SummaryManagerState.Starting;
230
+ summarizer.stop(shouldSummarizeState.stopReason);
231
+ return `early exit after starting summarizer ${shouldSummarizeState.stopReason}`;
232
+ }
233
+ this.logger.sendTelemetryEvent({
234
+ eventName: "LastAttemptToSummarize",
235
+ });
221
236
  }
222
237
 
223
238
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion