Skip to content
1 change: 1 addition & 0 deletions packages/common/client-utils/src/indexBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
export { createEmitter } from "./events/index.js";

export {
type FluidLayer,
checkLayerCompatibility,
type LayerCompatCheckResult,
type ILayerCompatDetails,
Expand Down
1 change: 1 addition & 0 deletions packages/common/client-utils/src/indexNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
export { createEmitter } from "./events/index.js";

export {
type FluidLayer,
checkLayerCompatibility,
type LayerCompatCheckResult,
type ILayerCompatDetails,
Expand Down
6 changes: 6 additions & 0 deletions packages/common/client-utils/src/layerCompat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
* Licensed under the MIT License.
*/

/**
* The different Fluid layers in a client.
* @internal
*/
export type FluidLayer = "loader" | "driver" | "runtime" | "datastore";

/**
* Result of a layer compatibility check - whether a layer is compatible with another layer.
* @internal
Expand Down
6 changes: 5 additions & 1 deletion packages/loader/container-loader/src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@ export class Container
validateDriverCompatibility(
maybeDriverCompatDetails.ILayerCompatDetails,
(error) => {} /* disposeFn */, // There is nothing to dispose here, so just ignore the error.
subLogger,
);

this.connectionTransitionTimes[ConnectionState.Disconnected] = performanceNow();
Expand Down Expand Up @@ -2481,7 +2482,10 @@ export class Container

// Validate that the Runtime is compatible with this Loader.
const maybeRuntimeCompatDetails = runtime as FluidObject<ILayerCompatDetails>;
validateRuntimeCompatibility(maybeRuntimeCompatDetails.ILayerCompatDetails);
validateRuntimeCompatibility(
maybeRuntimeCompatDetails.ILayerCompatDetails,
this.mc.logger,
);

this._runtime = runtime;
this._lifecycleEvents.emit("runtimeInstantiated");
Expand Down
55 changes: 19 additions & 36 deletions packages/loader/container-loader/src/loaderLayerCompatState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
* Licensed under the MIT License.
*/

import {
checkLayerCompatibility,
type ILayerCompatDetails,
type ILayerCompatSupportRequirements,
import type {
ILayerCompatDetails,
ILayerCompatSupportRequirements,
} from "@fluid-internal/client-utils";
import type { ICriticalContainerError } from "@fluidframework/container-definitions";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
import { validateLayerCompatibility } from "@fluidframework/telemetry-utils/internal";

import { pkgVersion } from "./packageVersion.js";

Expand Down Expand Up @@ -78,25 +78,17 @@ export const driverSupportRequirementsForLoader: ILayerCompatSupportRequirements
*/
export function validateRuntimeCompatibility(
maybeRuntimeCompatDetails: ILayerCompatDetails | undefined,
logger: ITelemetryBaseLogger,
): void {
const layerCheckResult = checkLayerCompatibility(
validateLayerCompatibility(
"loader",
"runtime",
loaderCompatDetailsForRuntime,
runtimeSupportRequirementsForLoader,
maybeRuntimeCompatDetails,
() => {} /* disposeFn - no op. This will be handled by the caller */,
logger,
);
if (!layerCheckResult.isCompatible) {
const error = new UsageError("Loader is not compatible with Runtime", {
errorDetails: JSON.stringify({
loaderVersion: loaderCompatDetailsForRuntime.pkgVersion,
runtimeVersion: maybeRuntimeCompatDetails?.pkgVersion,
loaderGeneration: loaderCompatDetailsForRuntime.generation,
runtimeGeneration: maybeRuntimeCompatDetails?.generation,
minSupportedGeneration: runtimeSupportRequirementsForLoader.minSupportedGeneration,
isGenerationCompatible: layerCheckResult.isGenerationCompatible,
unsupportedFeatures: layerCheckResult.unsupportedFeatures,
}),
});
throw error;
}
}

/**
Expand All @@ -106,24 +98,15 @@ export function validateRuntimeCompatibility(
export function validateDriverCompatibility(
maybeDriverCompatDetails: ILayerCompatDetails | undefined,
disposeFn: (error?: ICriticalContainerError) => void,
logger: ITelemetryBaseLogger,
): void {
const layerCheckResult = checkLayerCompatibility(
validateLayerCompatibility(
"loader",
"driver",
loaderCompatDetailsForRuntime,
driverSupportRequirementsForLoader,
maybeDriverCompatDetails,
disposeFn,
logger,
);
if (!layerCheckResult.isCompatible) {
const error = new UsageError("Loader is not compatible with Driver", {
errorDetails: JSON.stringify({
loaderVersion: loaderCoreCompatDetails.pkgVersion,
driverVersion: maybeDriverCompatDetails?.pkgVersion,
loaderGeneration: loaderCoreCompatDetails.generation,
driverGeneration: maybeDriverCompatDetails?.generation,
minSupportedGeneration: driverSupportRequirementsForLoader.minSupportedGeneration,
isGenerationCompatible: layerCheckResult.isGenerationCompatible,
unsupportedFeatures: layerCheckResult.unsupportedFeatures,
}),
});
disposeFn(error);
throw error;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
type ITelemetryBaseProperties,
} from "@fluidframework/core-interfaces/internal";
import type { IResolvedUrl, IUrlResolver } from "@fluidframework/driver-definitions/internal";
import { isFluidError } from "@fluidframework/telemetry-utils/internal";
import { createChildLogger, isFluidError } from "@fluidframework/telemetry-utils/internal";
import Sinon from "sinon";

import { Loader } from "../loader.js";
Expand Down Expand Up @@ -45,7 +45,7 @@ function validateFailureProperties(
error: Error,
isGenerationCompatible: boolean,
layerGeneration: number,
layerType: "Runtime" | "Driver",
layerType: "runtime" | "driver",
unsupportedFeatures?: string[],
): boolean {
assert(
Expand All @@ -59,39 +59,49 @@ function validateFailureProperties(
);
const telemetryProps = error.getTelemetryProperties();
assert(typeof telemetryProps.errorDetails === "string", "Error details should be present");
const properties = JSON.parse(telemetryProps.errorDetails) as ITelemetryBaseProperties;
const detailedProperties = JSON.parse(
telemetryProps.errorDetails,
) as ITelemetryBaseProperties;
assert.strictEqual(
properties.isGenerationCompatible,
detailedProperties.isGenerationCompatible,
isGenerationCompatible,
"Generation compatibility not as expected",
);
assert.strictEqual(properties.loaderVersion, pkgVersion, "Loader version not as expected");
assert.strictEqual(
properties.loaderGeneration,
telemetryProps.loaderVersion,
pkgVersion,
"Loader version not as expected",
);
assert.strictEqual(
detailedProperties.loaderGeneration,
loaderCoreCompatDetails.generation,
"Loader generation not as expected",
);
assert.deepStrictEqual(
properties.unsupportedFeatures,
detailedProperties.unsupportedFeatures,
unsupportedFeatures,
"Unsupported features not as expected",
);

if (layerType === "Runtime") {
if (layerType === "runtime") {
assert.strictEqual(
properties.runtimeVersion,
telemetryProps.runtimeVersion,
pkgVersion,
"Runtime version not as expected",
);
assert.strictEqual(
properties.runtimeGeneration,
detailedProperties.runtimeGeneration,
layerGeneration,
"Runtime generation not as expected",
);
} else {
assert.strictEqual(properties.driverVersion, pkgVersion, "Driver version not as expected");
assert.strictEqual(
properties.driverGeneration,
telemetryProps.driverVersion,
pkgVersion,
"Driver version not as expected",
);
assert.strictEqual(
detailedProperties.driverGeneration,
layerGeneration,
"Driver generation not as expected",
);
Expand All @@ -100,10 +110,10 @@ function validateFailureProperties(
}

function validateDisposeCall(
layerType: "Runtime" | "Driver",
layerType: "runtime" | "driver",
disposeFn: Sinon.SinonSpy,
): void {
if (layerType === "Runtime") {
if (layerType === "runtime") {
// In case of "Runtime", the dispose is not called during validation. It is called as part of the overall
// container creation / load.
assert(disposeFn.notCalled, `Dispose should not be called for ${layerType} layer`);
Expand All @@ -118,23 +128,26 @@ describe("Loader Layer compatibility", () => {
* and has the correct error / properties.
*/
describe("Validation error and properties", () => {
const logger = createChildLogger();
const testCases: {
layerType: "Runtime" | "Driver";
layerType: "runtime" | "driver";
layerSupportRequirements: ILayerCompatSupportRequirementsOverride;
validateCompatibility: (
maybeCompatDetails: ILayerCompatDetails | undefined,
disposeFn: (error?: ICriticalContainerError) => void,
) => void;
}[] = [
{
layerType: "Runtime",
validateCompatibility: validateRuntimeCompatibility,
layerType: "runtime",
validateCompatibility: (maybeCompatDetails, disposeFn) =>
validateRuntimeCompatibility(maybeCompatDetails, logger),
layerSupportRequirements:
runtimeSupportRequirementsForLoader as ILayerCompatSupportRequirementsOverride,
},
{
layerType: "Driver",
validateCompatibility: validateDriverCompatibility,
layerType: "driver",
validateCompatibility: (maybeCompatDetails, disposeFn) =>
validateDriverCompatibility(maybeCompatDetails, disposeFn, logger),
layerSupportRequirements:
driverSupportRequirementsForLoader as ILayerCompatSupportRequirementsOverride,
},
Expand Down Expand Up @@ -264,16 +277,16 @@ describe("Loader Layer compatibility", () => {
*/
describe("Validation during load / initialization", () => {
const testCases: {
layerType: "Runtime" | "Driver";
layerType: "runtime" | "driver";
layerSupportRequirements: ILayerCompatSupportRequirementsOverride;
}[] = [
{
layerType: "Runtime",
layerType: "runtime",
layerSupportRequirements:
runtimeSupportRequirementsForLoader as ILayerCompatSupportRequirementsOverride,
},
{
layerType: "Driver",
layerType: "driver",
layerSupportRequirements:
driverSupportRequirementsForLoader as ILayerCompatSupportRequirementsOverride,
},
Expand Down Expand Up @@ -317,11 +330,11 @@ describe("Loader Layer compatibility", () => {
};
const loader = new Loader({
codeLoader: createTestCodeLoaderProxy(
testCase.layerType === "Runtime" ? { layerCompatDetails } : {},
testCase.layerType === "runtime" ? { layerCompatDetails } : {},
),
documentServiceFactory: createTestDocumentServiceFactoryProxy(
resolvedUrl,
testCase.layerType === "Driver" ? layerCompatDetails : undefined,
testCase.layerType === "driver" ? layerCompatDetails : undefined,
),
urlResolver,
});
Expand All @@ -341,11 +354,11 @@ describe("Loader Layer compatibility", () => {
};
const loader = new Loader({
codeLoader: createTestCodeLoaderProxy(
testCase.layerType === "Runtime" ? { layerCompatDetails } : {},
testCase.layerType === "runtime" ? { layerCompatDetails } : {},
),
documentServiceFactory: createTestDocumentServiceFactoryProxy(
resolvedUrl,
testCase.layerType === "Driver" ? layerCompatDetails : undefined,
testCase.layerType === "driver" ? layerCompatDetails : undefined,
),
urlResolver,
});
Expand Down
15 changes: 8 additions & 7 deletions packages/runtime/container-runtime/src/containerRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1581,13 +1581,6 @@ export class ContainerRuntime

this.isSnapshotInstanceOfISnapshot = snapshotWithContents !== undefined;

// Validate that the Loader is compatible with this Runtime.
const maybeLoaderCompatDetailsForRuntime = context as FluidObject<ILayerCompatDetails>;
validateLoaderCompatibility(
maybeLoaderCompatDetailsForRuntime.ILayerCompatDetails,
this.disposeFn,
);

this.mc = createChildMonitoringContext({
logger: this.baseLogger,
namespace: "ContainerRuntime",
Expand All @@ -1598,6 +1591,14 @@ export class ContainerRuntime
},
});

// Validate that the Loader is compatible with this Runtime.
const maybeLoaderCompatDetailsForRuntime = context as FluidObject<ILayerCompatDetails>;
validateLoaderCompatibility(
maybeLoaderCompatDetailsForRuntime.ILayerCompatDetails,
this.disposeFn,
this.mc.logger,
);

// If we support multiple algorithms in the future, then we would need to manage it here carefully.
// We can use runtimeOptions.compressionOptions.compressionAlgorithm, but only if it's in the schema list!
// If it's not in the list, then we will need to either use no compression, or fallback to some other (supported by format)
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/container-runtime/src/dataStoreContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,7 @@ export abstract class FluidDataStoreContext
validateDatastoreCompatibility(
maybeDataStoreCompatDetails.ILayerCompatDetails,
this.dispose.bind(this),
this.mc.logger,
);

// And now mark the runtime active
Expand Down
Loading
Loading