Skip to content

Commit

Permalink
feat(migration-tools): Minimize model loading pattern presence in fav…
Browse files Browse the repository at this point in the history
…or of entryPoint (#23119)
  • Loading branch information
ChumpChief authored Nov 20, 2024
1 parent 224f59a commit d3bf90c
Show file tree
Hide file tree
Showing 33 changed files with 901 additions and 791 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { NamedFluidDataStoreRegistryEntries } from "@fluidframework/runtime-defi
/**
* @internal
*/
export interface IModelContainerRuntimeEntryPoint<T> {
getModel(container: IContainer): Promise<T>;
export interface IModelContainerRuntimeEntryPoint<ModelType> {
getModel(container: IContainer): Promise<ModelType>;
}

/**
Expand Down
89 changes: 88 additions & 1 deletion examples/utils/migration-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,94 @@

This package contains tools for migrating data from one version to another, used by Fluid examples. They are not currently intended for use in production scenarios.

See [GitHub](https://github.com/microsoft/FluidFramework) for more details on the Fluid Framework and packages within.
Use of the migration tools imposes several requirements on the container code and application, detailed here.

## Implementing `IMigratableModel`

Your data model must implement `IMigratableModel` to be migrated using the migration tools.

This includes:
1. A `version` string to identify the model version.
1. Methods to export and import data, and to detect if the model supports a given data format:
1. `importData: (initialData: ImportType) => Promise<void>`
1. `exportData: () => Promise<ExportType>`
1. `supportsDataFormat: (initialData: unknown) => initialData is ImportType`
1. A `dispose` method to clean up the container - most likely calling `IContainer.dispose`.

## Implementing the composite runtime pattern

See documentation for the composite runtime pattern [here](./src/compositeRuntime/README.md).

The migration tools expect to find an `IMigratableModel` by accessing and calling a `getModel()` function provided on the `entryPoint`. They also expect to find an `IMigrationTool` by accessing a `migrationTool` member of the `entryPoint`. These requirements are most easily satisfied by using the composite runtime pattern.

`getModel()` is a function that takes an `IContainer` to aid in producing the `IMigratableModel`. This is because the contract of `IMigratableModel` likely requires functionality from `IContainer` (especially `IContainer.dispose()`).

### Defining the entry point piece

```ts
const rootDatastoreAlias = "my-root-datastore";

export const getModelEntryPointPiece: IEntryPointPiece = {
name: "getModel",
registryEntries: [MyRootDatastoreFactory.registryEntry],
onCreate: async (runtime: IContainerRuntime): Promise<void> => {
const rootDatastore = await runtime.createDataStore(MyRootDatastoreFactory.type);
await rootDatastore.trySetAlias(rootDatastoreAlias);
},
onLoad: async (runtime: IContainerRuntime): Promise<void> => {},
createPiece: async (runtime: IContainerRuntime): Promise<(container: IContainer) => Promise<FluidObject>> => {
const entryPointHandle = await containerRuntime.getAliasedDataStoreEntryPoint(rootDatastoreAlias);

if (entryPointHandle === undefined) {
throw new Error(`Default dataStore [${rootDatastoreAlias}] must exist`);
}

// Entry points are typed as FluidObject and must be cast. Here we know it's a MyRootDatastore since
// we created it just above. Type validation can be added here if desired.
const rootDatastore = entryPointHandle.get() as Promise<MyRootDatastore>;
// MigratableAppModel (defined by the container code author) must implement IMigratableModel.
// Note that we're returning a function of type (container: IContainer) => Promise<FluidObject>,
// where the FluidObject is expected to be an IMigratableModel.
return async (container: IContainer) => new MigratableAppModel(rootDatastore, container);
},
};
```

```ts
// In the IRuntimeFactory
public async instantiateRuntime(
context: IContainerContext,
existing: boolean,
): Promise<IRuntime> {
const compositeEntryPoint = new CompositeEntryPoint();
compositeEntryPoint.addEntryPointPiece(getModelEntryPointPiece);
// migrationToolEntryPointPiece is provided by the migration-tools package
compositeEntryPoint.addEntryPointPiece(migrationToolEntryPointPiece);
return loadCompositeRuntime(context, existing, compositeEntryPoint, this.runtimeOptions);
}
```

### `migrationToolEntryPointPiece`

This package additionally provides a `migrationToolEntryPointPiece` which is an off-the-shelf implementation of the piece to provide the `IMigrationTool`. With these provided pieces, you're only responsible for implementing the `IMigratableModel` piece with your data model.

## `Migrator`

Finally, to actually execute the migration we provide the `Migrator` class. This takes a `SimpleLoader` (see below), the initially loaded model, migration tool, and container ID (TODO: can we simplify this handoff), as well as an optional `DataTransformationCallback` (see below). The migrator provides a collection of APIs to observe the state of the migration, as well as to acquire the new container after migration completes. (TODO: should the migrate() API also live here?)

TODO: Detail usage of the Migrator

### `SimpleLoader`

See documentation for `SimpleLoader` [here](./src/simpleLoader/README.md). `SimpleLoader` is used in place of a `Loader` and is used by the `Migrator`.

### Code loader

To migrate between two different code versions, you must also provide a code loader to the `SimpleLoader` that is capable of loading those two respective code versions. This uses the usual `ICodeDetailsLoader` interface.

### `DataTransformationCallback`

If your old and new code share an import/export format, you don't need a `DataTransformationCallback`. But if the import/export format has changed between versions, you can provide this callback to the `Migrator` and it will be called with the old exported data. This callback is responsible for transforming the data to the new format and returning the transformed data.

<!-- AUTO-GENERATED-CONTENT:START (README_FOOTER) -->

Expand Down
143 changes: 70 additions & 73 deletions examples/utils/migration-tools/api-report/migration-tools.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@
```ts

// @alpha
export type CreateModelCallback<ModelType> = (runtime: IContainerRuntime, container: IContainer) => Promise<ModelType>;
// @alpha (undocumented)
export class CompositeEntryPoint {
// (undocumented)
readonly addEntryPointPiece: (entryPointPiece: IEntryPointPiece) => void;
// (undocumented)
readonly onCreate: (runtime: IContainerRuntime) => Promise<void>;
// (undocumented)
readonly onLoad: (runtime: IContainerRuntime) => Promise<void>;
// (undocumented)
readonly provideEntryPoint: (runtime: IContainerRuntime) => Promise<Record<string, FluidObject>>;
// (undocumented)
get registryEntries(): NamedFluidDataStoreRegistryEntries;
}

// @alpha
export type DataTransformationCallback = (exportedData: unknown, modelVersion: string) => Promise<unknown>;
Expand All @@ -16,17 +27,18 @@ export interface IAcceptedMigrationDetails {
newVersion: string;
}

// @alpha
export interface IAttachedMigratableModel<ModelType> {
migrationTool: IMigrationTool;
model: ModelType;
}

// @alpha
export interface IDetachedMigratableModel<ModelType> {
attach: () => Promise<string>;
migrationTool: IMigrationTool;
model: ModelType;
// @alpha (undocumented)
export interface IEntryPointPiece {
// (undocumented)
readonly createPiece: (runtime: IContainerRuntime) => Promise<FluidObject>;
// (undocumented)
readonly name: string;
// (undocumented)
readonly onCreate: (runtime: IContainerRuntime) => Promise<void>;
// (undocumented)
readonly onLoad: (runtime: IContainerRuntime) => Promise<void>;
// (undocumented)
readonly registryEntries: NamedFluidDataStoreRegistryEntries;
}

// @alpha
Expand All @@ -41,23 +53,6 @@ export interface IMigratableModel extends IVersionedModel, IImportExportModel<un
dispose(): void;
}

// @alpha (undocumented)
export interface IMigratableModelContainerRuntimeEntryPoint<T> {
// (undocumented)
getModelAndMigrationTool(container: IContainer): Promise<{
model: T;
migrationTool: IMigrationTool;
}>;
}

// @alpha (undocumented)
export interface IMigratableModelLoader<ModelType> {
createDetached(version: string): Promise<IDetachedMigratableModel<ModelType>>;
loadExisting(id: string): Promise<IAttachedMigratableModel<ModelType>>;
loadExistingToSequenceNumber(id: string, sequenceNumber: number): Promise<IAttachedMigratableModel<ModelType>>;
supportsVersion(version: string): Promise<boolean>;
}

// @alpha (undocumented)
export interface IMigrationTool {
readonly acceptedMigration: IAcceptedMigrationDetails | undefined;
Expand Down Expand Up @@ -102,58 +97,30 @@ export interface IMigratorEvents extends IEvent {
(event: "migrationNotSupported", listener: (version: string) => void): any;
}

// @alpha
export const instantiateMigratableRuntime: <ModelType>(context: IContainerContext, existing: boolean, registryEntries: NamedFluidDataStoreRegistryEntries, createModel: CreateModelCallback<ModelType>, runtimeOptions?: IContainerRuntimeOptions) => Promise<IContainerRuntime & IRuntime>;

// @alpha
export interface IVersionedModel {
readonly version: string;
}

// @alpha (undocumented)
export class MigratableModelLoader<ModelType> implements IMigratableModelLoader<ModelType> {
constructor(props: Pick<ILoaderProps, "urlResolver" | "documentServiceFactory" | "codeLoader" | "logger"> & {
generateCreateNewRequest: () => IRequest;
});
// (undocumented)
createDetached(version: string): Promise<IDetachedMigratableModel<ModelType>>;
// (undocumented)
loadExisting(id: string): Promise<IAttachedMigratableModel<ModelType>>;
// (undocumented)
loadExistingToSequenceNumber(id: string, sequenceNumber: number): Promise<IAttachedMigratableModel<ModelType>>;
// (undocumented)
export interface ISimpleLoader {
createDetached(version: string): Promise<{
container: IContainer;
attach: () => Promise<string>;
}>;
loadExisting(id: string): Promise<IContainer>;
supportsVersion(version: string): Promise<boolean>;
}

// @alpha (undocumented)
export class MigratableSessionStorageModelLoader<ModelType> implements IMigratableModelLoader<ModelType> {
constructor(codeLoader: ICodeDetailsLoader, logger?: ITelemetryBaseLogger | undefined);
// (undocumented)
createDetached(version: string): Promise<IDetachedMigratableModel<ModelType>>;
// (undocumented)
loadExisting(id: string): Promise<IAttachedMigratableModel<ModelType>>;
// (undocumented)
loadExistingToSequenceNumber(id: string, sequenceNumber: number): Promise<IAttachedMigratableModel<ModelType>>;
// (undocumented)
supportsVersion(version: string): Promise<boolean>;
// @alpha
export interface IVersionedModel {
readonly version: string;
}

// @alpha (undocumented)
export class MigratableTinyliciousModelLoader<ModelType> implements IMigratableModelLoader<ModelType> {
constructor(codeLoader: ICodeDetailsLoader);
// (undocumented)
createDetached(version: string): Promise<IDetachedMigratableModel<ModelType>>;
// (undocumented)
loadExisting(id: string): Promise<IAttachedMigratableModel<ModelType>>;
// (undocumented)
loadExistingToSequenceNumber(id: string, sequenceNumber: number): Promise<IAttachedMigratableModel<ModelType>>;
// (undocumented)
supportsVersion(version: string): Promise<boolean>;
}
// @alpha
export const loadCompositeRuntime: (context: IContainerContext, existing: boolean, compositeEntryPoint: CompositeEntryPoint, runtimeOptions?: IContainerRuntimeOptions) => Promise<IContainerRuntime & IRuntime>;

// @alpha
export type MigrationState = "collaborating" | "stopping" | "migrating" | "migrated";

// @alpha (undocumented)
export const migrationToolEntryPointPiece: IEntryPointPiece;

// @alpha (undocumented)
export class MigrationToolFactory implements IFluidDataStoreFactory {
// (undocumented)
Expand All @@ -166,7 +133,7 @@ export class MigrationToolFactory implements IFluidDataStoreFactory {

// @alpha
export class Migrator implements IMigrator {
constructor(modelLoader: IMigratableModelLoader<IMigratableModel>, initialMigratable: IMigratableModel, initialMigrationTool: IMigrationTool, initialId: string, dataTransformationCallback?: DataTransformationCallback | undefined);
constructor(simpleLoader: ISimpleLoader, initialMigratable: IMigratableModel, initialMigrationTool: IMigrationTool, initialId: string, dataTransformationCallback?: DataTransformationCallback | undefined);
// (undocumented)
get connected(): boolean;
// (undocumented)
Expand All @@ -181,4 +148,34 @@ export class Migrator implements IMigrator {
get migrationState(): MigrationState;
}

// @alpha (undocumented)
export class SessionStorageSimpleLoader implements ISimpleLoader {
constructor(codeLoader: ICodeDetailsLoader, logger?: ITelemetryBaseLogger | undefined);
// (undocumented)
createDetached(version: string): Promise<{
container: IContainer;
attach: () => Promise<string>;
}>;
// (undocumented)
loadExisting(id: string): Promise<IContainer>;
// (undocumented)
supportsVersion(version: string): Promise<boolean>;
}

// @alpha (undocumented)
export class SimpleLoader implements ISimpleLoader {
constructor(props: Pick<ILoaderProps, "urlResolver" | "documentServiceFactory" | "codeLoader" | "logger"> & {
generateCreateNewRequest: () => IRequest;
});
// (undocumented)
createDetached(version: string): Promise<{
container: IContainer;
attach: () => Promise<string>;
}>;
// (undocumented)
loadExisting(id: string): Promise<IContainer>;
// (undocumented)
supportsVersion(version: string): Promise<boolean>;
}

```
68 changes: 68 additions & 0 deletions examples/utils/migration-tools/src/compositeRuntime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
## The composite runtime pattern

Fluid containers provide an `entryPoint`, which is how apps access the contents of the container. This `entryPoint` is specified by the author of the "container code", also known as the "container runtime factory" or `IRuntimeFactory`.

Traditionally the container code author creates a single root datastore, and accessing the container `entryPoint` simply returns that datastore. However, the `entryPoint` can actually be any arbitrary object (it is typed as a `FluidObject`).

The composite runtime pattern explores returning an object that is composed of multiple members, each independent from one another. This facilitates mixin patterns, such as adding a data migration tool to a container without impacting the root datastore.

This package provides a `CompositeEntryPoint`, which collects entry point "pieces" that are defined by the container code author (`IEntryPointPiece`). `CompositeEntryPoint` can subsequently be used with `loadCompositeRuntime()` in place of `ContainerRuntime.loadRuntime()` to produce a runtime with the desired `entryPoint`.

Each `IEntryPointPiece` consists of:

* `name`: The name that the entry point piece will be given in the resulting composite entryPoint.
* `registryEntries`: The registry entries that should be added to the container runtime.
* `onCreate`: Actions to be taken upon container creation, e.g. creating and aliasing a datastore.
* `onLoad`: Actions to be taken upon every container load.
* `createPiece`: A function to produce the entry point piece object that the app developer will access.

### Defining the entry point piece

```ts
const rootDatastoreAlias = "my-root-datastore";

export const rootDatastoreEntryPointPiece: IEntryPointPiece = {
name: "rootDatastore",
registryEntries: [MyRootDatastoreFactory.registryEntry],
onCreate: async (runtime: IContainerRuntime): Promise<void> => {
const rootDatastore = await runtime.createDataStore(MyRootDatastoreFactory.type);
await rootDatastore.trySetAlias(rootDatastoreAlias);
},
onLoad: async (runtime: IContainerRuntime): Promise<void> => {},
createPiece: async (runtime: IContainerRuntime): Promise<FluidObject> => {
const entryPointHandle = await containerRuntime.getAliasedDataStoreEntryPoint(rootDatastoreAlias);

if (entryPointHandle === undefined) {
throw new Error(`Default dataStore [${rootDatastoreAlias}] must exist`);
}

return entryPointHandle.get();
},
};
```

### Composing and loading the runtime

```ts
// In the IRuntimeFactory
public async instantiateRuntime(
context: IContainerContext,
existing: boolean,
): Promise<IRuntime> {
const compositeEntryPoint = new CompositeEntryPoint();
compositeEntryPoint.addEntryPointPiece(rootDatastoreEntryPointPiece);
// migrationToolEntryPointPiece is provided by the migration-tools package
compositeEntryPoint.addEntryPointPiece(migrationToolEntryPointPiece);
return loadCompositeRuntime(context, existing, compositeEntryPoint, this.runtimeOptions);
}
```

### Accessing the composite entryPoint from the app

```ts
// Entry points are typed as FluidObject and must be cast. Type validation can be added here if desired.
const { rootDatastore, migrationTool } = (await container.getEntryPoint()) as {
rootDatastore: MyRootDatastore;
migrationTool: IMigrationTool;
};
```
10 changes: 10 additions & 0 deletions examples/utils/migration-tools/src/compositeRuntime/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

export { IEntryPointPiece } from "./interfaces.js";
export {
CompositeEntryPoint,
loadCompositeRuntime,
} from "./loadCompositeRuntime.js";
19 changes: 19 additions & 0 deletions examples/utils/migration-tools/src/compositeRuntime/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import type { IContainerRuntime } from "@fluidframework/container-runtime-definitions/internal";
import type { FluidObject } from "@fluidframework/core-interfaces";
import type { NamedFluidDataStoreRegistryEntries } from "@fluidframework/runtime-definitions/internal";

/**
* @alpha
*/
export interface IEntryPointPiece {
readonly name: string;
readonly registryEntries: NamedFluidDataStoreRegistryEntries;
readonly onCreate: (runtime: IContainerRuntime) => Promise<void>;
readonly onLoad: (runtime: IContainerRuntime) => Promise<void>;
readonly createPiece: (runtime: IContainerRuntime) => Promise<FluidObject>;
}
Loading

0 comments on commit d3bf90c

Please sign in to comment.