@fluidframework/container-runtime 2.101.1 → 2.102.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 +4 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +48 -9
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +1 -4
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/gc/garbageCollection.d.ts.map +1 -1
- package/dist/gc/garbageCollection.js +12 -5
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/opLifecycle/duplicateBatchDetector.d.ts +39 -3
- package/dist/opLifecycle/duplicateBatchDetector.d.ts.map +1 -1
- package/dist/opLifecycle/duplicateBatchDetector.js +57 -15
- package/dist/opLifecycle/duplicateBatchDetector.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/runtimeLayerCompatState.d.ts +2 -2
- package/dist/summary/documentSchema.d.ts.map +1 -1
- package/dist/summary/documentSchema.js +35 -3
- package/dist/summary/documentSchema.js.map +1 -1
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +48 -9
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +2 -5
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/gc/garbageCollection.d.ts.map +1 -1
- package/lib/gc/garbageCollection.js +12 -5
- package/lib/gc/garbageCollection.js.map +1 -1
- package/lib/opLifecycle/duplicateBatchDetector.d.ts +39 -3
- package/lib/opLifecycle/duplicateBatchDetector.d.ts.map +1 -1
- package/lib/opLifecycle/duplicateBatchDetector.js +57 -15
- package/lib/opLifecycle/duplicateBatchDetector.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/runtimeLayerCompatState.d.ts +2 -2
- package/lib/summary/documentSchema.d.ts.map +1 -1
- package/lib/summary/documentSchema.js +35 -3
- package/lib/summary/documentSchema.js.map +1 -1
- package/package.json +18 -18
- package/src/containerRuntime.ts +54 -10
- package/src/dataStoreContext.ts +2 -4
- package/src/gc/garbageCollection.ts +12 -5
- package/src/opLifecycle/duplicateBatchDetector.ts +103 -23
- package/src/packageVersion.ts +1 -1
- package/src/summary/documentSchema.ts +56 -3
|
@@ -840,14 +840,21 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
840
840
|
const gcDataSuperSet = concatGarbageCollectionData(previousGCData, currentGCData);
|
|
841
841
|
const newOutboundRoutesSinceLastRun: string[] = [];
|
|
842
842
|
for (const [sourceNodeId, outboundRoutes] of this.newReferencesSinceLastRun) {
|
|
843
|
-
|
|
843
|
+
const target: string[] | undefined = gcDataSuperSet.gcNodes[sourceNodeId];
|
|
844
|
+
if (target === undefined) {
|
|
844
845
|
gcDataSuperSet.gcNodes[sourceNodeId] = outboundRoutes;
|
|
845
846
|
} else {
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
|
|
847
|
+
// Avoid `push(...outboundRoutes)`: spreading a large array into a variadic call
|
|
848
|
+
// can exceed the engine's argument-count limit and throw RangeError.
|
|
849
|
+
for (const route of outboundRoutes) {
|
|
850
|
+
target.push(route);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
// Avoid `push(...outboundRoutes)`: spreading a large array into a variadic call
|
|
854
|
+
// can exceed the engine's argument-count limit and throw RangeError.
|
|
855
|
+
for (const route of outboundRoutes) {
|
|
856
|
+
newOutboundRoutesSinceLastRun.push(route);
|
|
849
857
|
}
|
|
850
|
-
newOutboundRoutesSinceLastRun.push(...outboundRoutes);
|
|
851
858
|
}
|
|
852
859
|
|
|
853
860
|
/**
|
|
@@ -9,6 +9,41 @@ import type { ITelemetryContext } from "@fluidframework/runtime-definitions/inte
|
|
|
9
9
|
import { getEffectiveBatchId } from "./batchManager.js";
|
|
10
10
|
import type { BatchStartInfo } from "./remoteMessageProcessor.js";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Identifying info for a previously-recorded batch that we can include in DuplicateBatch telemetry
|
|
14
|
+
* to help diagnose where the duplicate came from.
|
|
15
|
+
*
|
|
16
|
+
* @remarks `batchIdExplicit` distinguishes the two main duplicate-source scenarios:
|
|
17
|
+
* - `true`: the batchId was stamped on the wire as explicit metadata, indicating a resubmit (PendingStateManager.replayPendingStates).
|
|
18
|
+
* - `false`: the batchId was derived from the wire `clientId` and `batchStartCsn`, indicating a fresh, non-resubmit batch.
|
|
19
|
+
*/
|
|
20
|
+
export interface RecordedBatchInfo {
|
|
21
|
+
/**
|
|
22
|
+
* Wire clientId on the message that started the batch (NOT necessarily the `originalClientId`
|
|
23
|
+
* encoded in the batchId for resubmits).
|
|
24
|
+
*/
|
|
25
|
+
readonly clientId: string;
|
|
26
|
+
/**
|
|
27
|
+
* Wire client sequence number at the start of the batch.
|
|
28
|
+
*/
|
|
29
|
+
readonly batchStartCsn: number;
|
|
30
|
+
/**
|
|
31
|
+
* True if the batchId came from explicit metadata on the wire (i.e. a resubmit),
|
|
32
|
+
* false if it was derived from clientId + batchStartCsn (i.e. a fresh submit).
|
|
33
|
+
*/
|
|
34
|
+
readonly batchIdExplicit: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface RecordedBatch {
|
|
38
|
+
readonly batchId: string;
|
|
39
|
+
/**
|
|
40
|
+
* Identifying info for the batch as observed at runtime.
|
|
41
|
+
* `undefined` if the batch was loaded from a summary snapshot (where only the
|
|
42
|
+
* `[seqNum, batchId]` pair is persisted).
|
|
43
|
+
*/
|
|
44
|
+
readonly info: RecordedBatchInfo | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
12
47
|
/**
|
|
13
48
|
* Detects duplicate batches that can arise from the "parallel fork" scenario:
|
|
14
49
|
* Container 1 is serialized, and Containers 2 and 3 are rehydrated from that state.
|
|
@@ -24,9 +59,21 @@ export class DuplicateBatchDetector {
|
|
|
24
59
|
private readonly seqNumByBatchId = new Map<string, number>();
|
|
25
60
|
|
|
26
61
|
/**
|
|
27
|
-
*
|
|
62
|
+
* Map from sequenceNumber to the recorded batch info. Used to clear out old entries as MSN
|
|
63
|
+
* advances, and to report identifying info about the original occurrence when a duplicate
|
|
64
|
+
* is detected.
|
|
65
|
+
*/
|
|
66
|
+
private readonly batchesBySeqNum = new Map<number, RecordedBatch>();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Number of inbound batches processed since the last summary. Reset by getRecentBatchInfoForSummary.
|
|
70
|
+
*/
|
|
71
|
+
private processedBatchCount = 0;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Largest tracked-batch count observed since the last summary. Reset by getRecentBatchInfoForSummary.
|
|
28
75
|
*/
|
|
29
|
-
private
|
|
76
|
+
private peakTrackedBatchCount = 0;
|
|
30
77
|
|
|
31
78
|
/**
|
|
32
79
|
* Initialize from snapshot data if provided - otherwise initialize empty
|
|
@@ -34,22 +81,31 @@ export class DuplicateBatchDetector {
|
|
|
34
81
|
constructor(batchIdsFromSnapshot: [number, string][] | undefined) {
|
|
35
82
|
if (batchIdsFromSnapshot) {
|
|
36
83
|
for (const [seqNum, batchId] of batchIdsFromSnapshot) {
|
|
37
|
-
|
|
84
|
+
// Entries loaded from a snapshot don't carry the original clientId/csn/explicit-bit;
|
|
85
|
+
// we record them with `info: undefined` so duplicate telemetry can indicate that.
|
|
86
|
+
this.batchesBySeqNum.set(seqNum, { batchId, info: undefined });
|
|
38
87
|
this.seqNumByBatchId.set(batchId, seqNum);
|
|
39
88
|
}
|
|
89
|
+
this.peakTrackedBatchCount = this.batchesBySeqNum.size;
|
|
40
90
|
}
|
|
41
91
|
}
|
|
42
92
|
|
|
43
93
|
/**
|
|
44
94
|
* Records this batch's batchId, and checks if it's a duplicate of a batch we've already seen.
|
|
45
|
-
* If it's a duplicate, also return the sequence number of the other batch
|
|
95
|
+
* If it's a duplicate, also return the sequence number of the other batch (and identifying info,
|
|
96
|
+
* if the other batch was seen during this container session rather than loaded from snapshot) for logging.
|
|
46
97
|
*
|
|
47
98
|
* @remarks We also use the minimumSequenceNumber to clear out old batchIds that are no longer at risk for duplicates.
|
|
48
99
|
*/
|
|
49
|
-
public processInboundBatch(
|
|
50
|
-
|
|
51
|
-
|
|
100
|
+
public processInboundBatch(batchStart: BatchStartInfo):
|
|
101
|
+
| {
|
|
102
|
+
duplicate: true;
|
|
103
|
+
otherSequenceNumber: number;
|
|
104
|
+
otherBatchInfo: RecordedBatchInfo | undefined;
|
|
105
|
+
}
|
|
106
|
+
| { duplicate: false } {
|
|
52
107
|
const { sequenceNumber, minimumSequenceNumber } = batchStart.keyMessage;
|
|
108
|
+
this.processedBatchCount++;
|
|
53
109
|
|
|
54
110
|
// Glance at this batch's MSN. Any batchIds we're tracking with a lower sequence number are now safe to forget.
|
|
55
111
|
// Why? Because any other client holding the same batch locally would have seen the earlier batch and closed before submitting its duplicate.
|
|
@@ -64,22 +120,37 @@ export class DuplicateBatchDetector {
|
|
|
64
120
|
// O(1) duplicate check + get otherSequenceNumber in one lookup
|
|
65
121
|
const otherSequenceNumber = this.seqNumByBatchId.get(batchId);
|
|
66
122
|
if (otherSequenceNumber !== undefined) {
|
|
123
|
+
const other = this.batchesBySeqNum.get(otherSequenceNumber);
|
|
67
124
|
assert(
|
|
68
|
-
|
|
125
|
+
other?.batchId === batchId,
|
|
69
126
|
0xce0 /* batchIdToSeqNum and seqNumToBatchId should be in sync for duplicate */,
|
|
70
127
|
);
|
|
71
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
duplicate: true,
|
|
130
|
+
otherSequenceNumber,
|
|
131
|
+
otherBatchInfo: other.info,
|
|
132
|
+
};
|
|
72
133
|
}
|
|
73
134
|
|
|
74
135
|
// Now we know it's not a duplicate, so add it to the tracked batchIds and return.
|
|
75
136
|
assert(
|
|
76
|
-
!this.
|
|
137
|
+
!this.batchesBySeqNum.has(sequenceNumber),
|
|
77
138
|
0xce1 /* seqNumToBatchId and batchIdToSeqNum should be in sync */,
|
|
78
139
|
);
|
|
79
140
|
|
|
80
|
-
// Add new batch
|
|
81
|
-
|
|
141
|
+
// Add new batch. Record identifying info so we can report it if a future duplicate matches us.
|
|
142
|
+
const info: RecordedBatchInfo | undefined = {
|
|
143
|
+
clientId: batchStart.clientId,
|
|
144
|
+
batchStartCsn: batchStart.batchStartCsn,
|
|
145
|
+
// True iff the wire carried explicit batchId metadata (resubmit path).
|
|
146
|
+
// False indicates the batchId was derived from clientId + batchStartCsn (fresh submit).
|
|
147
|
+
batchIdExplicit: batchStart.batchId !== undefined,
|
|
148
|
+
};
|
|
149
|
+
this.batchesBySeqNum.set(sequenceNumber, { batchId, info });
|
|
82
150
|
this.seqNumByBatchId.set(batchId, sequenceNumber);
|
|
151
|
+
if (this.batchesBySeqNum.size > this.peakTrackedBatchCount) {
|
|
152
|
+
this.peakTrackedBatchCount = this.batchesBySeqNum.size;
|
|
153
|
+
}
|
|
83
154
|
|
|
84
155
|
return { duplicate: false };
|
|
85
156
|
}
|
|
@@ -89,10 +160,10 @@ export class DuplicateBatchDetector {
|
|
|
89
160
|
* since the batch start has been processed by all clients, and local batches are deduped and the forked client would close.
|
|
90
161
|
*/
|
|
91
162
|
private clearOldBatchIds(msn: number): void {
|
|
92
|
-
for (const [sequenceNumber,
|
|
163
|
+
for (const [sequenceNumber, recorded] of this.batchesBySeqNum) {
|
|
93
164
|
if (sequenceNumber < msn) {
|
|
94
|
-
this.
|
|
95
|
-
this.seqNumByBatchId.delete(batchId);
|
|
165
|
+
this.batchesBySeqNum.delete(sequenceNumber);
|
|
166
|
+
this.seqNumByBatchId.delete(recorded.batchId);
|
|
96
167
|
} else {
|
|
97
168
|
break;
|
|
98
169
|
}
|
|
@@ -108,16 +179,25 @@ export class DuplicateBatchDetector {
|
|
|
108
179
|
public getRecentBatchInfoForSummary(
|
|
109
180
|
telemetryContext?: ITelemetryContext,
|
|
110
181
|
): [number, string][] | undefined {
|
|
111
|
-
if (
|
|
112
|
-
|
|
182
|
+
if (telemetryContext !== undefined) {
|
|
183
|
+
const prefix = "fluid_DuplicateBatchDetector_";
|
|
184
|
+
telemetryContext.set(prefix, "recentBatchCount", this.batchesBySeqNum.size);
|
|
185
|
+
telemetryContext.set(prefix, "peakRecentBatchCount", this.peakTrackedBatchCount);
|
|
186
|
+
telemetryContext.set(prefix, "processedBatchCount", this.processedBatchCount);
|
|
113
187
|
}
|
|
114
188
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
189
|
+
// Reset per-window perf counters so each summary covers only the activity since the
|
|
190
|
+
// previous one. Peak resets to the current size (the floor for the next window).
|
|
191
|
+
this.processedBatchCount = 0;
|
|
192
|
+
this.peakTrackedBatchCount = this.batchesBySeqNum.size;
|
|
193
|
+
|
|
194
|
+
if (this.batchesBySeqNum.size === 0) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
120
197
|
|
|
121
|
-
return [...this.
|
|
198
|
+
return [...this.batchesBySeqNum.entries()].map(([seqNum, recorded]) => [
|
|
199
|
+
seqNum,
|
|
200
|
+
recorded.batchId,
|
|
201
|
+
]);
|
|
122
202
|
}
|
|
123
203
|
}
|
package/src/packageVersion.ts
CHANGED
|
@@ -303,6 +303,58 @@ const documentSchemaSupportedConfigs = {
|
|
|
303
303
|
disallowedVersions: new CheckVersions(),
|
|
304
304
|
};
|
|
305
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Entry in {@link retiredDocumentSchemaFeatures}.
|
|
308
|
+
*
|
|
309
|
+
* - `handler` validates/merges the persisted value (same as a "non-retired" feature).
|
|
310
|
+
* - `value` is the hardcoded value that this runtime will always use for the desired schema.
|
|
311
|
+
*/
|
|
312
|
+
interface IRetiredFeatureEntry {
|
|
313
|
+
handler: IProperty;
|
|
314
|
+
value: DocumentSchemaValueType;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Retired runtime features. A retired feature is one this runtime version no longer toggles
|
|
319
|
+
* via {@link IDocumentSchemaFeatures}, but that older documents may still carry in their
|
|
320
|
+
* persisted schema. Each entry bundles the property handler with the hardcoded value for the feature.
|
|
321
|
+
*
|
|
322
|
+
* Retired features participate in the normal merge / schema-change-op flow exactly like
|
|
323
|
+
* non-retired features — the only difference is that their values are hardcoded.
|
|
324
|
+
*/
|
|
325
|
+
const retiredDocumentSchemaFeatures = {
|
|
326
|
+
// Note: There are currently no retired retired features. To retire a feature, remove it from IDocumentSchemaFeatures
|
|
327
|
+
// and documentSchemaSupportedConfigs and add an entry here, e.g.:
|
|
328
|
+
// featureFoo: { handler: new TrueOrUndefined(), value: true },
|
|
329
|
+
} satisfies Record<string, IRetiredFeatureEntry> & {
|
|
330
|
+
// This ensures that retiredDocumentSchemaFeatures and IDocumentSchemaFeatures are mutually exclusive.
|
|
331
|
+
[K in keyof IDocumentSchemaFeatures]?: never;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Looks up the validator/merger for a given runtime property name across both supported and
|
|
336
|
+
* retired configs. Returns `undefined` if the property is unknown to this runtime.
|
|
337
|
+
*/
|
|
338
|
+
function getRuntimeConfigHandler(name: string): IProperty | undefined {
|
|
339
|
+
return (
|
|
340
|
+
(documentSchemaSupportedConfigs as Record<string, IProperty>)[name] ??
|
|
341
|
+
(retiredDocumentSchemaFeatures as Record<string, IRetiredFeatureEntry>)[name]?.handler
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Builds the `{ key: value }` for retired features (for building the desired schema).
|
|
347
|
+
*/
|
|
348
|
+
function retiredFeatureValues(): Record<string, DocumentSchemaValueType> {
|
|
349
|
+
const result: Record<string, DocumentSchemaValueType> = {};
|
|
350
|
+
for (const [key, entry] of Object.entries(
|
|
351
|
+
retiredDocumentSchemaFeatures as Record<string, IRetiredFeatureEntry>,
|
|
352
|
+
)) {
|
|
353
|
+
result[key] = entry.value;
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
|
|
306
358
|
/**
|
|
307
359
|
* Checks if a given schema is compatible with current code, i.e. if current code can understand all the features of that schema.
|
|
308
360
|
* If schema is not compatible with current code, it throws an exception.
|
|
@@ -343,7 +395,7 @@ function checkRuntimeCompatibility(
|
|
|
343
395
|
unknownProperty = "runtime";
|
|
344
396
|
} else {
|
|
345
397
|
for (const [name, value] of Object.entries(documentSchema.runtime)) {
|
|
346
|
-
const validator =
|
|
398
|
+
const validator = getRuntimeConfigHandler(name);
|
|
347
399
|
if (!(validator?.validate(value) ?? false)) {
|
|
348
400
|
unknownProperty = `runtime/${name}`;
|
|
349
401
|
}
|
|
@@ -377,7 +429,7 @@ function and(
|
|
|
377
429
|
...Object.keys(persistedSchema.runtime),
|
|
378
430
|
...Object.keys(providedSchema.runtime),
|
|
379
431
|
])) {
|
|
380
|
-
runtime[key] = (
|
|
432
|
+
runtime[key] = (getRuntimeConfigHandler(key) as IProperty).and(
|
|
381
433
|
persistedSchema.runtime[key],
|
|
382
434
|
providedSchema.runtime[key],
|
|
383
435
|
);
|
|
@@ -405,7 +457,7 @@ function or(
|
|
|
405
457
|
...Object.keys(persistedSchema.runtime),
|
|
406
458
|
...Object.keys(providedSchema.runtime),
|
|
407
459
|
])) {
|
|
408
|
-
runtime[key] = (
|
|
460
|
+
runtime[key] = (getRuntimeConfigHandler(key) as IProperty).or(
|
|
409
461
|
persistedSchema.runtime[key],
|
|
410
462
|
providedSchema.runtime[key],
|
|
411
463
|
);
|
|
@@ -625,6 +677,7 @@ export class DocumentsSchemaController {
|
|
|
625
677
|
opGroupingEnabled: boolToProp(features.opGroupingEnabled),
|
|
626
678
|
createBlobPayloadPending: features.createBlobPayloadPending,
|
|
627
679
|
disallowedVersions: arrayToProp(features.disallowedVersions),
|
|
680
|
+
...retiredFeatureValues(),
|
|
628
681
|
},
|
|
629
682
|
};
|
|
630
683
|
|