@fluidframework/container-runtime 2.0.0-internal.1.1.2 → 2.0.0-internal.1.2.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 (137) hide show
  1. package/dist/batchManager.d.ts +37 -0
  2. package/dist/batchManager.d.ts.map +1 -0
  3. package/dist/batchManager.js +73 -0
  4. package/dist/batchManager.js.map +1 -0
  5. package/dist/batchTracker.d.ts +1 -2
  6. package/dist/batchTracker.d.ts.map +1 -1
  7. package/dist/batchTracker.js +1 -2
  8. package/dist/batchTracker.js.map +1 -1
  9. package/dist/containerRuntime.d.ts +52 -20
  10. package/dist/containerRuntime.d.ts.map +1 -1
  11. package/dist/containerRuntime.js +252 -126
  12. package/dist/containerRuntime.js.map +1 -1
  13. package/dist/dataStoreContext.d.ts +18 -9
  14. package/dist/dataStoreContext.d.ts.map +1 -1
  15. package/dist/dataStoreContext.js +24 -16
  16. package/dist/dataStoreContext.js.map +1 -1
  17. package/dist/dataStores.d.ts +6 -2
  18. package/dist/dataStores.d.ts.map +1 -1
  19. package/dist/dataStores.js +7 -9
  20. package/dist/dataStores.js.map +1 -1
  21. package/dist/deltaScheduler.d.ts +6 -4
  22. package/dist/deltaScheduler.d.ts.map +1 -1
  23. package/dist/deltaScheduler.js +6 -4
  24. package/dist/deltaScheduler.js.map +1 -1
  25. package/dist/garbageCollection.d.ts +41 -12
  26. package/dist/garbageCollection.d.ts.map +1 -1
  27. package/dist/garbageCollection.js +176 -98
  28. package/dist/garbageCollection.js.map +1 -1
  29. package/dist/gcSweepReadyUsageDetection.d.ts +53 -0
  30. package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -0
  31. package/dist/gcSweepReadyUsageDetection.js +135 -0
  32. package/dist/gcSweepReadyUsageDetection.js.map +1 -0
  33. package/dist/orderedClientElection.d.ts +28 -10
  34. package/dist/orderedClientElection.d.ts.map +1 -1
  35. package/dist/orderedClientElection.js +14 -4
  36. package/dist/orderedClientElection.js.map +1 -1
  37. package/dist/packageVersion.d.ts +1 -1
  38. package/dist/packageVersion.js +1 -1
  39. package/dist/packageVersion.js.map +1 -1
  40. package/dist/pendingStateManager.d.ts +0 -11
  41. package/dist/pendingStateManager.d.ts.map +1 -1
  42. package/dist/pendingStateManager.js +9 -44
  43. package/dist/pendingStateManager.js.map +1 -1
  44. package/dist/runningSummarizer.js +1 -1
  45. package/dist/runningSummarizer.js.map +1 -1
  46. package/dist/scheduleManager.d.ts +6 -3
  47. package/dist/scheduleManager.d.ts.map +1 -1
  48. package/dist/scheduleManager.js +22 -14
  49. package/dist/scheduleManager.js.map +1 -1
  50. package/dist/summarizerTypes.d.ts +16 -9
  51. package/dist/summarizerTypes.d.ts.map +1 -1
  52. package/dist/summarizerTypes.js +1 -1
  53. package/dist/summarizerTypes.js.map +1 -1
  54. package/dist/summaryCollection.d.ts +1 -0
  55. package/dist/summaryCollection.d.ts.map +1 -1
  56. package/dist/summaryCollection.js +29 -13
  57. package/dist/summaryCollection.js.map +1 -1
  58. package/dist/summaryManager.d.ts +2 -2
  59. package/dist/summaryManager.js +2 -2
  60. package/dist/summaryManager.js.map +1 -1
  61. package/lib/batchManager.d.ts +37 -0
  62. package/lib/batchManager.d.ts.map +1 -0
  63. package/lib/batchManager.js +69 -0
  64. package/lib/batchManager.js.map +1 -0
  65. package/lib/batchTracker.d.ts +1 -2
  66. package/lib/batchTracker.d.ts.map +1 -1
  67. package/lib/batchTracker.js +1 -2
  68. package/lib/batchTracker.js.map +1 -1
  69. package/lib/containerRuntime.d.ts +52 -20
  70. package/lib/containerRuntime.d.ts.map +1 -1
  71. package/lib/containerRuntime.js +255 -129
  72. package/lib/containerRuntime.js.map +1 -1
  73. package/lib/dataStoreContext.d.ts +18 -9
  74. package/lib/dataStoreContext.d.ts.map +1 -1
  75. package/lib/dataStoreContext.js +25 -17
  76. package/lib/dataStoreContext.js.map +1 -1
  77. package/lib/dataStores.d.ts +6 -2
  78. package/lib/dataStores.d.ts.map +1 -1
  79. package/lib/dataStores.js +7 -9
  80. package/lib/dataStores.js.map +1 -1
  81. package/lib/deltaScheduler.d.ts +6 -4
  82. package/lib/deltaScheduler.d.ts.map +1 -1
  83. package/lib/deltaScheduler.js +6 -4
  84. package/lib/deltaScheduler.js.map +1 -1
  85. package/lib/garbageCollection.d.ts +41 -12
  86. package/lib/garbageCollection.d.ts.map +1 -1
  87. package/lib/garbageCollection.js +175 -97
  88. package/lib/garbageCollection.js.map +1 -1
  89. package/lib/gcSweepReadyUsageDetection.d.ts +53 -0
  90. package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -0
  91. package/lib/gcSweepReadyUsageDetection.js +130 -0
  92. package/lib/gcSweepReadyUsageDetection.js.map +1 -0
  93. package/lib/orderedClientElection.d.ts +28 -10
  94. package/lib/orderedClientElection.d.ts.map +1 -1
  95. package/lib/orderedClientElection.js +14 -4
  96. package/lib/orderedClientElection.js.map +1 -1
  97. package/lib/packageVersion.d.ts +1 -1
  98. package/lib/packageVersion.js +1 -1
  99. package/lib/packageVersion.js.map +1 -1
  100. package/lib/pendingStateManager.d.ts +0 -11
  101. package/lib/pendingStateManager.d.ts.map +1 -1
  102. package/lib/pendingStateManager.js +9 -44
  103. package/lib/pendingStateManager.js.map +1 -1
  104. package/lib/runningSummarizer.js +1 -1
  105. package/lib/runningSummarizer.js.map +1 -1
  106. package/lib/scheduleManager.d.ts +6 -3
  107. package/lib/scheduleManager.d.ts.map +1 -1
  108. package/lib/scheduleManager.js +24 -16
  109. package/lib/scheduleManager.js.map +1 -1
  110. package/lib/summarizerTypes.d.ts +16 -9
  111. package/lib/summarizerTypes.d.ts.map +1 -1
  112. package/lib/summarizerTypes.js +1 -1
  113. package/lib/summarizerTypes.js.map +1 -1
  114. package/lib/summaryCollection.d.ts +1 -0
  115. package/lib/summaryCollection.d.ts.map +1 -1
  116. package/lib/summaryCollection.js +29 -13
  117. package/lib/summaryCollection.js.map +1 -1
  118. package/lib/summaryManager.d.ts +2 -2
  119. package/lib/summaryManager.js +2 -2
  120. package/lib/summaryManager.js.map +1 -1
  121. package/package.json +21 -18
  122. package/src/batchManager.ts +91 -0
  123. package/src/batchTracker.ts +1 -2
  124. package/src/containerRuntime.ts +331 -176
  125. package/src/dataStoreContext.ts +27 -17
  126. package/src/dataStores.ts +7 -8
  127. package/src/deltaScheduler.ts +6 -4
  128. package/src/garbageCollection.ts +224 -134
  129. package/src/gcSweepReadyUsageDetection.ts +147 -0
  130. package/src/orderedClientElection.ts +31 -10
  131. package/src/packageVersion.ts +1 -1
  132. package/src/pendingStateManager.ts +9 -57
  133. package/src/runningSummarizer.ts +1 -1
  134. package/src/scheduleManager.ts +32 -12
  135. package/src/summarizerTypes.ts +17 -9
  136. package/src/summaryCollection.ts +31 -16
  137. package/src/summaryManager.ts +2 -2
@@ -0,0 +1,147 @@
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
+ if (localStorageOverride !== undefined) {
78
+ this.localStorage = localStorageOverride;
79
+ } else {
80
+ try {
81
+ // localStorage is not defined in Node environment so this throws
82
+ this.localStorage = localStorage ?? noopStorage;
83
+ } catch (error) {
84
+ this.localStorage = noopStorage;
85
+ }
86
+ }
87
+
88
+ if (this.localStorage === noopStorage) {
89
+ // This means the Skip Closure Period logic will not work.
90
+ this.mc.logger.sendTelemetryEvent({ eventName: "SweepReadyUsageDetectionHandlerNoopStorage" });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * If SweepReady Usage Detection is enabled, close the interactive container.
96
+ * If the SkipClosureForXDays setting is set, don't close the container more than once in that period.
97
+ *
98
+ * Once Sweep is fully implemented, this will be removed since the objects will be gone
99
+ * and errors will arise elsewhere in the runtime
100
+ */
101
+ public usageDetectedInInteractiveClient(errorProps: ITelemetryProperties) {
102
+ if (!sweepReadyUsageDetectionSetting.read(this.mc.config).interactiveClient) {
103
+ return;
104
+ }
105
+
106
+ // Default stance is we close every time - this reflects the severity of SweepReady Object Usage.
107
+ // However, we may choose to "throttle" the closures by setting the SkipClosureForXDays setting,
108
+ // which will only allow the container to close once during that period, to avoid locking users out.
109
+ let shouldClose: boolean = true;
110
+ let pastClosuresMap: Record<string, { lastCloseTime: number; } | undefined> = {};
111
+ let lastCloseTime: number | undefined;
112
+ const skipClosureForXDays = this.mc.config.getNumber(skipClosureForXDaysKey);
113
+ if (skipClosureForXDays !== undefined) {
114
+ // Read pastClosuresMap from localStorage then extract the lastCloseTime from the map
115
+ try {
116
+ const rawValue = this.localStorage.getItem(closuresMapLocalStorageKey);
117
+ const parsedValue = rawValue === null ? {} : JSON.parse(rawValue);
118
+ if (typeof parsedValue === "object") {
119
+ pastClosuresMap = parsedValue;
120
+ }
121
+ } catch (e) {
122
+ }
123
+ lastCloseTime = pastClosuresMap[this.uniqueContainerKey]?.lastCloseTime;
124
+
125
+ // Don't close if we did already within the Skip Closure Period
126
+ if (lastCloseTime !== undefined && Date.now() < lastCloseTime + skipClosureForXDays * oneDayMs) {
127
+ shouldClose = false;
128
+ }
129
+ }
130
+
131
+ const error = new SweepReadyUsageError(
132
+ "SweepReady object used in Non-Summarizer Client",
133
+ { errorDetails: JSON.stringify({ ...errorProps, lastCloseTime, skipClosureForXDays }) },
134
+ );
135
+ if (shouldClose) {
136
+ // Update closures map in localStorage before closing
137
+ // Note there is a race condition between different tabs updating localStorage and overwriting
138
+ // each others' updates. If so, some tab will crash again. Just reload one at a time to get unstuck
139
+ pastClosuresMap[this.uniqueContainerKey] = { lastCloseTime: Date.now() };
140
+ this.localStorage.setItem(closuresMapLocalStorageKey, JSON.stringify(pastClosuresMap));
141
+
142
+ this.closeFn(error);
143
+ } else {
144
+ this.mc.logger.sendErrorEvent({ eventName: "SweepReadyObject_UsageAllowed" }, error);
145
+ }
146
+ }
147
+ }
@@ -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 = "2.0.0-internal.1.1.2";
9
+ export const pkgVersion = "2.0.0-internal.1.2.0";
@@ -11,7 +11,6 @@ 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";
17
16
  import { pkgVersion } from "./packageVersion";
@@ -69,10 +68,6 @@ export interface IRuntimeStateHandler{
69
68
  content: any,
70
69
  localOpMetadata: unknown,
71
70
  opMetadata: Record<string, unknown> | undefined): void;
72
- rollback(
73
- type: ContainerMessageType,
74
- content: any,
75
- localOpMetadata: unknown): void;
76
71
  }
77
72
 
78
73
  /**
@@ -233,7 +228,11 @@ export class PendingStateManager implements IDisposable {
233
228
 
234
229
  // then we push onto pendingStates which will cause PendingStateManager to resubmit when we connect
235
230
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
236
- 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
+ }
237
236
  }
238
237
  }
239
238
 
@@ -267,6 +266,7 @@ export class PendingStateManager implements IDisposable {
267
266
  }
268
267
 
269
268
  this._pendingMessagesCount--;
269
+ assert(this._pendingMessagesCount >= 0, 0x3d6 /* positive */);
270
270
 
271
271
  // Post-processing part - If we are processing a batch then this could be the last message in the batch.
272
272
  this.maybeProcessBatchEnd(message);
@@ -286,9 +286,11 @@ export class PendingStateManager implements IDisposable {
286
286
 
287
287
  /**
288
288
  * We are checking if the next message is the start of a batch. It can happen in the following scenarios:
289
+ *
289
290
  * 1. The FlushMode was set to TurnBased before this message was sent.
291
+ *
290
292
  * 2. The FlushMode was already TurnBased and a flush was called before this message was sent. This essentially
291
- * 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.
292
294
  *
293
295
  * Keep reading pending states from the queue until we encounter a message. It's possible that the FlushMode was
294
296
  * updated a bunch of times without sending any messages.
@@ -393,31 +395,6 @@ export class PendingStateManager implements IDisposable {
393
395
  this.isProcessingBatch = false;
394
396
  }
395
397
 
396
- /**
397
- * Capture the pending state at this point
398
- */
399
- public checkpoint() {
400
- const checkpointHead = this.pendingStates.peekBack();
401
- return {
402
- rollback: () => {
403
- try {
404
- while (this.pendingStates.peekBack() !== checkpointHead) {
405
- this.rollbackNextPendingState();
406
- }
407
- } catch (err) {
408
- const error = wrapError(err, (message) => {
409
- return DataProcessingError.create(
410
- `RollbackError: ${message}`,
411
- "checkpointRollback",
412
- undefined) as DataProcessingError;
413
- });
414
- this.stateHandler.close(error);
415
- throw error;
416
- }
417
- },
418
- };
419
- }
420
-
421
398
  /**
422
399
  * Returns the next pending state from the pending state queue.
423
400
  */
@@ -427,31 +404,6 @@ export class PendingStateManager implements IDisposable {
427
404
  return nextPendingState;
428
405
  }
429
406
 
430
- /**
431
- * Undo the last pending state
432
- */
433
- private rollbackNextPendingState() {
434
- const pendingStatesCount = this.pendingStates.length;
435
- if (pendingStatesCount === 0) {
436
- return;
437
- }
438
-
439
- this._pendingMessagesCount--;
440
-
441
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
442
- const pendingState = this.pendingStates.pop()!;
443
- switch (pendingState.type) {
444
- case "message":
445
- this.stateHandler.rollback(
446
- pendingState.messageType,
447
- pendingState.content,
448
- pendingState.localOpMetadata);
449
- break;
450
- default:
451
- throw new Error(`Can't rollback state ${pendingState.type}`);
452
- }
453
- }
454
-
455
407
  /**
456
408
  * Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
457
409
  * states in its queue. This includes setting the FlushMode and triggering resubmission of unacked ops.
@@ -239,7 +239,7 @@ export class RunningSummarizer implements IDisposable {
239
239
  public handleOp(op: ISequencedDocumentMessage) {
240
240
  this.heuristicData.lastOpSequenceNumber = op.sequenceNumber;
241
241
 
242
- if (op.type !== MessageType.Summarize && isRuntimeMessage(op)) {
242
+ if (isRuntimeMessage(op)) {
243
243
  this.heuristicData.numRuntimeOps++;
244
244
  } else {
245
245
  this.heuristicData.numNonRuntimeOps++;
@@ -8,8 +8,12 @@ import { IDocumentMessage, ISequencedDocumentMessage } from "@fluidframework/pro
8
8
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
9
9
  import { ChildLogger } from "@fluidframework/telemetry-utils";
10
10
  import { assert, performance } from "@fluidframework/common-utils";
11
- import { isUnpackedRuntimeMessage } from "@fluidframework/driver-utils";
12
- import { DataCorruptionError, extractSafePropertiesFromMessage } from "@fluidframework/container-utils";
11
+ import { isRuntimeMessage } from "@fluidframework/driver-utils";
12
+ import {
13
+ DataCorruptionError,
14
+ DataProcessingError,
15
+ extractSafePropertiesFromMessage,
16
+ } from "@fluidframework/container-utils";
13
17
  import { DeltaScheduler } from "./deltaScheduler";
14
18
  import { pkgVersion } from "./packageVersion";
15
19
  import { latencyThreshold } from "./connectionTelemetry";
@@ -20,10 +24,12 @@ type IRuntimeMessageMetadata = undefined | {
20
24
 
21
25
  /**
22
26
  * This class has the following responsibilities:
27
+ *
23
28
  * 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)
29
+ * As part of it, it validates batch correctness (i.e. no system ops in the middle of batch)
30
+ *
25
31
  * 2. It creates instance of ScheduleManagerCore that ensures we never start processing ops from batch
26
- * unless all ops of the batch are in.
32
+ * unless all ops of the batch are in.
27
33
  */
28
34
  export class ScheduleManager {
29
35
  private readonly deltaScheduler: DeltaScheduler;
@@ -33,13 +39,14 @@ export class ScheduleManager {
33
39
  constructor(
34
40
  private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
35
41
  private readonly emitter: EventEmitter,
42
+ readonly getClientId: () => string | undefined,
36
43
  private readonly logger: ITelemetryLogger,
37
44
  ) {
38
45
  this.deltaScheduler = new DeltaScheduler(
39
46
  this.deltaManager,
40
47
  ChildLogger.create(this.logger, "DeltaScheduler"),
41
48
  );
42
- void new ScheduleManagerCore(deltaManager, logger);
49
+ void new ScheduleManagerCore(deltaManager, getClientId, logger);
43
50
  }
44
51
 
45
52
  public beforeOpProcessing(message: ISequencedDocumentMessage) {
@@ -52,11 +59,7 @@ export class ScheduleManager {
52
59
  this.deltaScheduler.batchBegin(message);
53
60
 
54
61
  const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
55
- if (batch) {
56
- this.batchClientId = message.clientId;
57
- } else {
58
- this.batchClientId = undefined;
59
- }
62
+ this.batchClientId = batch ? message.clientId : undefined;
60
63
  }
61
64
  }
62
65
 
@@ -99,6 +102,7 @@ class ScheduleManagerCore {
99
102
 
100
103
  constructor(
101
104
  private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
105
+ private readonly getClientId: () => string | undefined,
102
106
  private readonly logger: ITelemetryLogger,
103
107
  ) {
104
108
  // Listen for delta manager sends and add batch metadata to messages
@@ -234,9 +238,22 @@ class ScheduleManagerCore {
234
238
  const batchMetadata = metadata?.batch;
235
239
 
236
240
  // Protocol messages are never part of a runtime batch of messages
237
- if (!isUnpackedRuntimeMessage(message)) {
241
+ if (!isRuntimeMessage(message)) {
238
242
  // 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!" */);
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
+
240
257
  assert(batchMetadata === undefined, 0x29b /* "system op in a batch?" */);
241
258
  assert(!this.localPaused, 0x29c /* "we should be processing ops when there is no active batch" */);
242
259
  return;
@@ -258,6 +275,9 @@ class ScheduleManagerCore {
258
275
  {
259
276
  runtimeVersion: pkgVersion,
260
277
  batchClientId: this.currentBatchClientId,
278
+ pauseSequenceNumber: this.pauseSequenceNumber,
279
+ localBatch: this.currentBatchClientId === this.getClientId(),
280
+ localMessage: message.clientId === this.getClientId(),
261
281
  ...extractSafePropertiesFromMessage(message),
262
282
  });
263
283
  }
@@ -25,16 +25,16 @@ import { SummarizeReason } from "./summaryGenerator";
25
25
  import { ISummaryConfigurationHeuristics } from ".";
26
26
 
27
27
  /**
28
- * @deprecated - This will be removed in a later release.
28
+ * @deprecated This will be removed in a later release.
29
29
  */
30
30
  export const ISummarizer: keyof IProvideSummarizer = "ISummarizer";
31
31
 
32
32
  /**
33
- * @deprecated - This will be removed in a later release.
33
+ * @deprecated This will be removed in a later release.
34
34
  */
35
35
  export interface IProvideSummarizer {
36
36
  /**
37
- * @deprecated - This will be removed in a later release.
37
+ * @deprecated This will be removed in a later release.
38
38
  */
39
39
  readonly ISummarizer: ISummarizer;
40
40
  }
@@ -130,6 +130,7 @@ export interface IOnDemandSummarizeOptions extends ISummarizeOptions {
130
130
  export interface IEnqueueSummarizeOptions extends IOnDemandSummarizeOptions {
131
131
  /** If specified, The summarize attempt will not occur until after this sequence number. */
132
132
  readonly afterSequenceNumber?: number;
133
+
133
134
  /**
134
135
  * True to override the existing enqueued summarize attempt if there is one.
135
136
  * This will guarantee that this attempt gets enqueued. If override is false,
@@ -204,11 +205,16 @@ export interface ISubmitSummaryOpResult extends Omit<IUploadSummaryResult, "stag
204
205
  * The result consists of 4 possible stages, each with its own data.
205
206
  * The data is cumulative, so each stage will contain the data from the previous stages.
206
207
  * If the final "submitted" stage is not reached, the result may contain the error object.
208
+ *
207
209
  * Stages:
208
- * 1. "base" - stopped before the summary tree was even generated, and the result only contains the base data
209
- * 2. "generate" - the summary tree was generated, and the result will contain that tree + stats
210
- * 3. "upload" - the summary was uploaded to storage, and the result contains the server-provided handle
211
- * 4. "submit" - the summarize op was submitted, and the result contains the op client sequence number.
210
+ *
211
+ * 1. "base" - stopped before the summary tree was even generated, and the result only contains the base data
212
+ *
213
+ * 2. "generate" - the summary tree was generated, and the result will contain that tree + stats
214
+ *
215
+ * 3. "upload" - the summary was uploaded to storage, and the result contains the server-provided handle
216
+ *
217
+ * 4. "submit" - the summarize op was submitted, and the result contains the op client sequence number.
212
218
  */
213
219
  export type SubmitSummaryResult =
214
220
  | IBaseSummarizeResult
@@ -453,8 +459,10 @@ type SummaryGeneratorOptionalTelemetryProperties =
453
459
  "opsSinceLastAttempt" |
454
460
  /** Delta between the current reference sequence number and the reference sequence number of the last summary */
455
461
  "opsSinceLastSummary" |
456
- /** Delta in sum of op sizes between the current reference sequence number and the reference
457
- * sequence number of the last summary */
462
+ /**
463
+ * Delta in sum of op sizes between the current reference sequence number and the reference
464
+ * sequence number of the last summary
465
+ */
458
466
  "opsSizesSinceLastSummary" |
459
467
  /** Delta between the number of non-runtime ops since the last summary */
460
468
  "nonRuntimeOpsSinceLastSummary" |
@@ -239,9 +239,7 @@ export class SummaryCollection extends TypedEventEmitter<ISummaryCollectionOpEve
239
239
  private readonly logger: ITelemetryLogger,
240
240
  ) {
241
241
  super();
242
- this.deltaManager.on(
243
- "op",
244
- (op) => this.handleOp(op));
242
+ this.deltaManager.on("op", (op) => this.handleOp(op));
245
243
  }
246
244
 
247
245
  /**
@@ -295,24 +293,41 @@ export class SummaryCollection extends TypedEventEmitter<ISummaryCollectionOpEve
295
293
  return this.lastAck;
296
294
  }
297
295
 
296
+ private parseContent(op: ISequencedDocumentMessage) {
297
+ // back-compat: ADO #1385: Make this unconditional in the future,
298
+ // when Container.processRemoteMessage stops parsing contents. That said, we should move to
299
+ // listen for "op" events from ContainerRuntime, and parsing may not be required at all if
300
+ // ContainerRuntime.process() would parse it for all types of ops.
301
+ // Can make either of those changes only when LTS moves to a version that has no content
302
+ // parsing in loader layer!
303
+ if (typeof op.contents === "string") {
304
+ op.contents = JSON.parse(op.contents);
305
+ }
306
+ }
307
+
298
308
  /**
299
309
  * Handler for ops; only handles ops relating to summaries.
300
310
  * @param op - op message to handle
301
311
  */
302
- private handleOp(op: ISequencedDocumentMessage) {
312
+ private handleOp(opArg: ISequencedDocumentMessage) {
313
+ const op = { ...opArg };
314
+
303
315
  switch (op.type) {
304
- case MessageType.Summarize: {
305
- this.handleSummaryOp(op as ISummaryOpMessage);
306
- return;
307
- }
308
- case MessageType.SummaryAck: {
309
- this.handleSummaryAck(op as ISummaryAckMessage);
310
- return;
311
- }
312
- case MessageType.SummaryNack: {
313
- this.handleSummaryNack(op as ISummaryNackMessage);
314
- return;
315
- }
316
+ case MessageType.Summarize:
317
+ this.parseContent(op);
318
+ return this.handleSummaryOp(op as ISummaryOpMessage);
319
+ case MessageType.SummaryAck:
320
+ case MessageType.SummaryNack:
321
+ // Old files (prior to PR #10077) may not contain this info
322
+ // back-compat: ADO #1385: remove cast when ISequencedDocumentMessage changes are propagated
323
+ if ((op as any).data !== undefined) {
324
+ op.contents = JSON.parse((op as any).data);
325
+ } else {
326
+ this.parseContent(op);
327
+ }
328
+ return op.type === MessageType.SummaryAck
329
+ ? this.handleSummaryAck(op as ISummaryAckMessage)
330
+ : this.handleSummaryNack(op as ISummaryNackMessage);
316
331
  default: {
317
332
  // If the difference between timestamp of current op and last summary op is greater than
318
333
  // the maxAckWaitTime, then we need to inform summarizer to not wait and summarize
@@ -302,8 +302,8 @@ export class SummaryManager implements IDisposable {
302
302
 
303
303
  /**
304
304
  * Implements initial delay before creating summarizer
305
- * @returns true, if creation is delayed due to heuristics (not many ops to summarize).
306
- * False if summarizer should start immediately due to too many unsummarized ops.
305
+ * @returns `true`, if creation is delayed due to heuristics (not many ops to summarize).
306
+ * `false` if summarizer should start immediately due to too many unsummarized ops.
307
307
  */
308
308
  private async delayBeforeCreatingSummarizer(): Promise<boolean> {
309
309
  // throttle creation of new summarizer containers to prevent spamming the server with websocket connections