Skip to content
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

(compat) Added supported features and generation across the Loader / Runtime boundary #22877

Merged
merged 20 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .changeset/light-pears-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@fluidframework/container-definitions": minor
---
---
"section": deprecation
---

`supportedFeatures` is deprecated in `IContainerContext`

This was an optional property that was used internally to communicate features supported by the Loader layer to Runtime. This has been replaced with an internal-only functionality.
8 changes: 8 additions & 0 deletions packages/common/client-utils/src/indexBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ export {
} from "./typedEventEmitter.js";

export { createEmitter } from "./events/index.js";

export {
checkLayerCompatibility,
type LayerCompatCheckResult,
type ILayerCompatDetails,
type IProvideLayerCompatDetails,
type ILayerCompatSupportRequirements,
} from "./layerCompat.js";
8 changes: 8 additions & 0 deletions packages/common/client-utils/src/indexNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ export {
} from "./typedEventEmitter.js";

export { createEmitter } from "./events/index.js";

export {
checkLayerCompatibility,
type LayerCompatCheckResult,
type ILayerCompatDetails,
type IProvideLayerCompatDetails,
type ILayerCompatSupportRequirements,
} from "./layerCompat.js";
125 changes: 125 additions & 0 deletions packages/common/client-utils/src/layerCompat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

/**
* Result of a layer compatibility check - whether a layer is compatible with another layer.
* @internal
*/
export type LayerCompatCheckResult =
| { readonly isCompatible: true }
| {
readonly isCompatible: false;
/**
* Whether the generation of the layer is compatible with the other layer.
*/
readonly isGenerationCompatible: boolean;
/**
* The features that are required by the layer but are not supported by the other layer. This will only
* be set if there are unsupported features.
*/
readonly unsupportedFeatures: readonly string[] | undefined;
};

/**
* @internal
*/
export const ILayerCompatDetails: keyof IProvideLayerCompatDetails = "ILayerCompatDetails";
markfields marked this conversation as resolved.
Show resolved Hide resolved

/**
* @internal
*/
export interface IProvideLayerCompatDetails {
readonly ILayerCompatDetails: ILayerCompatDetails;
}

/**
* This interface is used to communicate the compatibility details of a layer to another layer.
* @internal
*/
export interface ILayerCompatDetails extends Partial<IProvideLayerCompatDetails> {
/**
* A list of features supported by the layer at a particular layer boundary. This is used to check if these
* set of features satisfy the requirements of another layer.
*/
readonly supportedFeatures: ReadonlySet<string>;
/**
* The generation of the layer. The other layer at the layer boundary uses this to check if this satisfies
* the minimum generation it requires to be compatible.
*/
readonly generation: number;
/**
* The package version of the layer. When an incompatibility is detected, this is used to provide more context
* on what the versions of the incompatible layers are.
*/
readonly pkgVersion: string;
}

/**
* This is the default compat details for a layer when it doesn't provide any compat details. This is used for
* backwards compatibility to allow older layers to work before compatibility enforcement was introduced.
* @internal
*/
export const defaultLayerCompatDetails: ILayerCompatDetails = {
supportedFeatures: new Set(),
generation: 0, // 0 is reserved for layers before compatibility enforcement was introduced.
pkgVersion: "unknown",
};

/**
* The requirements that a layer needs another layer to support for them to be compatible.
* @internal
*/
export interface ILayerCompatSupportRequirements {
/**
* The minimum supported generation the other layer needs to be at.
*/
readonly minSupportedGeneration: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: minGeneration may be a sufficient name

/**
* The features that the other layer needs to support.
*/
readonly requiredFeatures: readonly string[];
}

/**
* Checks compatibility of a layer (layer1) with another layer (layer2).
* @param compatSupportRequirementsLayer1 - The requirements from layer1 that layer2 needs to meet.
* @param compatDetailsLayer2 - The compatibility details of the layer2. If this is undefined, then the
* default compatibility details are used for backwards compatibility.
* @returns The result of the compatibility check indicating whether layer2 is compatible with layer1.
*
* @internal
*/
export function checkLayerCompatibility(
compatSupportRequirementsLayer1: ILayerCompatSupportRequirements,
compatDetailsLayer2: ILayerCompatDetails | undefined,
): LayerCompatCheckResult {
const compatDetailsLayer2ToUse = compatDetailsLayer2 ?? defaultLayerCompatDetails;
markfields marked this conversation as resolved.
Show resolved Hide resolved
let isGenerationCompatible = true;
const unsupportedFeatures: string[] = [];

// If layer2's generation is less than the required minimum supported generation of layer1,
// then it is not compatible.
if (
compatDetailsLayer2ToUse.generation <
compatSupportRequirementsLayer1.minSupportedGeneration
) {
isGenerationCompatible = false;
}

// All features required by layer1 must be supported by layer2 to be compatible.
for (const feature of compatSupportRequirementsLayer1.requiredFeatures) {
if (!compatDetailsLayer2ToUse.supportedFeatures.has(feature)) {
unsupportedFeatures.push(feature);
}
}

return isGenerationCompatible && unsupportedFeatures.length === 0
? { isCompatible: true }
: {
isCompatible: false,
isGenerationCompatible,
unsupportedFeatures: unsupportedFeatures.length > 0 ? unsupportedFeatures : undefined,
};
}
160 changes: 160 additions & 0 deletions packages/common/client-utils/src/test/mocha/layerCompat.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { strict as assert } from "node:assert";

import {
checkLayerCompatibility,
type ILayerCompatDetails,
type ILayerCompatSupportRequirements,
type LayerCompatCheckResult,
} from "../../layerCompat.js";

const pkgVersion = "1.0.0";

describe("checkLayerCompatibility", () => {
it("should return not compatible when other layer doesn't support ILayerCompatDetails", () => {
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
requiredFeatures: ["feature1", "feature2"],
minSupportedGeneration: 1,
};

const result: LayerCompatCheckResult = checkLayerCompatibility(
compatSupportRequirementsLayer1,
undefined /* compatDetailsLayer2 */,
);
const expectedResults: LayerCompatCheckResult = {
isCompatible: false,
isGenerationCompatible: false,
unsupportedFeatures: compatSupportRequirementsLayer1.requiredFeatures,
};
assert.deepStrictEqual(result, expectedResults, "Layers should not be compatible");
});

it("should return compatible when other layer doesn't support ILayerCompatDetails (back compat)", () => {
// For backwards compatibility, the minSupportedGeneration is 0 and there are no required features.
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
requiredFeatures: [],
minSupportedGeneration: 0,
};

const result: LayerCompatCheckResult = checkLayerCompatibility(
compatSupportRequirementsLayer1,
undefined /* compatDetailsLayer2 */,
);
const expectedResults: LayerCompatCheckResult = {
isCompatible: true,
};
assert.deepStrictEqual(result, expectedResults, "Layers should be compatible");
markfields marked this conversation as resolved.
Show resolved Hide resolved
});

it("should return compatible when both generation and features are compatible", () => {
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
requiredFeatures: ["feature1", "feature2"],
minSupportedGeneration: 1,
};

const compatDetailsLayer2: ILayerCompatDetails = {
pkgVersion,
generation: 1,
supportedFeatures: new Set(["feature1", "feature2", "feature3"]),
};
const result: LayerCompatCheckResult = checkLayerCompatibility(
compatSupportRequirementsLayer1,
compatDetailsLayer2,
);
const expectedResults: LayerCompatCheckResult = {
isCompatible: true,
};
assert.deepStrictEqual(result, expectedResults, "Layers should be compatible");
});

it("should return not compatible when generation is incompatible", () => {
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
requiredFeatures: ["feature1", "feature2"],
minSupportedGeneration: 2,
};
// Layer 2 has lower generation (1) than the minimum supported generation of Layer 1 (2).
const compatDetailsLayer2: ILayerCompatDetails = {
pkgVersion,
generation: 1,
supportedFeatures: new Set(["feature1", "feature2"]),
};

const result: LayerCompatCheckResult = checkLayerCompatibility(
compatSupportRequirementsLayer1,
compatDetailsLayer2,
);
const expectedResults: LayerCompatCheckResult = {
isCompatible: false,
isGenerationCompatible: false,
unsupportedFeatures: undefined,
};

assert.deepStrictEqual(
result,
expectedResults,
"Layers should not be compatible because generation is not compatible",
);
});

it("should return not compatible when features are incompatible", () => {
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
requiredFeatures: ["feature1", "feature2"],
minSupportedGeneration: 1,
};
// Layer 2 doesn't support feature2.
const compatDetailsLayer2: ILayerCompatDetails = {
pkgVersion,
generation: 1,
supportedFeatures: new Set(["feature1", "feature3"]),
};

const result: LayerCompatCheckResult = checkLayerCompatibility(
compatSupportRequirementsLayer1,
compatDetailsLayer2,
);
const expectedResults: LayerCompatCheckResult = {
isCompatible: false,
isGenerationCompatible: true,
unsupportedFeatures: ["feature2"],
};

assert.deepStrictEqual(
result,
expectedResults,
"Layers should not be compatible because required features are not supported",
);
});

it("should return not compatible when both generation and features are incompatible", () => {
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
requiredFeatures: ["feature1", "feature2"],
minSupportedGeneration: 2,
};
// Layer 2 doesn't support feature1 or feature2.
const compatDetailsLayer2: ILayerCompatDetails = {
pkgVersion,
generation: 1,
supportedFeatures: new Set(["feature3"]),
};

const result: LayerCompatCheckResult = checkLayerCompatibility(
compatSupportRequirementsLayer1,
compatDetailsLayer2,
);
const expectedResults: LayerCompatCheckResult = {
isCompatible: false,
isGenerationCompatible: false,
unsupportedFeatures: compatSupportRequirementsLayer1.requiredFeatures,
};

assert.deepStrictEqual(
result,
expectedResults,
"Layers should not be compatible because no required features are supported",
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export interface IContainerContext {
readonly submitSignalFn: (contents: unknown, targetClientId?: string) => void;
// (undocumented)
readonly submitSummaryFn: (summaryOp: ISummaryContent, referenceSequenceNumber?: number) => number;
// (undocumented)
// @deprecated (undocumented)
readonly supportedFeatures?: ReadonlyMap<string, unknown>;
// (undocumented)
readonly taggedLogger: ITelemetryBaseLogger;
Expand Down
3 changes: 3 additions & 0 deletions packages/common/container-definitions/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ export interface IContainerContext {

updateDirtyContainerState(dirty: boolean): void;

/**
* @deprecated - This has been deprecated. It was used internally and there is no replacement.
*/
readonly supportedFeatures?: ReadonlyMap<string, unknown>;

/**
Expand Down
17 changes: 15 additions & 2 deletions packages/loader/container-loader/src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

/* eslint-disable unicorn/consistent-function-scoping */

import { TypedEventEmitter, performance } from "@fluid-internal/client-utils";
import {
TypedEventEmitter,
performance,
type ILayerCompatDetails,
} from "@fluid-internal/client-utils";
import {
AttachState,
IAudience,
Expand Down Expand Up @@ -123,6 +127,7 @@ import {
getPackageName,
} from "./contracts.js";
import { DeltaManager, IConnectionArgs } from "./deltaManager.js";
import { validateRuntimeCompatibility } from "./layerCompatState.js";
// eslint-disable-next-line import/no-deprecated
import { IDetachedBlobStorage, ILoaderOptions, RelativeLoader } from "./loader.js";
import {
Expand Down Expand Up @@ -2461,11 +2466,19 @@ export class Container
snapshot,
);

this._runtime = await PerformanceEvent.timedExecAsync(
const runtime = await PerformanceEvent.timedExecAsync(
this.subLogger,
{ eventName: "InstantiateRuntime" },
async () => runtimeFactory.instantiateRuntime(context, existing),
);

const maybeRuntimeCompatDetails = runtime as FluidObject<ILayerCompatDetails>;
validateRuntimeCompatibility(maybeRuntimeCompatDetails.ILayerCompatDetails, (error) =>
this.dispose(error),
);

this._runtime = runtime;

this._lifecycleEvents.emit("runtimeInstantiated");

this._loadedCodeDetails = codeDetails;
Expand Down
Loading
Loading