@fluidframework/container-runtime 2.93.0 → 2.101.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 +31 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/blobManager/blobManager.d.ts +3 -0
- package/dist/blobManager/blobManager.d.ts.map +1 -1
- package/dist/blobManager/blobManager.js +3 -0
- package/dist/blobManager/blobManager.js.map +1 -1
- package/dist/blobManager/blobManagerSnapSum.d.ts +3 -0
- package/dist/blobManager/blobManagerSnapSum.d.ts.map +1 -1
- package/dist/blobManager/blobManagerSnapSum.js +12 -5
- package/dist/blobManager/blobManagerSnapSum.js.map +1 -1
- package/dist/connectionTelemetry.d.ts.map +1 -1
- package/dist/connectionTelemetry.js +3 -1
- package/dist/connectionTelemetry.js.map +1 -1
- package/dist/containerRuntime.d.ts +20 -3
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +88 -39
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.d.ts +1 -1
- package/dist/dataStore.d.ts.map +1 -1
- package/dist/dataStore.js +6 -7
- package/dist/dataStore.js.map +1 -1
- package/dist/gc/garbageCollection.d.ts +4 -0
- package/dist/gc/garbageCollection.d.ts.map +1 -1
- package/dist/gc/garbageCollection.js +3 -2
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/gc/gcDefinitions.d.ts +10 -4
- package/dist/gc/gcDefinitions.d.ts.map +1 -1
- package/dist/gc/gcDefinitions.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -12
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +14 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js.map +1 -1
- package/dist/opLifecycle/duplicateBatchDetector.d.ts +8 -0
- package/dist/opLifecycle/duplicateBatchDetector.d.ts.map +1 -1
- package/dist/opLifecycle/duplicateBatchDetector.js +23 -1
- package/dist/opLifecycle/duplicateBatchDetector.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.js +2 -2
- package/dist/opLifecycle/opGroupingManager.js.map +1 -1
- package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
- package/dist/opLifecycle/opSplitter.js +13 -3
- package/dist/opLifecycle/opSplitter.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/runtimeLayerCompatState.d.ts +1 -1
- package/dist/summary/summarizerTypes.d.ts +3 -1
- package/dist/summary/summarizerTypes.d.ts.map +1 -1
- package/dist/summary/summarizerTypes.js.map +1 -1
- package/lib/blobManager/blobManager.d.ts +3 -0
- package/lib/blobManager/blobManager.d.ts.map +1 -1
- package/lib/blobManager/blobManager.js +3 -0
- package/lib/blobManager/blobManager.js.map +1 -1
- package/lib/blobManager/blobManagerSnapSum.d.ts +3 -0
- package/lib/blobManager/blobManagerSnapSum.d.ts.map +1 -1
- package/lib/blobManager/blobManagerSnapSum.js +12 -5
- package/lib/blobManager/blobManagerSnapSum.js.map +1 -1
- package/lib/connectionTelemetry.d.ts.map +1 -1
- package/lib/connectionTelemetry.js +3 -1
- package/lib/connectionTelemetry.js.map +1 -1
- package/lib/containerRuntime.d.ts +20 -3
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +90 -41
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.d.ts +1 -1
- package/lib/dataStore.d.ts.map +1 -1
- package/lib/dataStore.js +1 -2
- package/lib/dataStore.js.map +1 -1
- package/lib/gc/garbageCollection.d.ts +4 -0
- package/lib/gc/garbageCollection.d.ts.map +1 -1
- package/lib/gc/garbageCollection.js +3 -2
- package/lib/gc/garbageCollection.js.map +1 -1
- package/lib/gc/gcDefinitions.d.ts +10 -4
- package/lib/gc/gcDefinitions.d.ts.map +1 -1
- package/lib/gc/gcDefinitions.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/metadata.d.ts +14 -0
- package/lib/metadata.d.ts.map +1 -1
- package/lib/metadata.js.map +1 -1
- package/lib/opLifecycle/duplicateBatchDetector.d.ts +8 -0
- package/lib/opLifecycle/duplicateBatchDetector.d.ts.map +1 -1
- package/lib/opLifecycle/duplicateBatchDetector.js +23 -1
- package/lib/opLifecycle/duplicateBatchDetector.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.js +2 -2
- package/lib/opLifecycle/opGroupingManager.js.map +1 -1
- package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
- package/lib/opLifecycle/opSplitter.js +13 -3
- package/lib/opLifecycle/opSplitter.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/runtimeLayerCompatState.d.ts +1 -1
- package/lib/summary/summarizerTypes.d.ts +3 -1
- package/lib/summary/summarizerTypes.d.ts.map +1 -1
- package/lib/summary/summarizerTypes.js.map +1 -1
- package/package.json +20 -20
- package/src/blobManager/blobManager.ts +3 -0
- package/src/blobManager/blobManagerSnapSum.ts +12 -5
- package/src/connectionTelemetry.ts +15 -10
- package/src/containerRuntime.ts +110 -47
- package/src/dataStore.ts +5 -6
- package/src/gc/garbageCollection.ts +8 -6
- package/src/gc/gcDefinitions.ts +11 -4
- package/src/index.ts +5 -0
- package/src/metadata.ts +14 -0
- package/src/opLifecycle/duplicateBatchDetector.ts +27 -6
- package/src/opLifecycle/opGroupingManager.ts +2 -2
- package/src/opLifecycle/opSplitter.ts +13 -3
- package/src/packageVersion.ts +1 -1
- package/src/summary/summarizerTypes.ts +3 -1
package/src/containerRuntime.ts
CHANGED
|
@@ -57,6 +57,7 @@ import type {
|
|
|
57
57
|
ITelemetryBaseLogger,
|
|
58
58
|
Listenable,
|
|
59
59
|
} from "@fluidframework/core-interfaces";
|
|
60
|
+
import { LogLevel } from "@fluidframework/core-interfaces";
|
|
60
61
|
import type {
|
|
61
62
|
IFluidHandleContext,
|
|
62
63
|
IFluidHandleInternal,
|
|
@@ -95,7 +96,6 @@ import { FetchSource, MessageType } from "@fluidframework/driver-definitions/int
|
|
|
95
96
|
import { readAndParse } from "@fluidframework/driver-utils/internal";
|
|
96
97
|
import type { IIdCompressor } from "@fluidframework/id-compressor";
|
|
97
98
|
import type {
|
|
98
|
-
// eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
|
|
99
99
|
IIdCompressorCore,
|
|
100
100
|
IdCreationRange,
|
|
101
101
|
SerializedIdCompressorWithNoSession,
|
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
createIdCompressor,
|
|
106
106
|
createSessionId,
|
|
107
107
|
deserializeIdCompressor,
|
|
108
|
+
toIdCompressorWithCore,
|
|
108
109
|
} from "@fluidframework/id-compressor/internal";
|
|
109
110
|
import {
|
|
110
111
|
FlushMode,
|
|
@@ -131,7 +132,6 @@ import type {
|
|
|
131
132
|
IContainerRuntimeBaseInternal,
|
|
132
133
|
MinimumVersionForCollab,
|
|
133
134
|
ContainerExtensionExpectations,
|
|
134
|
-
ContainerRuntimeBaseAlpha,
|
|
135
135
|
} from "@fluidframework/runtime-definitions/internal";
|
|
136
136
|
import {
|
|
137
137
|
addBlobToSummary,
|
|
@@ -173,6 +173,7 @@ import {
|
|
|
173
173
|
wrapError,
|
|
174
174
|
tagCodeArtifacts,
|
|
175
175
|
normalizeError,
|
|
176
|
+
toITelemetryLoggerExt,
|
|
176
177
|
} from "@fluidframework/telemetry-utils/internal";
|
|
177
178
|
import { gt } from "semver-ts";
|
|
178
179
|
import { v4 as uuid } from "uuid";
|
|
@@ -675,18 +676,11 @@ export function getDeviceSpec(): {
|
|
|
675
676
|
deviceMemory?: number | undefined;
|
|
676
677
|
hardwareConcurrency?: number | undefined;
|
|
677
678
|
} {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
hardwareConcurrency: navigator.hardwareConcurrency,
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
} catch {
|
|
687
|
-
// Eat the error
|
|
688
|
-
}
|
|
689
|
-
return {};
|
|
679
|
+
return {
|
|
680
|
+
// deviceMemory is only available in browsers and is not part of the Navigator type definition. In Node 22 it is undefined.
|
|
681
|
+
deviceMemory: (navigator as Navigator & { deviceMemory?: number }).deviceMemory,
|
|
682
|
+
hardwareConcurrency: navigator.hardwareConcurrency,
|
|
683
|
+
};
|
|
690
684
|
}
|
|
691
685
|
|
|
692
686
|
/**
|
|
@@ -846,7 +840,7 @@ export async function loadContainerRuntime(
|
|
|
846
840
|
* @legacy @alpha
|
|
847
841
|
*/
|
|
848
842
|
export async function loadContainerRuntimeAlpha(params: LoadContainerRuntimeParams): Promise<{
|
|
849
|
-
runtime: IContainerRuntime &
|
|
843
|
+
runtime: IContainerRuntime & IRuntime;
|
|
850
844
|
}> {
|
|
851
845
|
return ContainerRuntime.loadRuntime2({
|
|
852
846
|
...params,
|
|
@@ -1185,7 +1179,6 @@ export class ContainerRuntime
|
|
|
1185
1179
|
idCompressorMode = desiredIdCompressorMode;
|
|
1186
1180
|
}
|
|
1187
1181
|
|
|
1188
|
-
// eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
|
|
1189
1182
|
const createIdCompressorFn = (): IIdCompressor & IIdCompressorCore => {
|
|
1190
1183
|
/**
|
|
1191
1184
|
* Because the IdCompressor emits so much telemetry, this function is used to sample
|
|
@@ -1204,17 +1197,21 @@ export class ContainerRuntime
|
|
|
1204
1197
|
const pendingLocalState = context.pendingLocalState as IPendingRuntimeState;
|
|
1205
1198
|
|
|
1206
1199
|
if (pendingLocalState?.pendingIdCompressorState !== undefined) {
|
|
1207
|
-
return
|
|
1208
|
-
|
|
1209
|
-
|
|
1200
|
+
return toIdCompressorWithCore(
|
|
1201
|
+
deserializeIdCompressor(
|
|
1202
|
+
pendingLocalState.pendingIdCompressorState,
|
|
1203
|
+
toITelemetryLoggerExt(compressorLogger),
|
|
1204
|
+
),
|
|
1210
1205
|
);
|
|
1211
1206
|
} else if (serializedIdCompressor === undefined) {
|
|
1212
|
-
return createIdCompressor(compressorLogger);
|
|
1207
|
+
return toIdCompressorWithCore(createIdCompressor(compressorLogger));
|
|
1213
1208
|
} else {
|
|
1214
|
-
return
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1209
|
+
return toIdCompressorWithCore(
|
|
1210
|
+
deserializeIdCompressor(
|
|
1211
|
+
serializedIdCompressor,
|
|
1212
|
+
createSessionId(),
|
|
1213
|
+
toITelemetryLoggerExt(compressorLogger),
|
|
1214
|
+
),
|
|
1218
1215
|
);
|
|
1219
1216
|
}
|
|
1220
1217
|
};
|
|
@@ -1339,7 +1336,12 @@ export class ContainerRuntime
|
|
|
1339
1336
|
targetClientId?: string,
|
|
1340
1337
|
) => void;
|
|
1341
1338
|
public readonly disposeFn: (error?: ICriticalContainerError) => void;
|
|
1342
|
-
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Initiate closing of the container due to a critical error.
|
|
1342
|
+
* @param error - The critical error that caused the container to close.
|
|
1343
|
+
*/
|
|
1344
|
+
private readonly closeFn: (error: ICriticalContainerError) => void;
|
|
1343
1345
|
|
|
1344
1346
|
public get flushMode(): FlushMode {
|
|
1345
1347
|
return this._flushMode;
|
|
@@ -1378,7 +1380,6 @@ export class ContainerRuntime
|
|
|
1378
1380
|
return this.documentsSchemaController.sessionSchema.runtime;
|
|
1379
1381
|
}
|
|
1380
1382
|
|
|
1381
|
-
// eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
|
|
1382
1383
|
private _idCompressor: (IIdCompressor & IIdCompressorCore) | undefined;
|
|
1383
1384
|
|
|
1384
1385
|
// We accumulate Id compressor Ops while Id compressor is not loaded yet (only for "delayed" mode)
|
|
@@ -1394,7 +1395,6 @@ export class ContainerRuntime
|
|
|
1394
1395
|
/**
|
|
1395
1396
|
* {@inheritDoc @fluidframework/runtime-definitions#IContainerRuntimeBase.idCompressor}
|
|
1396
1397
|
*/
|
|
1397
|
-
// eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
|
|
1398
1398
|
public get idCompressor(): (IIdCompressor & IIdCompressorCore) | undefined {
|
|
1399
1399
|
// Expose ID Compressor only if it's On from the start.
|
|
1400
1400
|
// If container uses delayed mode, then we can only expose generateDocumentUniqueId() and nothing else.
|
|
@@ -1615,7 +1615,6 @@ export class ContainerRuntime
|
|
|
1615
1615
|
|
|
1616
1616
|
blobManagerLoadInfo: IBlobManagerLoadInfo,
|
|
1617
1617
|
private readonly _storage: IContainerStorageService,
|
|
1618
|
-
// eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
|
|
1619
1618
|
private readonly createIdCompressorFn: () => IIdCompressor & IIdCompressorCore,
|
|
1620
1619
|
|
|
1621
1620
|
private readonly documentsSchemaController: DocumentsSchemaController,
|
|
@@ -1891,20 +1890,38 @@ export class ContainerRuntime
|
|
|
1891
1890
|
this.mc.config.getNumber("Fluid.ContainerRuntime.StagingModeAutoFlushThreshold") ??
|
|
1892
1891
|
runtimeOptions.stagingModeAutoFlushThreshold ??
|
|
1893
1892
|
defaultStagingModeAutoFlushThreshold;
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1893
|
+
// BatchId tracking powers DuplicateBatchDetector (catching forked-container duplicates)
|
|
1894
|
+
// and is also a prerequisite for the Offline Load feature. It is enabled by default
|
|
1895
|
+
// when both TurnBased flush mode and grouped batching are active; the kill-switch
|
|
1896
|
+
// below allows disabling it without a code change if a regression is observed.
|
|
1897
|
+
// Grouped batching is required because resubmits can produce empty batches that must
|
|
1898
|
+
// still be sent on the wire as a placeholder grouped batch to preserve their batchId
|
|
1899
|
+
// (see OpGroupingManager.createEmptyGroupedBatch / outbox.flushEmptyBatch).
|
|
1900
|
+
// Offline Load requires both prerequisites, so a consumer that opts into it without
|
|
1901
|
+
// them gets an explicit UsageError rather than silent degradation.
|
|
1902
|
+
const offlineLoadRequested =
|
|
1903
|
+
this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") === true;
|
|
1904
|
+
const disableBatchIdTracking =
|
|
1905
|
+
this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableBatchIdTracking") === true;
|
|
1906
|
+
|
|
1907
|
+
if (offlineLoadRequested && this._flushMode !== FlushMode.TurnBased) {
|
|
1900
1908
|
const error = new UsageError("Offline mode is only supported in turn-based mode");
|
|
1901
1909
|
this.closeFn(error);
|
|
1902
1910
|
throw error;
|
|
1903
1911
|
}
|
|
1912
|
+
if (offlineLoadRequested && !this.groupedBatchingEnabled) {
|
|
1913
|
+
const error = new UsageError("Offline mode requires grouped batching to be enabled");
|
|
1914
|
+
this.closeFn(error);
|
|
1915
|
+
throw error;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
this.batchIdTrackingEnabled =
|
|
1919
|
+
!disableBatchIdTracking &&
|
|
1920
|
+
this._flushMode === FlushMode.TurnBased &&
|
|
1921
|
+
this.groupedBatchingEnabled;
|
|
1904
1922
|
|
|
1905
|
-
//
|
|
1906
|
-
//
|
|
1907
|
-
// Don't waste resources doing so if not needed.
|
|
1923
|
+
// DuplicateBatchDetector maintains a cache of all batchIds/sequenceNumbers within the
|
|
1924
|
+
// collab window. Skip allocating it when batchId tracking is off.
|
|
1908
1925
|
if (this.batchIdTrackingEnabled) {
|
|
1909
1926
|
this.duplicateBatchDetector = new DuplicateBatchDetector(recentBatchInfo);
|
|
1910
1927
|
}
|
|
@@ -1924,6 +1941,7 @@ export class ContainerRuntime
|
|
|
1924
1941
|
|
|
1925
1942
|
this.garbageCollector = GarbageCollector.create({
|
|
1926
1943
|
runtime: this,
|
|
1944
|
+
closeFn: this.closeFn,
|
|
1927
1945
|
gcOptions: runtimeOptions.gcOptions,
|
|
1928
1946
|
baseSnapshot,
|
|
1929
1947
|
baseLogger: this.mc.logger,
|
|
@@ -2140,13 +2158,6 @@ export class ContainerRuntime
|
|
|
2140
2158
|
// (We have to call flush _before_ processing a runtime op, but after is ok for non-runtime op)
|
|
2141
2159
|
this.deltaManager.on("op", () => this.flush());
|
|
2142
2160
|
|
|
2143
|
-
// logging hardware telemetry
|
|
2144
|
-
this.baseLogger.send({
|
|
2145
|
-
category: "generic",
|
|
2146
|
-
eventName: "DeviceSpec",
|
|
2147
|
-
...getDeviceSpec(),
|
|
2148
|
-
});
|
|
2149
|
-
|
|
2150
2161
|
this.mc.logger.sendTelemetryEvent({
|
|
2151
2162
|
eventName: "ContainerLoadStats",
|
|
2152
2163
|
...this.createContainerMetadata,
|
|
@@ -2169,6 +2180,8 @@ export class ContainerRuntime
|
|
|
2169
2180
|
groupedBatchingEnabled: this.groupedBatchingEnabled,
|
|
2170
2181
|
initialSequenceNumber: this.deltaManager.initialSequenceNumber,
|
|
2171
2182
|
minVersionForCollab: this.minVersionForCollab,
|
|
2183
|
+
// logging hardware telemetry
|
|
2184
|
+
deviceSpec: { ...getDeviceSpec() },
|
|
2172
2185
|
});
|
|
2173
2186
|
|
|
2174
2187
|
ReportOpPerfTelemetry(this.clientId, this._deltaManager, this, this.baseLogger);
|
|
@@ -2402,12 +2415,18 @@ export class ContainerRuntime
|
|
|
2402
2415
|
}
|
|
2403
2416
|
}
|
|
2404
2417
|
|
|
2418
|
+
public close(): void {
|
|
2419
|
+
this.garbageCollector.dispose();
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2405
2422
|
public dispose(error?: Error): void {
|
|
2406
2423
|
if (this._disposed) {
|
|
2407
2424
|
return;
|
|
2408
2425
|
}
|
|
2409
2426
|
this._disposed = true;
|
|
2410
2427
|
|
|
2428
|
+
// The ContainerRuntimeDisposed event is redundant with the loader's ContainerDispose event
|
|
2429
|
+
// (see #27126) and can be removed once the change for ContainerDispose has saturated in telemetry.
|
|
2411
2430
|
this.mc.logger.sendTelemetryEvent(
|
|
2412
2431
|
{
|
|
2413
2432
|
eventName: "ContainerRuntimeDisposed",
|
|
@@ -2416,6 +2435,7 @@ export class ContainerRuntime
|
|
|
2416
2435
|
attachState: this.attachState,
|
|
2417
2436
|
},
|
|
2418
2437
|
error,
|
|
2438
|
+
LogLevel.info,
|
|
2419
2439
|
);
|
|
2420
2440
|
|
|
2421
2441
|
if (this.summaryManager !== undefined) {
|
|
@@ -3598,7 +3618,18 @@ export class ContainerRuntime
|
|
|
3598
3618
|
let stageControls: StageControlsInternal | undefined;
|
|
3599
3619
|
if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback") === true) {
|
|
3600
3620
|
if (!this.batchRunner.running && !this.inStagingMode) {
|
|
3601
|
-
|
|
3621
|
+
// Use silent=true to suppress stagingModeChanged events for orderSequentially.
|
|
3622
|
+
// orderSequentially uses staging mode as a rollback mechanism.
|
|
3623
|
+
// Emitting stagingModeChanged here would:
|
|
3624
|
+
// - Cause UI flicker — consumers rendering staging mode event would see
|
|
3625
|
+
// unexpected enter/exit flashes on every orderSequentially call.
|
|
3626
|
+
// - consumers cannot distinguish this internal usage
|
|
3627
|
+
// from a user explicitly entering staging mode, as there is no source field
|
|
3628
|
+
// on the event to filter by.
|
|
3629
|
+
// - if orderSequentially is
|
|
3630
|
+
// later reimplemented without staging mode, consumers calibrated
|
|
3631
|
+
// to these events would break silently.
|
|
3632
|
+
stageControls = this.enterStagingModeCore(true);
|
|
3602
3633
|
}
|
|
3603
3634
|
// Note: we are not touching any batches other than mainBatch here, for two reasons:
|
|
3604
3635
|
// 1. It would not help, as other batches are flushed independently from main batch.
|
|
@@ -3674,9 +3705,22 @@ export class ContainerRuntime
|
|
|
3674
3705
|
* Enter Staging Mode, such that ops submitted to the ContainerRuntime will not be sent to the ordering service.
|
|
3675
3706
|
* To exit Staging Mode, call either discardChanges or commitChanges on the Stage Controls returned from this method.
|
|
3676
3707
|
*
|
|
3708
|
+
* @remarks
|
|
3709
|
+
* The `stagingModeChanged` event is emitted when staging mode is entered or exited via this method.
|
|
3710
|
+
* It is NOT emitted when staging mode is used internally (e.g. by `orderSequentially` for rollback support).
|
|
3711
|
+
*
|
|
3677
3712
|
* @returns Controls for exiting Staging Mode.
|
|
3678
3713
|
*/
|
|
3679
|
-
public enterStagingMode = (): StageControlsInternal =>
|
|
3714
|
+
public enterStagingMode = (): StageControlsInternal => this.enterStagingModeCore(false);
|
|
3715
|
+
|
|
3716
|
+
/**
|
|
3717
|
+
* Internal implementation of enterStagingMode.
|
|
3718
|
+
* @param silent - When true, suppresses `stagingModeChanged` event emission.
|
|
3719
|
+
* Pass `true` when staging mode is used as an internal implementation detail (e.g. by
|
|
3720
|
+
* `orderSequentially` for rollback support) so that external listeners only observe
|
|
3721
|
+
* user-initiated staging mode transitions. Pass `false` for all public entry points.
|
|
3722
|
+
*/
|
|
3723
|
+
private readonly enterStagingModeCore = (silent: boolean): StageControlsInternal => {
|
|
3680
3724
|
if (this.stageControls !== undefined) {
|
|
3681
3725
|
throw new UsageError("Already in staging mode");
|
|
3682
3726
|
}
|
|
@@ -3688,10 +3732,15 @@ export class ContainerRuntime
|
|
|
3688
3732
|
// since we mark whole batches as "staged" or not to indicate whether to submit them.
|
|
3689
3733
|
this.flush();
|
|
3690
3734
|
|
|
3735
|
+
// Note: `silent` is captured from the enclosing `enterStagingModeCore` call.
|
|
3736
|
+
// When `true`, both enter and exit events are suppressed (see orderSequentially).
|
|
3691
3737
|
const exitStagingMode = (
|
|
3692
3738
|
discardOrCommit: () => IPendingMessage["batchInfo"][],
|
|
3693
3739
|
exitMethod: "commit" | "discard",
|
|
3694
3740
|
): void => {
|
|
3741
|
+
if (this.stageControls !== stageControls) {
|
|
3742
|
+
throw new UsageError("Not in staging mode");
|
|
3743
|
+
}
|
|
3695
3744
|
try {
|
|
3696
3745
|
PerformanceEvent.timedExec(
|
|
3697
3746
|
this.mc.logger,
|
|
@@ -3723,6 +3772,12 @@ export class ContainerRuntime
|
|
|
3723
3772
|
this.closeFn(normalizedError);
|
|
3724
3773
|
throw normalizedError;
|
|
3725
3774
|
}
|
|
3775
|
+
if (!silent) {
|
|
3776
|
+
this.emit("stagingModeChanged", {
|
|
3777
|
+
inStagingMode: false,
|
|
3778
|
+
commit: exitMethod === "commit",
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3726
3781
|
};
|
|
3727
3782
|
|
|
3728
3783
|
const stageControls: StageControlsInternal = {
|
|
@@ -3752,8 +3807,16 @@ export class ContainerRuntime
|
|
|
3752
3807
|
|
|
3753
3808
|
this.stageControls = stageControls;
|
|
3754
3809
|
this.channelCollection.notifyStagingMode(true);
|
|
3810
|
+
if (!silent) {
|
|
3811
|
+
try {
|
|
3812
|
+
this.emit("stagingModeChanged", { inStagingMode: true });
|
|
3813
|
+
} catch (error) {
|
|
3814
|
+
// Don't let a listener error prevent the caller from receiving stage controls.
|
|
3815
|
+
this.mc.logger.sendErrorEvent({ eventName: "StagingModeChangedError" }, error);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3755
3818
|
|
|
3756
|
-
return
|
|
3819
|
+
return stageControls;
|
|
3757
3820
|
};
|
|
3758
3821
|
|
|
3759
3822
|
/**
|
package/src/dataStore.ts
CHANGED
|
@@ -7,11 +7,10 @@ import { AttachState } from "@fluidframework/container-definitions";
|
|
|
7
7
|
import type { FluidObject } from "@fluidframework/core-interfaces";
|
|
8
8
|
import type { IFluidHandleInternal } from "@fluidframework/core-interfaces/internal";
|
|
9
9
|
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
asLegacyAlpha,
|
|
10
|
+
import type {
|
|
11
|
+
AliasResult,
|
|
12
|
+
IDataStore,
|
|
13
|
+
IFluidDataStoreChannel,
|
|
15
14
|
} from "@fluidframework/runtime-definitions/internal";
|
|
16
15
|
import {
|
|
17
16
|
type ITelemetryLoggerExt,
|
|
@@ -81,7 +80,7 @@ class DataStore implements IDataStore {
|
|
|
81
80
|
if (alias.includes("/")) {
|
|
82
81
|
throw new UsageError(`The alias cannot contain slashes: '${alias}'`);
|
|
83
82
|
}
|
|
84
|
-
if (
|
|
83
|
+
if (this.parentContext.containerRuntime.inStagingMode === true) {
|
|
85
84
|
throw new UsageError("Cannot set aliases while in staging mode");
|
|
86
85
|
}
|
|
87
86
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { ICriticalContainerError } from "@fluidframework/container-definitions";
|
|
6
7
|
import type { IRequest } from "@fluidframework/core-interfaces";
|
|
7
8
|
import { assert, LazyPromise, Timer } from "@fluidframework/core-utils/internal";
|
|
8
9
|
import type { ISnapshotTree } from "@fluidframework/driver-definitions/internal";
|
|
@@ -141,6 +142,10 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
141
142
|
private completedRuns = 0;
|
|
142
143
|
|
|
143
144
|
private readonly runtime: IGarbageCollectionRuntime;
|
|
145
|
+
/**
|
|
146
|
+
* Called when the runtime should close because of an error.
|
|
147
|
+
*/
|
|
148
|
+
private readonly closeFn: (error: ICriticalContainerError) => void;
|
|
144
149
|
private readonly isSummarizerClient: boolean;
|
|
145
150
|
|
|
146
151
|
private readonly summaryStateTracker: GCSummaryStateTracker;
|
|
@@ -168,6 +173,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
168
173
|
|
|
169
174
|
protected constructor(createParams: IGarbageCollectorCreateParams) {
|
|
170
175
|
this.runtime = createParams.runtime;
|
|
176
|
+
this.closeFn = createParams.closeFn;
|
|
171
177
|
this.isSummarizerClient = createParams.isSummarizerClient;
|
|
172
178
|
this.getNodePackagePath = createParams.getNodePackagePath;
|
|
173
179
|
this.getLastSummaryTimestampMs = createParams.getLastSummaryTimestampMs;
|
|
@@ -202,14 +208,10 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
202
208
|
}
|
|
203
209
|
timeoutMs = overrideSessionExpiryTimeoutMs ?? timeoutMs;
|
|
204
210
|
if (timeoutMs <= 0) {
|
|
205
|
-
this.
|
|
206
|
-
new ClientSessionExpiredError(`Client session expired.`, timeoutMs),
|
|
207
|
-
);
|
|
211
|
+
this.closeFn(new ClientSessionExpiredError(`Client session expired.`, timeoutMs));
|
|
208
212
|
}
|
|
209
213
|
this.sessionExpiryTimer = new Timer(timeoutMs, () => {
|
|
210
|
-
this.
|
|
211
|
-
new ClientSessionExpiredError(`Client session expired.`, timeoutMs),
|
|
212
|
-
);
|
|
214
|
+
this.closeFn(new ClientSessionExpiredError(`Client session expired.`, timeoutMs));
|
|
213
215
|
});
|
|
214
216
|
this.sessionExpiryTimer.start();
|
|
215
217
|
this.sessionExpiryTimerStarted = Date.now();
|
package/src/gc/gcDefinitions.ts
CHANGED
|
@@ -363,10 +363,6 @@ export interface IGarbageCollectionRuntime {
|
|
|
363
363
|
* Returns the type of the GC node.
|
|
364
364
|
*/
|
|
365
365
|
getNodeType(nodePath: string): GCNodeType;
|
|
366
|
-
/**
|
|
367
|
-
* Called when the runtime should close because of an error.
|
|
368
|
-
*/
|
|
369
|
-
closeFn: (error?: ICriticalContainerError) => void;
|
|
370
366
|
}
|
|
371
367
|
|
|
372
368
|
/**
|
|
@@ -453,6 +449,12 @@ export interface IGarbageCollector {
|
|
|
453
449
|
*/
|
|
454
450
|
isNodeDeleted(nodePath: string): boolean;
|
|
455
451
|
setConnectionState(canSendOps: boolean, clientId?: string): void;
|
|
452
|
+
/**
|
|
453
|
+
* Cancels all GC timers and clears tracked state so timers do not keep the event loop alive
|
|
454
|
+
* or leak memory.
|
|
455
|
+
* @remarks
|
|
456
|
+
* This is idempotent - it is safe to call multiple times.
|
|
457
|
+
*/
|
|
456
458
|
dispose(): void;
|
|
457
459
|
}
|
|
458
460
|
|
|
@@ -497,6 +499,11 @@ export interface IGCNodeUpdatedProps {
|
|
|
497
499
|
*/
|
|
498
500
|
export interface IGarbageCollectorCreateParams {
|
|
499
501
|
readonly runtime: IGarbageCollectionRuntime;
|
|
502
|
+
/**
|
|
503
|
+
* Initiate closing of the container due to an error.
|
|
504
|
+
*/
|
|
505
|
+
readonly closeFn: (error: ICriticalContainerError) => void;
|
|
506
|
+
|
|
500
507
|
readonly gcOptions: IGCRuntimeOptions;
|
|
501
508
|
readonly baseLogger: ITelemetryLoggerExt;
|
|
502
509
|
readonly existing: boolean;
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,11 @@ export type {
|
|
|
30
30
|
} from "./messageTypes.js";
|
|
31
31
|
export { ContainerMessageType } from "./messageTypes.js";
|
|
32
32
|
export type { IBlobManagerLoadInfo } from "./blobManager/index.js";
|
|
33
|
+
export {
|
|
34
|
+
blobManagerBasePath,
|
|
35
|
+
blobsTreeName,
|
|
36
|
+
redirectTableBlobName,
|
|
37
|
+
} from "./blobManager/index.js";
|
|
33
38
|
export type { IDataStoreAliasMessage } from "./dataStore.js";
|
|
34
39
|
export { FluidDataStoreRegistry } from "./dataStoreRegistry.js";
|
|
35
40
|
export {
|
package/src/metadata.ts
CHANGED
|
@@ -40,6 +40,20 @@ export interface IBatchMetadata {
|
|
|
40
40
|
* Maybe set on first message of a batch, to the batchId generated when resubmitting (set/fixed on first resubmit)
|
|
41
41
|
*/
|
|
42
42
|
batchId?: BatchId;
|
|
43
|
+
/**
|
|
44
|
+
* Set on the envelope of a grouped batch op to the number of inner ops it contains.
|
|
45
|
+
* Exposed on the wire so consumers can record batch sizes in telemetry without parsing the grouped batch contents.
|
|
46
|
+
*
|
|
47
|
+
* Observable values:
|
|
48
|
+
* - Absent: either this is not a grouped batch envelope (e.g. a singleton batch that bypassed grouping), OR the producing runtime predates this field. Until the rollout is complete, telemetry consumers should treat absence as ambiguous and parse the envelope contents if a precise count is required for a grouped batch.
|
|
49
|
+
* - `0`: empty-grouped-batch placeholder produced when a resubmitted batch becomes empty.
|
|
50
|
+
* - `N` (N \> 0): grouped batch with N inner ops. For a chunked grouped batch this appears only on the last chunk's envelope (intermediate chunks carry no metadata).
|
|
51
|
+
*
|
|
52
|
+
* The field is intentionally advisory-only: the runtime does not validate that an inbound value matches the batch's actual inner op count. It is consumed exclusively by off-runtime telemetry.
|
|
53
|
+
*
|
|
54
|
+
* The field is always (re)stamped at outbound time from the current batch's actual size — `groupBatch` reads `batch.messages.length` directly, `createEmptyGroupedBatch` always writes `0`, and the chunking path only ever sees freshly-grouped envelopes from the same flush. It is never propagated from stashed pending state to the wire: on resubmit, ops re-enter grouping and the count is recomputed from the (possibly squashed, dropped, or added) outbound batch. This means the wire value always reflects the actual outbound size, even when the resubmitted batch differs from the original.
|
|
55
|
+
*/
|
|
56
|
+
groupedOpCount?: number;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/**
|
|
@@ -28,6 +28,16 @@ export class DuplicateBatchDetector {
|
|
|
28
28
|
*/
|
|
29
29
|
private readonly batchIdsBySeqNum = new Map<number, string>();
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Number of inbound batches processed since the last summary. Reset by getRecentBatchInfoForSummary.
|
|
33
|
+
*/
|
|
34
|
+
private processedBatchCount = 0;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Largest tracked-batch count observed since the last summary. Reset by getRecentBatchInfoForSummary.
|
|
38
|
+
*/
|
|
39
|
+
private peakTrackedBatchCount = 0;
|
|
40
|
+
|
|
31
41
|
/**
|
|
32
42
|
* Initialize from snapshot data if provided - otherwise initialize empty
|
|
33
43
|
*/
|
|
@@ -37,6 +47,7 @@ export class DuplicateBatchDetector {
|
|
|
37
47
|
this.batchIdsBySeqNum.set(seqNum, batchId);
|
|
38
48
|
this.seqNumByBatchId.set(batchId, seqNum);
|
|
39
49
|
}
|
|
50
|
+
this.peakTrackedBatchCount = this.batchIdsBySeqNum.size;
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
@@ -50,6 +61,7 @@ export class DuplicateBatchDetector {
|
|
|
50
61
|
batchStart: BatchStartInfo,
|
|
51
62
|
): { duplicate: true; otherSequenceNumber: number } | { duplicate: false } {
|
|
52
63
|
const { sequenceNumber, minimumSequenceNumber } = batchStart.keyMessage;
|
|
64
|
+
this.processedBatchCount++;
|
|
53
65
|
|
|
54
66
|
// Glance at this batch's MSN. Any batchIds we're tracking with a lower sequence number are now safe to forget.
|
|
55
67
|
// Why? Because any other client holding the same batch locally would have seen the earlier batch and closed before submitting its duplicate.
|
|
@@ -80,6 +92,9 @@ export class DuplicateBatchDetector {
|
|
|
80
92
|
// Add new batch
|
|
81
93
|
this.batchIdsBySeqNum.set(sequenceNumber, batchId);
|
|
82
94
|
this.seqNumByBatchId.set(batchId, sequenceNumber);
|
|
95
|
+
if (this.batchIdsBySeqNum.size > this.peakTrackedBatchCount) {
|
|
96
|
+
this.peakTrackedBatchCount = this.batchIdsBySeqNum.size;
|
|
97
|
+
}
|
|
83
98
|
|
|
84
99
|
return { duplicate: false };
|
|
85
100
|
}
|
|
@@ -108,16 +123,22 @@ export class DuplicateBatchDetector {
|
|
|
108
123
|
public getRecentBatchInfoForSummary(
|
|
109
124
|
telemetryContext?: ITelemetryContext,
|
|
110
125
|
): [number, string][] | undefined {
|
|
126
|
+
if (telemetryContext !== undefined) {
|
|
127
|
+
const prefix = "fluid_DuplicateBatchDetector_";
|
|
128
|
+
telemetryContext.set(prefix, "recentBatchCount", this.batchIdsBySeqNum.size);
|
|
129
|
+
telemetryContext.set(prefix, "peakRecentBatchCount", this.peakTrackedBatchCount);
|
|
130
|
+
telemetryContext.set(prefix, "processedBatchCount", this.processedBatchCount);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Reset per-window perf counters so each summary covers only the activity since the
|
|
134
|
+
// previous one. Peak resets to the current size (the floor for the next window).
|
|
135
|
+
this.processedBatchCount = 0;
|
|
136
|
+
this.peakTrackedBatchCount = this.batchIdsBySeqNum.size;
|
|
137
|
+
|
|
111
138
|
if (this.batchIdsBySeqNum.size === 0) {
|
|
112
139
|
return undefined;
|
|
113
140
|
}
|
|
114
141
|
|
|
115
|
-
telemetryContext?.set(
|
|
116
|
-
"fluid_DuplicateBatchDetector_",
|
|
117
|
-
"recentBatchCount",
|
|
118
|
-
this.batchIdsBySeqNum.size,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
142
|
return [...this.batchIdsBySeqNum.entries()];
|
|
122
143
|
}
|
|
123
144
|
}
|
|
@@ -100,7 +100,7 @@ export class OpGroupingManager {
|
|
|
100
100
|
const serializedOp = JSON.stringify(emptyGroupedBatch);
|
|
101
101
|
|
|
102
102
|
const placeholderMessage: LocalEmptyBatchPlaceholder = {
|
|
103
|
-
metadata: { batchId: resubmittingBatchId },
|
|
103
|
+
metadata: { batchId: resubmittingBatchId, groupedOpCount: 0 },
|
|
104
104
|
localOpMetadata: { emptyBatch: true },
|
|
105
105
|
referenceSequenceNumber,
|
|
106
106
|
runtimeOp: emptyGroupedBatch,
|
|
@@ -169,7 +169,7 @@ export class OpGroupingManager {
|
|
|
169
169
|
...batch,
|
|
170
170
|
messages: [
|
|
171
171
|
{
|
|
172
|
-
metadata: { batchId: groupedBatchId },
|
|
172
|
+
metadata: { batchId: groupedBatchId, groupedOpCount: batch.messages.length },
|
|
173
173
|
referenceSequenceNumber: batch.messages[0].referenceSequenceNumber,
|
|
174
174
|
contents: serializedContent,
|
|
175
175
|
},
|
|
@@ -171,12 +171,22 @@ export class OpSplitter {
|
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// The last chunk will be part of the new batch and needs to
|
|
175
|
-
//
|
|
174
|
+
// The last chunk will be part of the new batch and needs to preserve the
|
|
175
|
+
// batch metadata of the original batch. groupedOpCount is surfaced here
|
|
176
|
+
// (and only here, not on intermediate chunks) because intermediate chunks
|
|
177
|
+
// don't carry ops — they carry parts of a payload that only become ops
|
|
178
|
+
// once the last chunk is processed and the payload is reassembled.
|
|
179
|
+
// Stamping every chunk would let an observer double-count messages.
|
|
180
|
+
// batchId is deliberately not forwarded — it's a runtime dedup field
|
|
181
|
+
// consumed only after processChunk restores originalMetadata, not by
|
|
182
|
+
// wire observers.
|
|
176
183
|
const lastChunk = chunkToBatchMessage(
|
|
177
184
|
chunks[chunks.length - 1],
|
|
178
185
|
batch.referenceSequenceNumber,
|
|
179
|
-
{
|
|
186
|
+
{
|
|
187
|
+
batch: firstMessage.metadata?.batch,
|
|
188
|
+
groupedOpCount: firstMessage.metadata?.groupedOpCount,
|
|
189
|
+
},
|
|
180
190
|
);
|
|
181
191
|
|
|
182
192
|
this.logger.sendPerformanceEvent({
|
package/src/packageVersion.ts
CHANGED
|
@@ -125,8 +125,10 @@ export interface ISummarizerRuntime extends IConnectableRuntime {
|
|
|
125
125
|
*/
|
|
126
126
|
readonly summarizerClientId: string | undefined;
|
|
127
127
|
readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>;
|
|
128
|
+
/**
|
|
129
|
+
* Initiate disposal of the container.
|
|
130
|
+
*/
|
|
128
131
|
disposeFn(): void;
|
|
129
|
-
closeFn(): void;
|
|
130
132
|
on(
|
|
131
133
|
event: "op",
|
|
132
134
|
listener: (op: ISequencedDocumentMessage, runtimeMessage?: boolean) => void,
|