@fluidframework/container-runtime 2.91.0 → 2.93.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.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/api-report/container-runtime.legacy.beta.api.md +2 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/containerCompatibility.d.ts +1 -1
- package/dist/containerCompatibility.d.ts.map +1 -1
- package/dist/containerCompatibility.js.map +1 -1
- package/dist/containerRuntime.d.ts +38 -11
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +118 -86
- package/dist/containerRuntime.js.map +1 -1
- package/dist/gc/garbageCollection.d.ts +1 -0
- package/dist/gc/garbageCollection.d.ts.map +1 -1
- package/dist/gc/garbageCollection.js +3 -8
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/gc/gcDefinitions.d.ts +4 -0
- package/dist/gc/gcDefinitions.d.ts.map +1 -1
- package/dist/gc/gcDefinitions.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy.d.ts +1 -1
- package/dist/opLifecycle/batchManager.d.ts +3 -9
- package/dist/opLifecycle/batchManager.d.ts.map +1 -1
- package/dist/opLifecycle/batchManager.js +5 -3
- package/dist/opLifecycle/batchManager.js.map +1 -1
- package/dist/opLifecycle/index.d.ts +1 -1
- package/dist/opLifecycle/index.d.ts.map +1 -1
- package/dist/opLifecycle/index.js +2 -1
- package/dist/opLifecycle/index.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.d.ts +6 -0
- package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/dist/opLifecycle/opGroupingManager.js +11 -2
- package/dist/opLifecycle/opGroupingManager.js.map +1 -1
- package/dist/opLifecycle/opSerialization.d.ts +3 -1
- package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
- package/dist/opLifecycle/opSerialization.js +11 -9
- package/dist/opLifecycle/opSerialization.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts +8 -11
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- package/dist/opLifecycle/outbox.js +42 -66
- package/dist/opLifecycle/outbox.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +8 -9
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +24 -22
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/public.d.ts +1 -1
- package/dist/runtimeLayerCompatState.d.ts +2 -2
- package/dist/summary/documentSchema.d.ts +9 -3
- package/dist/summary/documentSchema.d.ts.map +1 -1
- package/dist/summary/documentSchema.js +19 -3
- package/dist/summary/documentSchema.js.map +1 -1
- package/dist/summary/orderedClientElection.js +2 -2
- package/dist/summary/orderedClientElection.js.map +1 -1
- package/dist/summary/summaryManager.d.ts +1 -0
- package/dist/summary/summaryManager.d.ts.map +1 -1
- package/dist/summary/summaryManager.js +9 -0
- package/dist/summary/summaryManager.js.map +1 -1
- package/eslint.config.mts +1 -1
- package/internal.d.ts +1 -1
- package/legacy.d.ts +1 -1
- package/lib/containerCompatibility.d.ts +1 -1
- package/lib/containerCompatibility.d.ts.map +1 -1
- package/lib/containerCompatibility.js.map +1 -1
- package/lib/containerRuntime.d.ts +38 -11
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +118 -87
- package/lib/containerRuntime.js.map +1 -1
- package/lib/gc/garbageCollection.d.ts +1 -0
- package/lib/gc/garbageCollection.d.ts.map +1 -1
- package/lib/gc/garbageCollection.js +3 -8
- package/lib/gc/garbageCollection.js.map +1 -1
- package/lib/gc/gcDefinitions.d.ts +4 -0
- package/lib/gc/gcDefinitions.d.ts.map +1 -1
- package/lib/gc/gcDefinitions.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/legacy.d.ts +1 -1
- package/lib/opLifecycle/batchManager.d.ts +3 -9
- package/lib/opLifecycle/batchManager.d.ts.map +1 -1
- package/lib/opLifecycle/batchManager.js +5 -3
- package/lib/opLifecycle/batchManager.js.map +1 -1
- package/lib/opLifecycle/index.d.ts +1 -1
- package/lib/opLifecycle/index.d.ts.map +1 -1
- package/lib/opLifecycle/index.js +1 -1
- package/lib/opLifecycle/index.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.d.ts +6 -0
- package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/lib/opLifecycle/opGroupingManager.js +10 -1
- package/lib/opLifecycle/opGroupingManager.js.map +1 -1
- package/lib/opLifecycle/opSerialization.d.ts +3 -1
- package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
- package/lib/opLifecycle/opSerialization.js +11 -9
- package/lib/opLifecycle/opSerialization.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts +8 -11
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- package/lib/opLifecycle/outbox.js +43 -67
- package/lib/opLifecycle/outbox.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +8 -9
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +24 -22
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/public.d.ts +1 -1
- package/lib/runtimeLayerCompatState.d.ts +2 -2
- package/lib/summary/documentSchema.d.ts +9 -3
- package/lib/summary/documentSchema.d.ts.map +1 -1
- package/lib/summary/documentSchema.js +19 -3
- package/lib/summary/documentSchema.js.map +1 -1
- package/lib/summary/orderedClientElection.js +2 -2
- package/lib/summary/orderedClientElection.js.map +1 -1
- package/lib/summary/summaryManager.d.ts +1 -0
- package/lib/summary/summaryManager.d.ts.map +1 -1
- package/lib/summary/summaryManager.js +9 -0
- package/lib/summary/summaryManager.js.map +1 -1
- package/lib/tsdoc-metadata.json +1 -1
- package/package.json +27 -28
- package/src/containerCompatibility.ts +2 -0
- package/src/containerRuntime.ts +163 -106
- package/src/gc/garbageCollection.ts +4 -9
- package/src/gc/gcDefinitions.ts +4 -0
- package/src/index.ts +1 -0
- package/src/opLifecycle/batchManager.ts +6 -13
- package/src/opLifecycle/index.ts +1 -0
- package/src/opLifecycle/opGroupingManager.ts +11 -1
- package/src/opLifecycle/opSerialization.ts +14 -12
- package/src/opLifecycle/outbox.ts +53 -86
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +31 -33
- package/src/summary/documentSchema.ts +25 -2
- package/src/summary/orderedClientElection.ts +2 -2
- package/src/summary/summaryManager.ts +11 -0
|
@@ -17,6 +17,13 @@ import type {
|
|
|
17
17
|
OutboundSingletonBatch,
|
|
18
18
|
} from "./definitions.js";
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The number of ops in a batch above which the batch is considered "large"
|
|
22
|
+
* for telemetry purposes. Used by both {@link OpGroupingManager} (GroupLargeBatch event)
|
|
23
|
+
* and as the default staging-mode auto-flush threshold.
|
|
24
|
+
*/
|
|
25
|
+
export const largeBatchThreshold = 1000;
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* Grouping makes assumptions about the shape of message contents. This interface codifies those assumptions, but does not validate them.
|
|
22
29
|
*/
|
|
@@ -123,7 +130,10 @@ export class OpGroupingManager {
|
|
|
123
130
|
return batch as OutboundSingletonBatch;
|
|
124
131
|
}
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
// Use > (not >=) so that batches flushed exactly at the staging-mode
|
|
134
|
+
// auto-flush threshold (which defaults to largeBatchThreshold) don't
|
|
135
|
+
// trigger this event. Only genuinely oversized batches are logged.
|
|
136
|
+
if (batch.messages.length > largeBatchThreshold) {
|
|
127
137
|
this.logger.sendTelemetryEvent({
|
|
128
138
|
eventName: "GroupLargeBatch",
|
|
129
139
|
length: batch.messages.length,
|
|
@@ -40,16 +40,18 @@ export function serializeOp(
|
|
|
40
40
|
| EmptyGroupedBatch
|
|
41
41
|
| LocalContainerRuntimeMessage
|
|
42
42
|
| LocalContainerRuntimeMessage[],
|
|
43
|
-
): string {
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
): { content: string } {
|
|
44
|
+
return {
|
|
45
|
+
content: JSON.stringify(
|
|
46
|
+
toSerialize,
|
|
47
|
+
// replacer:
|
|
48
|
+
(key, value: unknown) => {
|
|
49
|
+
// If 'value' is an IFluidHandle return its encoded form.
|
|
50
|
+
if (isFluidHandle(value)) {
|
|
51
|
+
return encodeHandleForSerialization(toFluidHandleInternal(value));
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
};
|
|
55
57
|
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type BatchSequenceNumbers,
|
|
29
29
|
sequenceNumbersMatch,
|
|
30
30
|
type BatchId,
|
|
31
|
+
addBatchMetadata,
|
|
31
32
|
} from "./batchManager.js";
|
|
32
33
|
import type {
|
|
33
34
|
LocalBatchMessage,
|
|
@@ -48,12 +49,6 @@ export interface IOutboxConfig {
|
|
|
48
49
|
* The maximum size of a batch that we can send over the wire.
|
|
49
50
|
*/
|
|
50
51
|
readonly maxBatchSizeInBytes: number;
|
|
51
|
-
/**
|
|
52
|
-
* If true, maybeFlushPartialBatch will flush the batch if the reference sequence number changed
|
|
53
|
-
* since the batch started. Otherwise, it will throw in this case (apart from reentrancy which is handled elsewhere).
|
|
54
|
-
* Once the new throw-based flow is proved in a production environment, this option will be removed.
|
|
55
|
-
*/
|
|
56
|
-
readonly flushPartialBatches: boolean;
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
export interface IOutboxParameters {
|
|
@@ -71,6 +66,13 @@ export interface IOutboxParameters {
|
|
|
71
66
|
readonly getCurrentSequenceNumbers: () => BatchSequenceNumbers;
|
|
72
67
|
readonly reSubmit: (message: PendingMessageResubmitData, squash: boolean) => void;
|
|
73
68
|
readonly opReentrancy: () => boolean;
|
|
69
|
+
/**
|
|
70
|
+
* JIT callback to generate an ID allocation op at flush time.
|
|
71
|
+
* Called after rebase (if any), so the returned message has the correct refSeq.
|
|
72
|
+
*
|
|
73
|
+
* @returns A LocalBatchMessage for the ID allocation op, or undefined if no IDs need allocating.
|
|
74
|
+
*/
|
|
75
|
+
readonly generateIdAllocationOp: () => LocalBatchMessage | undefined;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/**
|
|
@@ -142,7 +144,7 @@ export function localBatchToOutboundBatch({
|
|
|
142
144
|
// Shallow copy each message as we switch types
|
|
143
145
|
const outboundMessages = localBatch.messages.map<OutboundBatchMessage>(
|
|
144
146
|
({ runtimeOp, ...message }) => ({
|
|
145
|
-
contents: serializeOp(runtimeOp),
|
|
147
|
+
contents: serializeOp(runtimeOp).content,
|
|
146
148
|
...message,
|
|
147
149
|
}),
|
|
148
150
|
);
|
|
@@ -195,7 +197,6 @@ export class Outbox {
|
|
|
195
197
|
private readonly logger: ITelemetryLoggerExt;
|
|
196
198
|
private readonly mainBatch: BatchManager;
|
|
197
199
|
private readonly blobAttachBatch: BatchManager;
|
|
198
|
-
private readonly idAllocationBatch: BatchManager;
|
|
199
200
|
private batchRebasesToReport = 5;
|
|
200
201
|
private rebasing = false;
|
|
201
202
|
|
|
@@ -211,16 +212,12 @@ export class Outbox {
|
|
|
211
212
|
constructor(private readonly params: IOutboxParameters) {
|
|
212
213
|
this.logger = createChildLogger({ logger: params.logger, namespace: "Outbox" });
|
|
213
214
|
|
|
214
|
-
this.mainBatch = new BatchManager({
|
|
215
|
-
this.blobAttachBatch = new BatchManager({
|
|
216
|
-
this.idAllocationBatch = new BatchManager({
|
|
217
|
-
canRebase: false,
|
|
218
|
-
ignoreBatchId: true,
|
|
219
|
-
});
|
|
215
|
+
this.mainBatch = new BatchManager({ disableGroupedBatching: false });
|
|
216
|
+
this.blobAttachBatch = new BatchManager({ disableGroupedBatching: true });
|
|
220
217
|
}
|
|
221
218
|
|
|
222
219
|
public get messageCount(): number {
|
|
223
|
-
return this.mainBatch.length + this.blobAttachBatch.length
|
|
220
|
+
return this.mainBatch.length + this.blobAttachBatch.length;
|
|
224
221
|
}
|
|
225
222
|
|
|
226
223
|
public get mainBatchMessageCount(): number {
|
|
@@ -231,19 +228,12 @@ export class Outbox {
|
|
|
231
228
|
return this.blobAttachBatch.length;
|
|
232
229
|
}
|
|
233
230
|
|
|
234
|
-
public get idAllocationBatchMessageCount(): number {
|
|
235
|
-
return this.idAllocationBatch.length;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
231
|
public get isEmpty(): boolean {
|
|
239
232
|
return this.messageCount === 0;
|
|
240
233
|
}
|
|
241
234
|
|
|
242
235
|
public containsUserChanges(): boolean {
|
|
243
|
-
return (
|
|
244
|
-
this.mainBatch.containsUserChanges() || this.blobAttachBatch.containsUserChanges()
|
|
245
|
-
// ID Allocation ops are not user changes
|
|
246
|
-
);
|
|
236
|
+
return this.mainBatch.containsUserChanges() || this.blobAttachBatch.containsUserChanges();
|
|
247
237
|
}
|
|
248
238
|
|
|
249
239
|
/**
|
|
@@ -257,13 +247,11 @@ export class Outbox {
|
|
|
257
247
|
* last message processed by the ContainerRuntime. In the absence of op reentrancy, this
|
|
258
248
|
* pair will remain stable during a single JS turn during which the batch is being built up.
|
|
259
249
|
*/
|
|
260
|
-
private
|
|
250
|
+
private outboxSequenceNumberCoherencyCheck(): void {
|
|
261
251
|
const mainBatchSeqNums = this.mainBatch.sequenceNumbers;
|
|
262
252
|
const blobAttachSeqNums = this.blobAttachBatch.sequenceNumbers;
|
|
263
|
-
const idAllocSeqNums = this.idAllocationBatch.sequenceNumbers;
|
|
264
253
|
assert(
|
|
265
|
-
sequenceNumbersMatch(mainBatchSeqNums, blobAttachSeqNums)
|
|
266
|
-
sequenceNumbersMatch(mainBatchSeqNums, idAllocSeqNums),
|
|
254
|
+
sequenceNumbersMatch(mainBatchSeqNums, blobAttachSeqNums),
|
|
267
255
|
0x58d /* Reference sequence numbers from both batches must be in sync */,
|
|
268
256
|
);
|
|
269
257
|
|
|
@@ -271,8 +259,7 @@ export class Outbox {
|
|
|
271
259
|
|
|
272
260
|
if (
|
|
273
261
|
sequenceNumbersMatch(mainBatchSeqNums, currentSequenceNumbers) &&
|
|
274
|
-
sequenceNumbersMatch(blobAttachSeqNums, currentSequenceNumbers)
|
|
275
|
-
sequenceNumbersMatch(idAllocSeqNums, currentSequenceNumbers)
|
|
262
|
+
sequenceNumbersMatch(blobAttachSeqNums, currentSequenceNumbers)
|
|
276
263
|
) {
|
|
277
264
|
// The reference sequence numbers are stable, there is nothing to do
|
|
278
265
|
return;
|
|
@@ -295,10 +282,7 @@ export class Outbox {
|
|
|
295
282
|
this.logger.sendTelemetryEvent(
|
|
296
283
|
{
|
|
297
284
|
// Only log error if this is truly unexpected
|
|
298
|
-
category:
|
|
299
|
-
expectedDueToReentrancy || this.params.config.flushPartialBatches
|
|
300
|
-
? "generic"
|
|
301
|
-
: "error",
|
|
285
|
+
category: expectedDueToReentrancy ? "generic" : "error",
|
|
302
286
|
eventName: "ReferenceSequenceNumberMismatch",
|
|
303
287
|
details: {
|
|
304
288
|
expectedDueToReentrancy,
|
|
@@ -314,12 +298,6 @@ export class Outbox {
|
|
|
314
298
|
);
|
|
315
299
|
}
|
|
316
300
|
|
|
317
|
-
// If we're configured to flush partial batches, do that now and return (don't throw)
|
|
318
|
-
if (this.params.config.flushPartialBatches) {
|
|
319
|
-
this.flushAll();
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
301
|
// If we are in a reentrant context, we know this can happen without causing any harm.
|
|
324
302
|
if (expectedDueToReentrancy) {
|
|
325
303
|
return;
|
|
@@ -329,23 +307,17 @@ export class Outbox {
|
|
|
329
307
|
}
|
|
330
308
|
|
|
331
309
|
public submit(message: LocalBatchMessage): void {
|
|
332
|
-
this.
|
|
310
|
+
this.outboxSequenceNumberCoherencyCheck();
|
|
333
311
|
|
|
334
312
|
this.addMessageToBatchManager(this.mainBatch, message);
|
|
335
313
|
}
|
|
336
314
|
|
|
337
315
|
public submitBlobAttach(message: LocalBatchMessage): void {
|
|
338
|
-
this.
|
|
316
|
+
this.outboxSequenceNumberCoherencyCheck();
|
|
339
317
|
|
|
340
318
|
this.addMessageToBatchManager(this.blobAttachBatch, message);
|
|
341
319
|
}
|
|
342
320
|
|
|
343
|
-
public submitIdAllocation(message: LocalBatchMessage): void {
|
|
344
|
-
this.maybeFlushPartialBatch();
|
|
345
|
-
|
|
346
|
-
this.addMessageToBatchManager(this.idAllocationBatch, message);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
321
|
private addMessageToBatchManager(
|
|
350
322
|
batchManager: BatchManager,
|
|
351
323
|
message: LocalBatchMessage,
|
|
@@ -367,11 +339,12 @@ export class Outbox {
|
|
|
367
339
|
public flush(resubmitInfo?: BatchResubmitInfo): void {
|
|
368
340
|
// We have nothing to flush if all batchManagers are empty, and we we're not needing to resubmit an empty batch placeholder
|
|
369
341
|
if (
|
|
370
|
-
this.idAllocationBatch.empty &&
|
|
371
342
|
this.blobAttachBatch.empty &&
|
|
372
343
|
this.mainBatch.empty &&
|
|
373
344
|
resubmitInfo?.batchId === undefined
|
|
374
345
|
) {
|
|
346
|
+
// Note that it's possible that there are unfinalized ranges in the ID Compressor,
|
|
347
|
+
// but there's no urgency to flush those if they're not referenced in any messages.
|
|
375
348
|
return;
|
|
376
349
|
}
|
|
377
350
|
|
|
@@ -383,8 +356,7 @@ export class Outbox {
|
|
|
383
356
|
}
|
|
384
357
|
|
|
385
358
|
private flushAll(resubmitInfo?: BatchResubmitInfo): void {
|
|
386
|
-
const allBatchesEmpty =
|
|
387
|
-
this.idAllocationBatch.empty && this.blobAttachBatch.empty && this.mainBatch.empty;
|
|
359
|
+
const allBatchesEmpty = this.blobAttachBatch.empty && this.mainBatch.empty;
|
|
388
360
|
if (allBatchesEmpty) {
|
|
389
361
|
// If we're resubmitting with a batchId and all batches are empty, we need to flush an empty batch.
|
|
390
362
|
// Note that we currently resubmit one batch at a time, so on resubmit, 1 of the 2 batches will *always* be empty.
|
|
@@ -397,22 +369,8 @@ export class Outbox {
|
|
|
397
369
|
return;
|
|
398
370
|
}
|
|
399
371
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
this.flushInternal({
|
|
403
|
-
batchManager: this.idAllocationBatch,
|
|
404
|
-
// Note: For now, we will never stage ID Allocation messages.
|
|
405
|
-
// They won't contain personal info and no harm in extra allocations in case of discarding the staged changes
|
|
406
|
-
});
|
|
407
|
-
this.flushInternal({
|
|
408
|
-
batchManager: this.blobAttachBatch,
|
|
409
|
-
disableGroupedBatching: true,
|
|
410
|
-
resubmitInfo,
|
|
411
|
-
});
|
|
412
|
-
this.flushInternal({
|
|
413
|
-
batchManager: this.mainBatch,
|
|
414
|
-
resubmitInfo,
|
|
415
|
-
});
|
|
372
|
+
this.flushInternal(this.blobAttachBatch, resubmitInfo);
|
|
373
|
+
this.flushInternal(this.mainBatch, resubmitInfo);
|
|
416
374
|
}
|
|
417
375
|
|
|
418
376
|
private flushEmptyBatch(
|
|
@@ -444,17 +402,15 @@ export class Outbox {
|
|
|
444
402
|
return;
|
|
445
403
|
}
|
|
446
404
|
|
|
447
|
-
private flushInternal(
|
|
448
|
-
batchManager: BatchManager
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}): void {
|
|
452
|
-
const { batchManager, disableGroupedBatching = false, resubmitInfo } = params;
|
|
405
|
+
private flushInternal(
|
|
406
|
+
batchManager: BatchManager,
|
|
407
|
+
resubmitInfo?: BatchResubmitInfo, // undefined if not resubmitting
|
|
408
|
+
): void {
|
|
453
409
|
if (batchManager.empty) {
|
|
454
410
|
return;
|
|
455
411
|
}
|
|
456
412
|
|
|
457
|
-
|
|
413
|
+
let rawBatch = batchManager.popBatch();
|
|
458
414
|
|
|
459
415
|
// On resubmit we use the original batch's staged state, so these should match as well.
|
|
460
416
|
const staged = rawBatch.staged === true;
|
|
@@ -464,13 +420,15 @@ export class Outbox {
|
|
|
464
420
|
);
|
|
465
421
|
|
|
466
422
|
const groupingEnabled =
|
|
467
|
-
!disableGroupedBatching &&
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
423
|
+
!batchManager.options.disableGroupedBatching &&
|
|
424
|
+
this.params.groupingManager.groupedBatchingEnabled();
|
|
425
|
+
if (rawBatch.hasReentrantOps === true) {
|
|
426
|
+
assert(
|
|
427
|
+
resubmitInfo === undefined,
|
|
428
|
+
0xcf2 /* Re-submitting a batch with reentrant ops is not supported */,
|
|
429
|
+
);
|
|
473
430
|
assert(!this.rebasing, 0x6fa /* A rebased batch should never have reentrant ops */);
|
|
431
|
+
// Rebase the current batch (resubmit the ops one-by-one) and then reinvoke flushInternal.
|
|
474
432
|
// If a batch contains reentrant ops (ops created as a result from processing another op)
|
|
475
433
|
// it needs to be rebased so that we can ensure consistent reference sequence numbers
|
|
476
434
|
// and eventual consistency at the DDS level.
|
|
@@ -481,11 +439,22 @@ export class Outbox {
|
|
|
481
439
|
return;
|
|
482
440
|
}
|
|
483
441
|
|
|
484
|
-
let clientSequenceNumber: number | undefined;
|
|
485
442
|
// Did we disconnect? (i.e. is shouldSend false?)
|
|
486
443
|
// If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
|
|
487
444
|
// Because flush() is a task that executes async (on clean stack), we can get here in disconnected state.
|
|
488
|
-
|
|
445
|
+
const shouldSendNow = this.params.shouldSend() && !staged;
|
|
446
|
+
let clientSequenceNumber: number | undefined;
|
|
447
|
+
if (shouldSendNow) {
|
|
448
|
+
// Generate ID Allocation op just-in-time, after rebase (if any), and before addBatchMetadata,
|
|
449
|
+
// so that the prepended idAllocMsg is correctly marked as the first op in the batch.
|
|
450
|
+
// This ensures the refSeq is correct (matching the rest of the batch) and that
|
|
451
|
+
// ID ranges aren't lost during rebase (since reSubmit drops IdAllocation ops).
|
|
452
|
+
// Only generate for non-staged batches — ID alloc ops are always non-staged.
|
|
453
|
+
const idAllocMsg = this.params.generateIdAllocationOp();
|
|
454
|
+
if (idAllocMsg !== undefined) {
|
|
455
|
+
rawBatch = { ...rawBatch, messages: [idAllocMsg, ...rawBatch.messages] };
|
|
456
|
+
}
|
|
457
|
+
addBatchMetadata(rawBatch, resubmitInfo?.batchId);
|
|
489
458
|
const virtualizedBatch = this.virtualizeBatch(rawBatch, groupingEnabled);
|
|
490
459
|
|
|
491
460
|
clientSequenceNumber = this.sendBatch(virtualizedBatch);
|
|
@@ -493,13 +462,14 @@ export class Outbox {
|
|
|
493
462
|
clientSequenceNumber === undefined || clientSequenceNumber >= 0,
|
|
494
463
|
0x9d2 /* unexpected negative clientSequenceNumber (empty batch should yield undefined) */,
|
|
495
464
|
);
|
|
465
|
+
} else {
|
|
466
|
+
addBatchMetadata(rawBatch, resubmitInfo?.batchId);
|
|
496
467
|
}
|
|
497
468
|
|
|
498
469
|
this.params.pendingStateManager.onFlushBatch(
|
|
499
470
|
rawBatch.messages,
|
|
500
471
|
clientSequenceNumber,
|
|
501
472
|
staged,
|
|
502
|
-
batchManager.options.ignoreBatchId,
|
|
503
473
|
);
|
|
504
474
|
}
|
|
505
475
|
|
|
@@ -511,7 +481,6 @@ export class Outbox {
|
|
|
511
481
|
*/
|
|
512
482
|
private rebase(rawBatch: LocalBatch, batchManager: BatchManager): void {
|
|
513
483
|
assert(!this.rebasing, 0x6fb /* Reentrancy */);
|
|
514
|
-
assert(batchManager.options.canRebase, 0x9a7 /* BatchManager does not support rebase */);
|
|
515
484
|
|
|
516
485
|
this.rebasing = true;
|
|
517
486
|
const squash = false;
|
|
@@ -538,7 +507,7 @@ export class Outbox {
|
|
|
538
507
|
this.batchRebasesToReport--;
|
|
539
508
|
}
|
|
540
509
|
|
|
541
|
-
this.flushInternal(
|
|
510
|
+
this.flushInternal(batchManager);
|
|
542
511
|
this.rebasing = false;
|
|
543
512
|
}
|
|
544
513
|
|
|
@@ -679,7 +648,6 @@ export class Outbox {
|
|
|
679
648
|
*/
|
|
680
649
|
public getBatchCheckpoints(): {
|
|
681
650
|
mainBatch: IBatchCheckpoint;
|
|
682
|
-
idAllocationBatch: IBatchCheckpoint;
|
|
683
651
|
blobAttachBatch: IBatchCheckpoint;
|
|
684
652
|
} {
|
|
685
653
|
// This variable is declared with a specific type so that we have a standard import of the IBatchCheckpoint type.
|
|
@@ -687,7 +655,6 @@ export class Outbox {
|
|
|
687
655
|
const mainBatch: IBatchCheckpoint = this.mainBatch.checkpoint();
|
|
688
656
|
return {
|
|
689
657
|
mainBatch,
|
|
690
|
-
idAllocationBatch: this.idAllocationBatch.checkpoint(),
|
|
691
658
|
blobAttachBatch: this.blobAttachBatch.checkpoint(),
|
|
692
659
|
};
|
|
693
660
|
}
|
package/src/packageVersion.ts
CHANGED
|
@@ -84,10 +84,6 @@ export interface IPendingMessage {
|
|
|
84
84
|
* length of the batch (how many runtime messages here)
|
|
85
85
|
*/
|
|
86
86
|
length: number;
|
|
87
|
-
/**
|
|
88
|
-
* If true, don't compare batchID of incoming batches to this. e.g. ID Allocation Batch IDs should be ignored
|
|
89
|
-
*/
|
|
90
|
-
ignoreBatchId?: boolean;
|
|
91
87
|
/**
|
|
92
88
|
* If true, this batch is staged and should not actually be submitted on replayPendingStates.
|
|
93
89
|
*/
|
|
@@ -335,7 +331,9 @@ export class PendingStateManager implements IDisposable {
|
|
|
335
331
|
return this.pendingMessagesCount !== 0;
|
|
336
332
|
}
|
|
337
333
|
|
|
338
|
-
public getLocalState(snapshotSequenceNumber?: number):
|
|
334
|
+
public getLocalState(snapshotSequenceNumber?: number): {
|
|
335
|
+
pending: IPendingLocalState;
|
|
336
|
+
} {
|
|
339
337
|
assert(
|
|
340
338
|
this.initialMessages.isEmpty(),
|
|
341
339
|
0x2e9 /* "Must call getLocalState() after applying initial states" */,
|
|
@@ -359,10 +357,12 @@ export class PendingStateManager implements IDisposable {
|
|
|
359
357
|
}
|
|
360
358
|
}
|
|
361
359
|
return {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
360
|
+
pending: {
|
|
361
|
+
pendingStates: [
|
|
362
|
+
...newSavedOps,
|
|
363
|
+
...this.pendingMessages.toArray().map((message) => toSerializableForm(message)),
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
366
|
};
|
|
367
367
|
}
|
|
368
368
|
|
|
@@ -403,13 +403,11 @@ export class PendingStateManager implements IDisposable {
|
|
|
403
403
|
* @param clientSequenceNumber - The CSN of the first message in the batch,
|
|
404
404
|
* or undefined if the batch was not yet sent (e.g. by the time we flushed we lost the connection)
|
|
405
405
|
* @param staged - Indicates whether batch is staged (not to be submitted while runtime is in Staging Mode)
|
|
406
|
-
* @param ignoreBatchId - Whether to ignore the batchId in the batchStartInfo
|
|
407
406
|
*/
|
|
408
407
|
public onFlushBatch(
|
|
409
408
|
batch: LocalBatchMessage[] | [LocalEmptyBatchPlaceholder],
|
|
410
409
|
clientSequenceNumber: number | undefined,
|
|
411
410
|
staged: boolean,
|
|
412
|
-
ignoreBatchId?: boolean,
|
|
413
411
|
): void {
|
|
414
412
|
// clientId and batchStartCsn are used for generating the batchId so we can detect container forks
|
|
415
413
|
// where this batch was submitted by two different clients rehydrating from the same local state.
|
|
@@ -427,7 +425,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
427
425
|
clientId !== undefined,
|
|
428
426
|
0xa33 /* clientId (from stateHandler) could only be undefined if we've never connected, but we have a CSN so we know that's not the case */,
|
|
429
427
|
);
|
|
430
|
-
|
|
428
|
+
const batchInfo = { clientId, batchStartCsn, length: batch.length, staged };
|
|
431
429
|
for (const message of batch) {
|
|
432
430
|
const {
|
|
433
431
|
runtimeOp,
|
|
@@ -438,12 +436,11 @@ export class PendingStateManager implements IDisposable {
|
|
|
438
436
|
const pendingMessage: IPendingMessage = {
|
|
439
437
|
type: "message",
|
|
440
438
|
referenceSequenceNumber,
|
|
441
|
-
content: serializeOp(runtimeOp),
|
|
439
|
+
content: serializeOp(runtimeOp).content,
|
|
442
440
|
runtimeOp,
|
|
443
441
|
localOpMetadata,
|
|
444
442
|
opMetadata,
|
|
445
|
-
|
|
446
|
-
batchInfo: { clientId, batchStartCsn, length: batch.length, ignoreBatchId, staged },
|
|
443
|
+
batchInfo,
|
|
447
444
|
};
|
|
448
445
|
this.pendingMessages.push(pendingMessage);
|
|
449
446
|
}
|
|
@@ -509,23 +506,14 @@ export class PendingStateManager implements IDisposable {
|
|
|
509
506
|
* @returns whether the batch IDs match
|
|
510
507
|
*/
|
|
511
508
|
private remoteBatchMatchesPendingBatch(remoteBatchStart: BatchStartInfo): boolean {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const firstIndexUsingBatchId = Array.from({
|
|
515
|
-
length: this.pendingMessages.length,
|
|
516
|
-
}).findIndex((_, i) => this.pendingMessages.get(i)?.batchInfo.ignoreBatchId !== true);
|
|
517
|
-
const pendingMessageUsingBatchId =
|
|
518
|
-
firstIndexUsingBatchId === -1
|
|
519
|
-
? undefined
|
|
520
|
-
: this.pendingMessages.get(firstIndexUsingBatchId);
|
|
521
|
-
|
|
522
|
-
if (pendingMessageUsingBatchId === undefined) {
|
|
509
|
+
const pendingMessage = this.pendingMessages.peekFront();
|
|
510
|
+
if (pendingMessage === undefined) {
|
|
523
511
|
return false;
|
|
524
512
|
}
|
|
525
513
|
|
|
526
514
|
// We must compare the effective batch IDs, since one of these ops
|
|
527
515
|
// may have been the original, not resubmitted, so wouldn't have its batch ID stamped yet.
|
|
528
|
-
const pendingBatchId = getEffectiveBatchId(
|
|
516
|
+
const pendingBatchId = getEffectiveBatchId(pendingMessage);
|
|
529
517
|
const inboundBatchId = getEffectiveBatchId(remoteBatchStart);
|
|
530
518
|
|
|
531
519
|
return pendingBatchId === inboundBatchId;
|
|
@@ -747,8 +735,12 @@ export class PendingStateManager implements IDisposable {
|
|
|
747
735
|
* Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
|
|
748
736
|
* states in its queue. This includes triggering resubmission of unacked ops.
|
|
749
737
|
* ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
|
|
738
|
+
*
|
|
739
|
+
* @returns The unique batch infos for all batches that were replayed.
|
|
750
740
|
*/
|
|
751
|
-
public replayPendingStates(
|
|
741
|
+
public replayPendingStates(
|
|
742
|
+
options?: ReplayPendingStateOptions,
|
|
743
|
+
): IPendingMessage["batchInfo"][] {
|
|
752
744
|
const { committingStagedBatches, squash } = {
|
|
753
745
|
...defaultReplayPendingStatesOptions,
|
|
754
746
|
...options,
|
|
@@ -775,6 +767,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
775
767
|
|
|
776
768
|
const initialPendingMessagesCount = this.pendingMessages.length;
|
|
777
769
|
let remainingPendingMessagesCount = this.pendingMessages.length;
|
|
770
|
+
const replayedBatchSet = new Set<IPendingMessage["batchInfo"]>();
|
|
778
771
|
|
|
779
772
|
let seenStagedBatch = false;
|
|
780
773
|
|
|
@@ -802,16 +795,14 @@ export class PendingStateManager implements IDisposable {
|
|
|
802
795
|
assert(batchMetadataFlag !== false, 0x41b /* We cannot process batches in chunks */);
|
|
803
796
|
|
|
804
797
|
// The next message starts a batch (possibly single-message), and we'll need its batchId.
|
|
805
|
-
const batchId =
|
|
806
|
-
pendingMessage.batchInfo.ignoreBatchId === true
|
|
807
|
-
? undefined
|
|
808
|
-
: getEffectiveBatchId(pendingMessage);
|
|
798
|
+
const batchId = getEffectiveBatchId(pendingMessage);
|
|
809
799
|
|
|
810
800
|
const staged = pendingMessage.batchInfo.staged;
|
|
811
801
|
|
|
812
802
|
if (asEmptyBatchLocalOpMetadata(pendingMessage.localOpMetadata)?.emptyBatch === true) {
|
|
813
803
|
// Resubmit no messages, with the batchId. Will result in another empty batch marker.
|
|
814
804
|
this.stateHandler.reSubmitBatch([], { batchId, staged, squash });
|
|
805
|
+
replayedBatchSet.add(pendingMessage.batchInfo);
|
|
815
806
|
continue;
|
|
816
807
|
}
|
|
817
808
|
|
|
@@ -838,6 +829,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
838
829
|
],
|
|
839
830
|
{ batchId, staged, squash },
|
|
840
831
|
);
|
|
832
|
+
replayedBatchSet.add(pendingMessage.batchInfo);
|
|
841
833
|
continue;
|
|
842
834
|
}
|
|
843
835
|
// else: batchMetadataFlag === true (It's a typical multi-message batch)
|
|
@@ -877,6 +869,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
877
869
|
}
|
|
878
870
|
|
|
879
871
|
this.stateHandler.reSubmitBatch(batch, { batchId, staged, squash });
|
|
872
|
+
replayedBatchSet.add(pendingMessage.batchInfo);
|
|
880
873
|
}
|
|
881
874
|
|
|
882
875
|
if (!committingStagedBatches) {
|
|
@@ -894,6 +887,8 @@ export class PendingStateManager implements IDisposable {
|
|
|
894
887
|
clientId: this.stateHandler.clientId(),
|
|
895
888
|
});
|
|
896
889
|
}
|
|
890
|
+
|
|
891
|
+
return [...replayedBatchSet];
|
|
897
892
|
}
|
|
898
893
|
|
|
899
894
|
/**
|
|
@@ -904,11 +899,13 @@ export class PendingStateManager implements IDisposable {
|
|
|
904
899
|
// callback will only be given staged messages with a valid runtime op (i.e. not empty batch and not an initial message with only serialized content)
|
|
905
900
|
stagedMessage: IPendingMessage & { runtimeOp: LocalContainerRuntimeMessage },
|
|
906
901
|
) => void,
|
|
907
|
-
):
|
|
902
|
+
): IPendingMessage["batchInfo"][] {
|
|
903
|
+
const batchSet = new Set<IPendingMessage["batchInfo"]>();
|
|
908
904
|
while (!this.pendingMessages.isEmpty()) {
|
|
909
905
|
const stagedMessage = this.pendingMessages.peekBack();
|
|
910
906
|
if (stagedMessage?.batchInfo.staged === true) {
|
|
911
907
|
this.pendingMessages.pop();
|
|
908
|
+
batchSet.add(stagedMessage.batchInfo);
|
|
912
909
|
|
|
913
910
|
if (hasTypicalRuntimeOp(stagedMessage)) {
|
|
914
911
|
callback(stagedMessage);
|
|
@@ -921,6 +918,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
921
918
|
this.pendingMessages.toArray().every((m) => m.batchInfo.staged !== true),
|
|
922
919
|
0xb89 /* Shouldn't be any more staged messages */,
|
|
923
920
|
);
|
|
921
|
+
return [...batchSet];
|
|
924
922
|
}
|
|
925
923
|
}
|
|
926
924
|
|
|
@@ -512,7 +512,7 @@ function arrayToProp(arr: string[]): string[] | undefined {
|
|
|
512
512
|
*
|
|
513
513
|
* Users of this class need to use DocumentsSchemaController.sessionSchema to determine what features can be used.
|
|
514
514
|
*
|
|
515
|
-
* There are
|
|
515
|
+
* There are three modes this class can operate:
|
|
516
516
|
* 1) Legacy mode. In such mode it does not issue any ops to change document schema. Any changes happen implicitly,
|
|
517
517
|
* right away, and new features are available right away
|
|
518
518
|
* 2) Non-legacy mode. In such mode any changes to schema require an op roundtrip. This class will manage such transitions.
|
|
@@ -523,6 +523,9 @@ function arrayToProp(arr: string[]): string[] | undefined {
|
|
|
523
523
|
* then eventually all documents that are modified will have that feature reflected in their schema. It could require
|
|
524
524
|
* multiple reloads / new sessions to get there (depends on if code reacts to schema changes right away, or only consults
|
|
525
525
|
* schema on document load).
|
|
526
|
+
* 3) Schema upgrade disabled mode (disableSchemaUpgrade = true). In this mode the controller will never send DocumentSchemaChange ops
|
|
527
|
+
* and will throw an error if any incoming schema change ops are received. The document schema is effectively frozen at the schema
|
|
528
|
+
* loaded for this session (snapshot) and will not accept further schema-change ops.
|
|
526
529
|
*
|
|
527
530
|
* How schemas are changed (in non-legacy mode):
|
|
528
531
|
* If a client needs to change a schema, it will attempt to do so as part of normal ops sending process.
|
|
@@ -569,6 +572,7 @@ export class DocumentsSchemaController {
|
|
|
569
572
|
* @param onSchemaChange - callback that is called whenever schema is changed (not called on creation / load, only when processing document schema change ops)
|
|
570
573
|
* @param info - Informational properties of the document that are not subject to strict schema enforcement
|
|
571
574
|
* @param logger - telemetry logger from the runtime
|
|
575
|
+
* @param disableSchemaUpgrade - when true, the controller will never send or accept DocumentSchemaChange ops
|
|
572
576
|
*/
|
|
573
577
|
constructor(
|
|
574
578
|
existing: boolean,
|
|
@@ -578,6 +582,7 @@ export class DocumentsSchemaController {
|
|
|
578
582
|
private readonly onSchemaChange: (schema: IDocumentSchemaCurrent) => void,
|
|
579
583
|
info: IDocumentSchemaInfo,
|
|
580
584
|
logger: ITelemetryLoggerExt,
|
|
585
|
+
private readonly disableSchemaUpgrade: boolean,
|
|
581
586
|
) {
|
|
582
587
|
// For simplicity, let's only support new schema features for explicit schema control mode
|
|
583
588
|
assert(
|
|
@@ -704,9 +709,12 @@ export class DocumentsSchemaController {
|
|
|
704
709
|
* Called by Container runtime whenever it is about to send some op.
|
|
705
710
|
* It gives opportunity for controller to issue its own ops - we do not want to send ops if there are no local changes in document.
|
|
706
711
|
* Please consider note above constructor about race conditions - current design is to generate op only once in a session lifetime.
|
|
707
|
-
* @returns Optional message to send.
|
|
712
|
+
* @returns Optional message to send. Always returns undefined when disableSchemaUpgrade is true.
|
|
708
713
|
*/
|
|
709
714
|
public maybeGenerateSchemaMessage(): IDocumentSchemaChangeMessageOutgoing | undefined {
|
|
715
|
+
if (this.disableSchemaUpgrade) {
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
710
718
|
if (this.futureSchema !== undefined && !this.opPending) {
|
|
711
719
|
this.opPending = true;
|
|
712
720
|
assert(
|
|
@@ -739,6 +747,7 @@ export class DocumentsSchemaController {
|
|
|
739
747
|
/**
|
|
740
748
|
* Process document schema change messages
|
|
741
749
|
* Called by ContainerRuntime whenever it sees document schema messages.
|
|
750
|
+
* When disableSchemaUpgrade is true, an error is thrown if any incoming schema change ops are received.
|
|
742
751
|
* @param contents - contents of the messages
|
|
743
752
|
* @param local - whether op is local
|
|
744
753
|
* @param sequenceNumber - sequence number of the op
|
|
@@ -749,6 +758,20 @@ export class DocumentsSchemaController {
|
|
|
749
758
|
local: boolean,
|
|
750
759
|
sequenceNumber: number,
|
|
751
760
|
): boolean {
|
|
761
|
+
if (this.disableSchemaUpgrade) {
|
|
762
|
+
assert(
|
|
763
|
+
!local,
|
|
764
|
+
0xceb /* local schema change messages should never be generated when disableSchemaUpgrade is enabled */,
|
|
765
|
+
);
|
|
766
|
+
// Clients with disableSchemaUpgrade enabled should never generate schema change messages, but they
|
|
767
|
+
// may receive them from misconfigured clients. In such case, throw on any incoming schema change ops
|
|
768
|
+
// to prevent unexpected schema upgrades.
|
|
769
|
+
throw DataProcessingError.create(
|
|
770
|
+
"DocSchema: Received schema change op while disableSchemaUpgrade is enabled",
|
|
771
|
+
"processDocumentSchemaMessages",
|
|
772
|
+
undefined,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
752
775
|
for (const content of contents) {
|
|
753
776
|
this.validateSeqNumber(content.refSeq, this.documentSchema.refSeq, "content.refSeq");
|
|
754
777
|
this.validateSeqNumber(this.documentSchema.refSeq, sequenceNumber, "refSeq");
|
|
@@ -515,7 +515,7 @@ export class OrderedClientElection
|
|
|
515
515
|
"InteractiveClientElected",
|
|
516
516
|
client,
|
|
517
517
|
sequenceNumber,
|
|
518
|
-
|
|
518
|
+
false /* forceSend */,
|
|
519
519
|
reason,
|
|
520
520
|
);
|
|
521
521
|
// Changing the elected parent as well.
|
|
@@ -544,7 +544,7 @@ export class OrderedClientElection
|
|
|
544
544
|
"ParentElected",
|
|
545
545
|
client,
|
|
546
546
|
sequenceNumber,
|
|
547
|
-
|
|
547
|
+
false /* forceSend */,
|
|
548
548
|
reason,
|
|
549
549
|
);
|
|
550
550
|
this._electedParent = client;
|