@fluidframework/container-runtime 2.101.0 → 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 +26 -2
- 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 +31 -3
- package/dist/opLifecycle/duplicateBatchDetector.d.ts.map +1 -1
- package/dist/opLifecycle/duplicateBatchDetector.js +39 -19
- 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 +26 -2
- 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 +31 -3
- package/lib/opLifecycle/duplicateBatchDetector.d.ts.map +1 -1
- package/lib/opLifecycle/duplicateBatchDetector.js +39 -19
- 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 +28 -2
- package/src/dataStoreContext.ts +2 -4
- package/src/gc/garbageCollection.ts +12 -5
- package/src/opLifecycle/duplicateBatchDetector.ts +81 -22
- package/src/packageVersion.ts +1 -1
- package/src/summary/documentSchema.ts +56 -3
|
@@ -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,11 @@ 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.
|
|
28
65
|
*/
|
|
29
|
-
private readonly
|
|
66
|
+
private readonly batchesBySeqNum = new Map<number, RecordedBatch>();
|
|
30
67
|
|
|
31
68
|
/**
|
|
32
69
|
* Number of inbound batches processed since the last summary. Reset by getRecentBatchInfoForSummary.
|
|
@@ -44,22 +81,29 @@ export class DuplicateBatchDetector {
|
|
|
44
81
|
constructor(batchIdsFromSnapshot: [number, string][] | undefined) {
|
|
45
82
|
if (batchIdsFromSnapshot) {
|
|
46
83
|
for (const [seqNum, batchId] of batchIdsFromSnapshot) {
|
|
47
|
-
|
|
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 });
|
|
48
87
|
this.seqNumByBatchId.set(batchId, seqNum);
|
|
49
88
|
}
|
|
50
|
-
this.peakTrackedBatchCount = this.
|
|
89
|
+
this.peakTrackedBatchCount = this.batchesBySeqNum.size;
|
|
51
90
|
}
|
|
52
91
|
}
|
|
53
92
|
|
|
54
93
|
/**
|
|
55
94
|
* Records this batch's batchId, and checks if it's a duplicate of a batch we've already seen.
|
|
56
|
-
* 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.
|
|
57
97
|
*
|
|
58
98
|
* @remarks We also use the minimumSequenceNumber to clear out old batchIds that are no longer at risk for duplicates.
|
|
59
99
|
*/
|
|
60
|
-
public processInboundBatch(
|
|
61
|
-
|
|
62
|
-
|
|
100
|
+
public processInboundBatch(batchStart: BatchStartInfo):
|
|
101
|
+
| {
|
|
102
|
+
duplicate: true;
|
|
103
|
+
otherSequenceNumber: number;
|
|
104
|
+
otherBatchInfo: RecordedBatchInfo | undefined;
|
|
105
|
+
}
|
|
106
|
+
| { duplicate: false } {
|
|
63
107
|
const { sequenceNumber, minimumSequenceNumber } = batchStart.keyMessage;
|
|
64
108
|
this.processedBatchCount++;
|
|
65
109
|
|
|
@@ -76,24 +120,36 @@ export class DuplicateBatchDetector {
|
|
|
76
120
|
// O(1) duplicate check + get otherSequenceNumber in one lookup
|
|
77
121
|
const otherSequenceNumber = this.seqNumByBatchId.get(batchId);
|
|
78
122
|
if (otherSequenceNumber !== undefined) {
|
|
123
|
+
const other = this.batchesBySeqNum.get(otherSequenceNumber);
|
|
79
124
|
assert(
|
|
80
|
-
|
|
125
|
+
other?.batchId === batchId,
|
|
81
126
|
0xce0 /* batchIdToSeqNum and seqNumToBatchId should be in sync for duplicate */,
|
|
82
127
|
);
|
|
83
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
duplicate: true,
|
|
130
|
+
otherSequenceNumber,
|
|
131
|
+
otherBatchInfo: other.info,
|
|
132
|
+
};
|
|
84
133
|
}
|
|
85
134
|
|
|
86
135
|
// Now we know it's not a duplicate, so add it to the tracked batchIds and return.
|
|
87
136
|
assert(
|
|
88
|
-
!this.
|
|
137
|
+
!this.batchesBySeqNum.has(sequenceNumber),
|
|
89
138
|
0xce1 /* seqNumToBatchId and batchIdToSeqNum should be in sync */,
|
|
90
139
|
);
|
|
91
140
|
|
|
92
|
-
// Add new batch
|
|
93
|
-
|
|
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 });
|
|
94
150
|
this.seqNumByBatchId.set(batchId, sequenceNumber);
|
|
95
|
-
if (this.
|
|
96
|
-
this.peakTrackedBatchCount = this.
|
|
151
|
+
if (this.batchesBySeqNum.size > this.peakTrackedBatchCount) {
|
|
152
|
+
this.peakTrackedBatchCount = this.batchesBySeqNum.size;
|
|
97
153
|
}
|
|
98
154
|
|
|
99
155
|
return { duplicate: false };
|
|
@@ -104,10 +160,10 @@ export class DuplicateBatchDetector {
|
|
|
104
160
|
* since the batch start has been processed by all clients, and local batches are deduped and the forked client would close.
|
|
105
161
|
*/
|
|
106
162
|
private clearOldBatchIds(msn: number): void {
|
|
107
|
-
for (const [sequenceNumber,
|
|
163
|
+
for (const [sequenceNumber, recorded] of this.batchesBySeqNum) {
|
|
108
164
|
if (sequenceNumber < msn) {
|
|
109
|
-
this.
|
|
110
|
-
this.seqNumByBatchId.delete(batchId);
|
|
165
|
+
this.batchesBySeqNum.delete(sequenceNumber);
|
|
166
|
+
this.seqNumByBatchId.delete(recorded.batchId);
|
|
111
167
|
} else {
|
|
112
168
|
break;
|
|
113
169
|
}
|
|
@@ -125,7 +181,7 @@ export class DuplicateBatchDetector {
|
|
|
125
181
|
): [number, string][] | undefined {
|
|
126
182
|
if (telemetryContext !== undefined) {
|
|
127
183
|
const prefix = "fluid_DuplicateBatchDetector_";
|
|
128
|
-
telemetryContext.set(prefix, "recentBatchCount", this.
|
|
184
|
+
telemetryContext.set(prefix, "recentBatchCount", this.batchesBySeqNum.size);
|
|
129
185
|
telemetryContext.set(prefix, "peakRecentBatchCount", this.peakTrackedBatchCount);
|
|
130
186
|
telemetryContext.set(prefix, "processedBatchCount", this.processedBatchCount);
|
|
131
187
|
}
|
|
@@ -133,12 +189,15 @@ export class DuplicateBatchDetector {
|
|
|
133
189
|
// Reset per-window perf counters so each summary covers only the activity since the
|
|
134
190
|
// previous one. Peak resets to the current size (the floor for the next window).
|
|
135
191
|
this.processedBatchCount = 0;
|
|
136
|
-
this.peakTrackedBatchCount = this.
|
|
192
|
+
this.peakTrackedBatchCount = this.batchesBySeqNum.size;
|
|
137
193
|
|
|
138
|
-
if (this.
|
|
194
|
+
if (this.batchesBySeqNum.size === 0) {
|
|
139
195
|
return undefined;
|
|
140
196
|
}
|
|
141
197
|
|
|
142
|
-
return [...this.
|
|
198
|
+
return [...this.batchesBySeqNum.entries()].map(([seqNum, recorded]) => [
|
|
199
|
+
seqNum,
|
|
200
|
+
recorded.batchId,
|
|
201
|
+
]);
|
|
143
202
|
}
|
|
144
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
|
|