Skip to content

feat(container-runtime): Add fail fast mechanism if runtime options don't align with minVersionForCollab #24625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jun 6, 2025
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e297fcc
add fail fast mechanism
scottn12 May 14, 2025
e681906
update logic to account for objects and multiple "invalid" values
scottn12 May 15, 2025
0f5ae2e
update createBlobPayloadPending tests
scottn12 May 15, 2025
2ac6410
Merge branch 'main' into addFailFast
scottn12 May 15, 2025
9c3ca6d
allow stress test to pass minVersionForCollab
scottn12 May 15, 2025
22f9f48
api-report
scottn12 May 15, 2025
6e2e0eb
do not fail fast if minVersionForCollab is not set
scottn12 May 15, 2025
26cf587
Revert "allow stress test to pass minVersionForCollab"
scottn12 May 15, 2025
d41f5f1
Revert "update createBlobPayloadPending tests"
scottn12 May 15, 2025
d88bd60
add generic version of validate function, add UT
scottn12 May 16, 2025
06d3272
Merge branch 'main' into addFailFast
scottn12 May 16, 2025
fe42e8e
Update packages/runtime/container-runtime/src/test/compatUtils.spec.ts
scottn12 May 16, 2025
3801f88
remove accidental comment
scottn12 May 16, 2025
6f02668
pr feedback
scottn12 May 20, 2025
b559c3e
use inverted map structure for validation config
scottn12 May 20, 2025
2b5307a
remove comment
scottn12 May 20, 2025
ec01e82
configValueToMinVersionForCollab helper fn setup
scottn12 May 21, 2025
7385221
fix issue if multiple options are passed in a config option value
scottn12 May 22, 2025
2a6d5dd
combine helper functions
scottn12 May 22, 2025
7f0bc5d
update error message
scottn12 May 22, 2025
8e1dfea
update names/comments for clarity
scottn12 May 22, 2025
d618d6c
update comment
scottn12 May 22, 2025
09e94b7
update comments
scottn12 May 22, 2025
3687941
Merge branch 'main' into addFailFast
scottn12 May 22, 2025
adfcc91
Merge branch 'main' into addFailFast
scottn12 May 28, 2025
9611ea5
Merge branch 'main' into addFailFast
scottn12 Jun 2, 2025
452b792
Merge branch 'main' into addFailFast
scottn12 Jun 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 145 additions & 10 deletions packages/runtime/container-runtime/src/compatUtils.ts
Original file line number Diff line number Diff line change
@@ -3,8 +3,10 @@
* Licensed under the MIT License.
*/

import { assert } from "@fluidframework/core-utils/internal";
import { FlushMode } from "@fluidframework/runtime-definitions/internal";
import { compare, gte, lte, valid } from "semver-ts";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { compare, gt, gte, lte, valid } from "semver-ts";

import {
disabledCompressionConfig,
@@ -78,6 +80,13 @@ export type ConfigMap<T extends Record<string, unknown>> = {
};
};

/**
* Generic type for runtimeOptionsAffectingDocSchemaConfigValidationMap
*/
export type ConfigValidationMap<T extends Record<string, unknown>> = {
[K in keyof T]-?: (configValue: T[K]) => SemanticVersion | undefined;
};

/**
* Subset of the {@link ContainerRuntimeOptionsInternal} properties which
* affect {@link IDocumentSchemaFeatures}.
@@ -119,11 +128,11 @@ const runtimeOptionsAffectingDocSchemaConfigMap = {
enableGroupedBatching: {
"1.0.0": false,
"2.0.0-defaults": true,
} as const,
},
compressionOptions: {
"1.0.0": disabledCompressionConfig,
"2.0.0-defaults": enabledCompressionConfig,
} as const,
},
enableRuntimeIdCompressor: {
// For IdCompressorMode, `undefined` represents a logical state (off).
// However, to satisfy the Required<> constraint while
@@ -135,7 +144,7 @@ const runtimeOptionsAffectingDocSchemaConfigMap = {
// Therefore, we will require customers to explicitly enable it. We
// are keeping it as a DocSchema affecting option for now as this may
// change in the future.
} as const,
},
explicitSchemaControl: {
"1.0.0": false,
// This option's intention is to prevent 1.x clients from joining sessions
@@ -149,34 +158,66 @@ const runtimeOptionsAffectingDocSchemaConfigMap = {
// Only enable as a default when `minVersionForCollab` is specified at
// 2.0.0+.
"2.0.0": true,
} as const,
},
flushMode: {
// Note: 1.x clients are compatible with TurnBased flushing, but here we elect to remain on Immediate flush mode
// as a work-around for inability to send batches larger than 1Mb. Immediate flushing keeps batches smaller as
// fewer messages will be included per flush.
"1.0.0": FlushMode.Immediate,
"2.0.0-defaults": FlushMode.TurnBased,
} as const,
},
gcOptions: {
"1.0.0": {},
// Although sweep is supported in 2.x, it is disabled by default until minVersionForCollab>=3.0.0 to be extra safe.
"3.0.0": { enableGCSweep: true },
} as const,
},
createBlobPayloadPending: {
// This feature is new and disabled by default. In the future we will enable it by default, but we have not
// closed on the version where that will happen yet. Probably a .10 release since blob functionality is not
// exposed on the `@public` API surface.
"1.0.0": undefined,
} as const,
},
} as const satisfies ConfigMap<RuntimeOptionsAffectingDocSchema>;

const runtimeOptionsAffectingDocSchemaConfigValidationMap = {
enableGroupedBatching: configValueToMinVersionForCollab([
[false, "1.0.0"],
[true, "2.0.0-defaults"],
]),
compressionOptions: configValueToMinVersionForCollab([
[{ ...disabledCompressionConfig }, "1.0.0"],
[{ ...enabledCompressionConfig }, "2.0.0-defaults"],
]),
enableRuntimeIdCompressor: configValueToMinVersionForCollab([
[undefined, "1.0.0"],
["on", "2.0.0-defaults"],
["delayed", "2.0.0-defaults"],
]),
explicitSchemaControl: configValueToMinVersionForCollab([
[false, "1.0.0"],
[true, "2.0.0-defaults"],
]),
flushMode: configValueToMinVersionForCollab([
[FlushMode.Immediate, "1.0.0"],
[FlushMode.TurnBased, "2.0.0-defaults"],
]),
gcOptions: configValueToMinVersionForCollab([
[{ enableGCSweep: undefined }, "1.0.0"],
[{ enableGCSweep: true }, "2.0.0-defaults"],
]),
createBlobPayloadPending: configValueToMinVersionForCollab([
[undefined, "1.0.0"],
[true, "2.40.0"],
]),
} as const satisfies ConfigValidationMap<RuntimeOptionsAffectingDocSchema>;

/**
* Returns the default RuntimeOptionsAffectingDocSchema configuration for a given minVersionForCollab.
*/
export function getMinVersionForCollabDefaults(
minVersionForCollab: MinimumVersionForCollab,
): RuntimeOptionsAffectingDocSchema {
return getConfigsForCompatMode(
return getConfigsForMinVersionForCollab(
minVersionForCollab,
runtimeOptionsAffectingDocSchemaConfigMap,
// This is a bad cast away from Partial that getConfigsForCompatMode provides.
@@ -187,7 +228,7 @@ export function getMinVersionForCollabDefaults(
/**
* Returns a default configuration given minVersionForCollab and configuration version map.
*/
export function getConfigsForCompatMode<T extends Record<SemanticVersion, unknown>>(
export function getConfigsForMinVersionForCollab<T extends Record<SemanticVersion, unknown>>(
minVersionForCollab: SemanticVersion,
configMap: ConfigMap<T>,
): Partial<T> {
@@ -226,3 +267,97 @@ export function isValidMinVersionForCollab(
lte(minVersionForCollab, pkgVersion)
);
}

/**
* Validates if the runtime options passed in from the user are compatible with the minVersionForCollab.
* For example, if a user sets the `enableGroupedBatching` option to true, but the minVersionForCollab
* is set to "1.0.0", then we should throw a UsageError since 1.x clients do not support batching.
* */
export function validateRuntimeOptions(
minVersionForCollab: MinimumVersionForCollab,
runtimeOptions: Partial<ContainerRuntimeOptionsInternal>,
): void {
getValidationForRuntimeOptions<RuntimeOptionsAffectingDocSchema>(
minVersionForCollab,
runtimeOptions as Partial<RuntimeOptionsAffectingDocSchema>,
runtimeOptionsAffectingDocSchemaConfigValidationMap,
);
}

/**
* Generic function to validate runtime options against the minVersionForCollab.
*/
export function getValidationForRuntimeOptions<T extends Record<string, unknown>>(
minVersionForCollab: SemanticVersion,
runtimeOptions: Partial<T>,
validationMap: ConfigValidationMap<T>,
): void {
if (minVersionForCollab === defaultMinVersionForCollab) {
// If the minVersionForCollab is set to the default value, then we will not validate the runtime options
// This is to avoid disruption to users who have not yet set the minVersionForCollab value explicitly.
return;
}
// Iterate through each runtime option passed in by the user
for (const [passedRuntimeOption, passedRuntimeOptionValue] of Object.entries(
runtimeOptions,
) as [keyof T & string, T[keyof T & string]][]) {
// Skip if passedRuntimeOption is not in validation map
if (!(passedRuntimeOption in validationMap)) {
continue;
}

const requiredVersion = validationMap[passedRuntimeOption](passedRuntimeOptionValue);
if (requiredVersion !== undefined && gt(requiredVersion, minVersionForCollab)) {
throw new UsageError(
`Runtime option ${passedRuntimeOption}:${JSON.stringify(passedRuntimeOptionValue)} requires ` +
`runtime version ${requiredVersion}. Please update minVersionForCollab ` +
`(currently ${minVersionForCollab}) to ${requiredVersion} or later to proceed.`,
);
}
}
}

/**
* Helper function to map ContainerRuntimeOptionsInternal config values to
* minVersionForCollab in {@link runtimeOptionsAffectingDocSchemaConfigValidationMap}.
*/
export function configValueToMinVersionForCollab<
T extends string | number | boolean | undefined | object,
Arr extends readonly [T, SemanticVersion][],
>(configToMinVer: Arr): (configValue: T) => SemanticVersion | undefined {
const configValueToRequiredVersionMap = new Map(configToMinVer);
return (configValue: T) => {
// If the configValue is not an object then we can get the version required directly from the map.
if (typeof configValue !== "object") {
return configValueToRequiredVersionMap.get(configValue);
}
// When the input `configValue` is an object, this logic determines the minimum runtime version it requires.
// It iterates through each entry in `configValueToRequiredVersionMap`. If `possibleConfigValue` shares at
// least one key-value pair with the input `configValue`, its associated `versionRequired` is collected into
// `matchingVersions`. After checking all entries, the highest among the collected versions is returned.
// This represents the overall minimum version required to support the features implied by the input `configValue`.
const matchingVersions: SemanticVersion[] = [];
for (const [
possibleConfigValue,
versionRequired,
] of configValueToRequiredVersionMap.entries()) {
assert(
typeof possibleConfigValue == "object",
"possibleConfigValue should be an object",
);
// Check if `possibleConfigValue` and the input `configValue` share at least one
// common key-value pair. If they do, the `versionRequired` for this `possibleConfigValue`
// is added to `matchingVersions`.
if (Object.entries(possibleConfigValue).some(([k, v]) => configValue[k] === v)) {
matchingVersions.push(versionRequired);
}
}
if (matchingVersions.length > 0) {
// Return the latest minVersionForCollab among all matches.
return matchingVersions.sort((a, b) => compare(b, a))[0];
}
// If no matches then we return undefined. This means that the config value passed in
// does not require a specific minVersionForCollab to be valid.
return undefined;
};
}
5 changes: 5 additions & 0 deletions packages/runtime/container-runtime/src/containerRuntime.ts
Original file line number Diff line number Diff line change
@@ -181,6 +181,7 @@ import {
isValidMinVersionForCollab,
type RuntimeOptionsAffectingDocSchema,
type MinimumVersionForCollab,
validateRuntimeOptions,
} from "./compatUtils.js";
import type { ICompressionRuntimeOptions } from "./compressionDefinitions.js";
import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
@@ -880,6 +881,10 @@ export class ContainerRuntime
`Invalid minVersionForCollab: ${minVersionForCollab}. It must be an existing FF version (i.e. 2.22.1).`,
);
}
// We also validate that there is not a mismatch between `minVersionForCollab` and runtime options that
// were manually set.
validateRuntimeOptions(minVersionForCollab, runtimeOptions);

const defaultsAffectingDocSchema = getMinVersionForCollabDefaults(minVersionForCollab);

// The following are the default values for the options that do not affect the DocumentSchema.
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.