Skip to content

Commit 4c06412

Browse files
(compat) Added supported features and generation across the Loader / Runtime boundary (microsoft#22877)
## Description Added `supportedFeatures` and `generation`properties to Runtime and Loader layer boundary. These will be used to validate that the layers are compatible. These two properties along with `pkgVersion` are added to a `ILayerCompatDetails` interface. `ContainterContext` and `ContainerRuntime` implements this interface provider and uses the provider pattern to access it from the other object. The `interface` is internal and is not exposed since this in an internal implementation. `pkgVersion` is includes in the error / warning logs to give additional context to users as to what the incompatible version combination is. Also, added a `ILayerCompatSupportRequirements` interface that has properties that a layer requires other layer to be at to be compatible. ### Supported features - Both the Runtime and Loader layers have a `supportedFeatures` property that includes a set of features that a layer supports. This is advertised to the other layer at the layer boundary. - Both the layers have a `requiredFeatures` array that includes a set of features that it requires the other layer to support in order to be compatible. _Note that this is internal to that layer and is not advertised._ - At runtime, each layer validates that all the entries in `requiredFeatures` is present in the `supportedFeatures` of the other layer. If not, it closes the container with a usage error that says layers are not compatible. - Any change that adds new behavior or changes existing behavior across a layer boundary must add an entry to the supported features set. For example, changes such as "move writing protocol tree to summary from Loader to Runtime" should add an entry to the supported features set. ### Generation - In addition to supported features, a `generation` is also added to both the layers. This is advertised to the other layer at the layer boundary. - Generation is an integer which will be incremented every month. It will be used to validate compatibility more strictly (time-based) than supported features where layers are incompatible only if there are unsupported features. _Note: The logic to update the generation will be added in a later PR. See [AB#27054](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/27054) for a proposed solution._ - One key reason for adding generation is to have a clear cut off point where we officially stop supporting layer combinations. Our compat tests will test combinations up to this cut off point. The idea is that we test what we support and whatever we don't test should not be supported. This will help us achieve that. - Say that Runtime / Loader compatibility is N months. We can start throwing warnings when the layers are almost N-months apart saying that this combination is not supported. This will give applications an early signal that layers are about to be incompatible and they can take necessary steps. We can also choose to close the container if we decide to be strict about the layer combinations. - Both the layers have a `minSupportedGeneration` that it needs the other layer to be at. _Note that this is internal to that layer and is not advertised._ - At runtime, each layer validates that the `generation` of the other layer is >= `minSupportedGeneration`. - For example, say that the Runtime is at generation 20 and has a `minSupportedGeneration` of 8 for the Loader layer (12 month support window). If it sees that Loader's generation is lower than 8, the layers are incompatible. ### Backwards compatibility To support older layers before layer compat enforcement is introduced, the `minSupportedGeneration` is set to 0 whereas `generation` starts at 1. For older layers that did not implement `ILayerCompatDetails`, their generation is set to 0 and supported features is set to an empty set. So, their generation is compatible until we update the `minSupportedGeneration`. [AB#20805](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/20805)
1 parent 17da2df commit 4c06412

18 files changed

+1158
-14
lines changed

.changeset/light-pears-hammer.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@fluidframework/container-definitions": minor
3+
---
4+
---
5+
"section": deprecation
6+
---
7+
8+
`supportedFeatures` is deprecated in `IContainerContext`
9+
10+
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.

packages/common/client-utils/src/indexBrowser.ts

+8
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,11 @@ export {
2828
} from "./typedEventEmitter.js";
2929

3030
export { createEmitter } from "./events/index.js";
31+
32+
export {
33+
checkLayerCompatibility,
34+
type LayerCompatCheckResult,
35+
type ILayerCompatDetails,
36+
type IProvideLayerCompatDetails,
37+
type ILayerCompatSupportRequirements,
38+
} from "./layerCompat.js";

packages/common/client-utils/src/indexNode.ts

+8
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,11 @@ export {
2828
} from "./typedEventEmitter.js";
2929

3030
export { createEmitter } from "./events/index.js";
31+
32+
export {
33+
checkLayerCompatibility,
34+
type LayerCompatCheckResult,
35+
type ILayerCompatDetails,
36+
type IProvideLayerCompatDetails,
37+
type ILayerCompatSupportRequirements,
38+
} from "./layerCompat.js";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
/**
7+
* Result of a layer compatibility check - whether a layer is compatible with another layer.
8+
* @internal
9+
*/
10+
export type LayerCompatCheckResult =
11+
| { readonly isCompatible: true }
12+
| {
13+
readonly isCompatible: false;
14+
/**
15+
* Whether the generation of the layer is compatible with the other layer.
16+
*/
17+
readonly isGenerationCompatible: boolean;
18+
/**
19+
* The features that are required by the layer but are not supported by the other layer. This will only
20+
* be set if there are unsupported features.
21+
*/
22+
readonly unsupportedFeatures: readonly string[] | undefined;
23+
};
24+
25+
/**
26+
* @internal
27+
*/
28+
export const ILayerCompatDetails: keyof IProvideLayerCompatDetails = "ILayerCompatDetails";
29+
30+
/**
31+
* @internal
32+
*/
33+
export interface IProvideLayerCompatDetails {
34+
readonly ILayerCompatDetails: ILayerCompatDetails;
35+
}
36+
37+
/**
38+
* This interface is used to communicate the compatibility details of a layer to another layer.
39+
* @internal
40+
*/
41+
export interface ILayerCompatDetails extends Partial<IProvideLayerCompatDetails> {
42+
/**
43+
* A list of features supported by the layer at a particular layer boundary. This is used to check if these
44+
* set of features satisfy the requirements of another layer.
45+
*/
46+
readonly supportedFeatures: ReadonlySet<string>;
47+
/**
48+
* The generation of the layer. The other layer at the layer boundary uses this to check if this satisfies
49+
* the minimum generation it requires to be compatible.
50+
*/
51+
readonly generation: number;
52+
/**
53+
* The package version of the layer. When an incompatibility is detected, this is used to provide more context
54+
* on what the versions of the incompatible layers are.
55+
*/
56+
readonly pkgVersion: string;
57+
}
58+
59+
/**
60+
* This is the default compat details for a layer when it doesn't provide any compat details. This is used for
61+
* backwards compatibility to allow older layers to work before compatibility enforcement was introduced.
62+
* @internal
63+
*/
64+
export const defaultLayerCompatDetails: ILayerCompatDetails = {
65+
supportedFeatures: new Set(),
66+
generation: 0, // 0 is reserved for layers before compatibility enforcement was introduced.
67+
pkgVersion: "unknown",
68+
};
69+
70+
/**
71+
* The requirements that a layer needs another layer to support for them to be compatible.
72+
* @internal
73+
*/
74+
export interface ILayerCompatSupportRequirements {
75+
/**
76+
* The minimum supported generation the other layer needs to be at.
77+
*/
78+
readonly minSupportedGeneration: number;
79+
/**
80+
* The features that the other layer needs to support.
81+
*/
82+
readonly requiredFeatures: readonly string[];
83+
}
84+
85+
/**
86+
* Checks compatibility of a layer (layer1) with another layer (layer2).
87+
* @param compatSupportRequirementsLayer1 - The requirements from layer1 that layer2 needs to meet.
88+
* @param compatDetailsLayer2 - The compatibility details of the layer2. If this is undefined, then the
89+
* default compatibility details are used for backwards compatibility.
90+
* @returns The result of the compatibility check indicating whether layer2 is compatible with layer1.
91+
*
92+
* @internal
93+
*/
94+
export function checkLayerCompatibility(
95+
compatSupportRequirementsLayer1: ILayerCompatSupportRequirements,
96+
compatDetailsLayer2: ILayerCompatDetails | undefined,
97+
): LayerCompatCheckResult {
98+
const compatDetailsLayer2ToUse = compatDetailsLayer2 ?? defaultLayerCompatDetails;
99+
let isGenerationCompatible = true;
100+
const unsupportedFeatures: string[] = [];
101+
102+
// If layer2's generation is less than the required minimum supported generation of layer1,
103+
// then it is not compatible.
104+
if (
105+
compatDetailsLayer2ToUse.generation <
106+
compatSupportRequirementsLayer1.minSupportedGeneration
107+
) {
108+
isGenerationCompatible = false;
109+
}
110+
111+
// All features required by layer1 must be supported by layer2 to be compatible.
112+
for (const feature of compatSupportRequirementsLayer1.requiredFeatures) {
113+
if (!compatDetailsLayer2ToUse.supportedFeatures.has(feature)) {
114+
unsupportedFeatures.push(feature);
115+
}
116+
}
117+
118+
return isGenerationCompatible && unsupportedFeatures.length === 0
119+
? { isCompatible: true }
120+
: {
121+
isCompatible: false,
122+
isGenerationCompatible,
123+
unsupportedFeatures: unsupportedFeatures.length > 0 ? unsupportedFeatures : undefined,
124+
};
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import { strict as assert } from "node:assert";
7+
8+
import {
9+
checkLayerCompatibility,
10+
type ILayerCompatDetails,
11+
type ILayerCompatSupportRequirements,
12+
type LayerCompatCheckResult,
13+
} from "../../layerCompat.js";
14+
15+
const pkgVersion = "1.0.0";
16+
17+
describe("checkLayerCompatibility", () => {
18+
it("should return not compatible when other layer doesn't support ILayerCompatDetails", () => {
19+
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
20+
requiredFeatures: ["feature1", "feature2"],
21+
minSupportedGeneration: 1,
22+
};
23+
24+
const result: LayerCompatCheckResult = checkLayerCompatibility(
25+
compatSupportRequirementsLayer1,
26+
undefined /* compatDetailsLayer2 */,
27+
);
28+
const expectedResults: LayerCompatCheckResult = {
29+
isCompatible: false,
30+
isGenerationCompatible: false,
31+
unsupportedFeatures: compatSupportRequirementsLayer1.requiredFeatures,
32+
};
33+
assert.deepStrictEqual(result, expectedResults, "Layers should not be compatible");
34+
});
35+
36+
it("should return compatible when other layer doesn't support ILayerCompatDetails (back compat)", () => {
37+
// For backwards compatibility, the minSupportedGeneration is 0 and there are no required features.
38+
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
39+
requiredFeatures: [],
40+
minSupportedGeneration: 0,
41+
};
42+
43+
const result: LayerCompatCheckResult = checkLayerCompatibility(
44+
compatSupportRequirementsLayer1,
45+
undefined /* compatDetailsLayer2 */,
46+
);
47+
const expectedResults: LayerCompatCheckResult = {
48+
isCompatible: true,
49+
};
50+
assert.deepStrictEqual(result, expectedResults, "Layers should be compatible");
51+
});
52+
53+
it("should return compatible when both generation and features are compatible", () => {
54+
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
55+
requiredFeatures: ["feature1", "feature2"],
56+
minSupportedGeneration: 1,
57+
};
58+
59+
const compatDetailsLayer2: ILayerCompatDetails = {
60+
pkgVersion,
61+
generation: 1,
62+
supportedFeatures: new Set(["feature1", "feature2", "feature3"]),
63+
};
64+
const result: LayerCompatCheckResult = checkLayerCompatibility(
65+
compatSupportRequirementsLayer1,
66+
compatDetailsLayer2,
67+
);
68+
const expectedResults: LayerCompatCheckResult = {
69+
isCompatible: true,
70+
};
71+
assert.deepStrictEqual(result, expectedResults, "Layers should be compatible");
72+
});
73+
74+
it("should return not compatible when generation is incompatible", () => {
75+
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
76+
requiredFeatures: ["feature1", "feature2"],
77+
minSupportedGeneration: 2,
78+
};
79+
// Layer 2 has lower generation (1) than the minimum supported generation of Layer 1 (2).
80+
const compatDetailsLayer2: ILayerCompatDetails = {
81+
pkgVersion,
82+
generation: 1,
83+
supportedFeatures: new Set(["feature1", "feature2"]),
84+
};
85+
86+
const result: LayerCompatCheckResult = checkLayerCompatibility(
87+
compatSupportRequirementsLayer1,
88+
compatDetailsLayer2,
89+
);
90+
const expectedResults: LayerCompatCheckResult = {
91+
isCompatible: false,
92+
isGenerationCompatible: false,
93+
unsupportedFeatures: undefined,
94+
};
95+
96+
assert.deepStrictEqual(
97+
result,
98+
expectedResults,
99+
"Layers should not be compatible because generation is not compatible",
100+
);
101+
});
102+
103+
it("should return not compatible when features are incompatible", () => {
104+
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
105+
requiredFeatures: ["feature1", "feature2"],
106+
minSupportedGeneration: 1,
107+
};
108+
// Layer 2 doesn't support feature2.
109+
const compatDetailsLayer2: ILayerCompatDetails = {
110+
pkgVersion,
111+
generation: 1,
112+
supportedFeatures: new Set(["feature1", "feature3"]),
113+
};
114+
115+
const result: LayerCompatCheckResult = checkLayerCompatibility(
116+
compatSupportRequirementsLayer1,
117+
compatDetailsLayer2,
118+
);
119+
const expectedResults: LayerCompatCheckResult = {
120+
isCompatible: false,
121+
isGenerationCompatible: true,
122+
unsupportedFeatures: ["feature2"],
123+
};
124+
125+
assert.deepStrictEqual(
126+
result,
127+
expectedResults,
128+
"Layers should not be compatible because required features are not supported",
129+
);
130+
});
131+
132+
it("should return not compatible when both generation and features are incompatible", () => {
133+
const compatSupportRequirementsLayer1: ILayerCompatSupportRequirements = {
134+
requiredFeatures: ["feature1", "feature2"],
135+
minSupportedGeneration: 2,
136+
};
137+
// Layer 2 doesn't support feature1 or feature2.
138+
const compatDetailsLayer2: ILayerCompatDetails = {
139+
pkgVersion,
140+
generation: 1,
141+
supportedFeatures: new Set(["feature3"]),
142+
};
143+
144+
const result: LayerCompatCheckResult = checkLayerCompatibility(
145+
compatSupportRequirementsLayer1,
146+
compatDetailsLayer2,
147+
);
148+
const expectedResults: LayerCompatCheckResult = {
149+
isCompatible: false,
150+
isGenerationCompatible: false,
151+
unsupportedFeatures: compatSupportRequirementsLayer1.requiredFeatures,
152+
};
153+
154+
assert.deepStrictEqual(
155+
result,
156+
expectedResults,
157+
"Layers should not be compatible because no required features are supported",
158+
);
159+
});
160+
});

packages/common/container-definitions/api-report/container-definitions.legacy.alpha.api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export interface IContainerContext {
164164
readonly submitSignalFn: (contents: unknown, targetClientId?: string) => void;
165165
// (undocumented)
166166
readonly submitSummaryFn: (summaryOp: ISummaryContent, referenceSequenceNumber?: number) => number;
167-
// (undocumented)
167+
// @deprecated (undocumented)
168168
readonly supportedFeatures?: ReadonlyMap<string, unknown>;
169169
// (undocumented)
170170
readonly taggedLogger: ITelemetryBaseLogger;

packages/common/container-definitions/src/runtime.ts

+3
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ export interface IContainerContext {
206206

207207
updateDirtyContainerState(dirty: boolean): void;
208208

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

211214
/**

packages/loader/container-loader/src/container.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

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

8-
import { TypedEventEmitter, performance } from "@fluid-internal/client-utils";
8+
import {
9+
TypedEventEmitter,
10+
performance,
11+
type ILayerCompatDetails,
12+
} from "@fluid-internal/client-utils";
913
import {
1014
AttachState,
1115
IAudience,
@@ -123,6 +127,7 @@ import {
123127
getPackageName,
124128
} from "./contracts.js";
125129
import { DeltaManager, IConnectionArgs } from "./deltaManager.js";
130+
import { validateRuntimeCompatibility } from "./layerCompatState.js";
126131
// eslint-disable-next-line import/no-deprecated
127132
import { IDetachedBlobStorage, ILoaderOptions, RelativeLoader } from "./loader.js";
128133
import {
@@ -2461,11 +2466,19 @@ export class Container
24612466
snapshot,
24622467
);
24632468

2464-
this._runtime = await PerformanceEvent.timedExecAsync(
2469+
const runtime = await PerformanceEvent.timedExecAsync(
24652470
this.subLogger,
24662471
{ eventName: "InstantiateRuntime" },
24672472
async () => runtimeFactory.instantiateRuntime(context, existing),
24682473
);
2474+
2475+
const maybeRuntimeCompatDetails = runtime as FluidObject<ILayerCompatDetails>;
2476+
validateRuntimeCompatibility(maybeRuntimeCompatDetails.ILayerCompatDetails, (error) =>
2477+
this.dispose(error),
2478+
);
2479+
2480+
this._runtime = runtime;
2481+
24692482
this._lifecycleEvents.emit("runtimeInstantiated");
24702483

24712484
this._loadedCodeDetails = codeDetails;

0 commit comments

Comments
 (0)