-
Notifications
You must be signed in to change notification settings - Fork 536
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
POC to discuss: Add create/load Container APIs that give you an object before doing full initialization #23709
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -379,81 +379,104 @@ export class Container | |
extends EventEmitterWithErrorHandling<IContainerEvents> | ||
implements IContainer, IContainerExperimental | ||
{ | ||
/** | ||
* Not to be called directly on this type. | ||
*/ | ||
public async initialize(): Promise<void> { | ||
assert(false, "Method will be provided based on create/load API that is used"); | ||
} | ||
|
||
/** | ||
* Load an existing container. | ||
*/ | ||
public static async load( | ||
loadProps: IContainerLoadProps, | ||
createProps: IContainerCreateProps, | ||
): Promise<Container> { | ||
const container = Container.loadUninitialized(loadProps, createProps); | ||
return container.initialize().then(() => container); | ||
} | ||
|
||
/** | ||
* Load an existing container. | ||
*/ | ||
public static loadUninitialized( | ||
loadProps: IContainerLoadProps, | ||
createProps: IContainerCreateProps, | ||
): Container & { initialize: () => Promise<void> } { | ||
const { version, pendingLocalState, loadMode, resolvedUrl } = loadProps; | ||
|
||
const container = new Container(createProps, loadProps); | ||
|
||
return PerformanceEvent.timedExecAsync( | ||
container.mc.logger, | ||
{ eventName: "Load", ...loadMode }, | ||
async (event) => | ||
new Promise<Container>((resolve, reject) => { | ||
const defaultMode: IContainerLoadMode = { opsBeforeReturn: "cached" }; | ||
// if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection | ||
// to return container, so ignore this value and use undefined for opsBeforeReturn | ||
const mode: IContainerLoadMode = pendingLocalState | ||
? { ...(loadMode ?? defaultMode), opsBeforeReturn: undefined } | ||
: (loadMode ?? defaultMode); | ||
|
||
const onClosed = (err?: ICriticalContainerError): void => { | ||
// pre-0.58 error message: containerClosedWithoutErrorDuringLoad | ||
reject(err ?? new GenericError("Container closed without error during load")); | ||
}; | ||
container.on("closed", onClosed); | ||
|
||
container | ||
.load(version, mode, resolvedUrl, pendingLocalState) | ||
.finally(() => { | ||
container.removeListener("closed", onClosed); | ||
}) | ||
.then( | ||
(props) => { | ||
event.end({ ...props }); | ||
resolve(container); | ||
}, | ||
(error) => { | ||
const err = normalizeError(error); | ||
// Depending where error happens, we can be attempting to connect to web socket | ||
// and continuously retrying (consider offline mode) | ||
// Host has no container to close, so it's prudent to do it here | ||
// Note: We could only dispose the container instead of just close but that would | ||
// the telemetry where users sometimes search for ContainerClose event to look | ||
// for load failures. So not removing this at this time. | ||
container.close(err); | ||
container.dispose(err); | ||
onClosed(err); | ||
}, | ||
); | ||
}), | ||
{ start: true, end: true, cancel: "generic" }, | ||
); | ||
const initialize = async (): Promise<void> => | ||
PerformanceEvent.timedExecAsync( | ||
container.mc.logger, | ||
{ eventName: "Load", ...loadMode }, | ||
async (event) => | ||
new Promise((resolve, reject) => { | ||
const defaultMode: IContainerLoadMode = { opsBeforeReturn: "cached" }; | ||
// if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection | ||
// to return container, so ignore this value and use undefined for opsBeforeReturn | ||
const mode: IContainerLoadMode = pendingLocalState | ||
? { ...(loadMode ?? defaultMode), opsBeforeReturn: undefined } | ||
: (loadMode ?? defaultMode); | ||
|
||
const onClosed = (err?: ICriticalContainerError): void => { | ||
// pre-0.58 error message: containerClosedWithoutErrorDuringLoad | ||
reject(err ?? new GenericError("Container closed without error during load")); | ||
}; | ||
container.on("closed", onClosed); | ||
|
||
container | ||
.load(version, mode, resolvedUrl, pendingLocalState) | ||
.finally(() => { | ||
container.removeListener("closed", onClosed); | ||
}) | ||
.then( | ||
(props) => { | ||
event.end({ ...props }); | ||
resolve(); | ||
}, | ||
(error) => { | ||
const err = normalizeError(error); | ||
// Depending where error happens, we can be attempting to connect to web socket | ||
// and continuously retrying (consider offline mode) | ||
// Host has no container to close, so it's prudent to do it here | ||
// Note: We could only dispose the container instead of just close but that would | ||
// the telemetry where users sometimes search for ContainerClose event to look | ||
// for load failures. So not removing this at this time. | ||
container.close(err); | ||
container.dispose(err); | ||
onClosed(err); | ||
}, | ||
); | ||
}), | ||
{ start: true, end: true, cancel: "generic" }, | ||
); | ||
|
||
return Object.assign(container, { initialize }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i really don't like the use of object.assign in this pr. ideally we can decompose whatever is happening into free functions and small objects to build up the container, rather than tacking new functions on top the Container monolith There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I don't either, just playing around. I struggle to communicate what I'm looking for feedback on sometimes with these exploratory PRs. |
||
} | ||
|
||
/** | ||
* Create a new container in a detached state. | ||
*/ | ||
public static async createDetached( | ||
public static createDetached( | ||
createProps: IContainerCreateProps, | ||
codeDetails: IFluidCodeDetails, | ||
): Promise<Container> { | ||
): Container & { initialize: () => Promise<void> } { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Container type already has initialize on it - why do you need this new type (that looks identical to Container)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, lots to clean up in implementation, looking for feedback on the API first There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i find api easiest to review in the api reports, so you should update/generate those |
||
const container = new Container(createProps); | ||
|
||
return PerformanceEvent.timedExecAsync( | ||
container.mc.logger, | ||
{ eventName: "CreateDetached" }, | ||
async (_event) => { | ||
await container.createDetached(codeDetails); | ||
return container; | ||
}, | ||
{ start: true, end: true, cancel: "generic" }, | ||
); | ||
return Object.assign(container, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I got it right, you want symmetrical flow, i.e. call initialize() in both cases. |
||
initialize: async () => | ||
PerformanceEvent.timedExecAsync( | ||
container.mc.logger, | ||
{ eventName: "CreateDetached" }, | ||
async (_event) => { | ||
await container.createDetached(codeDetails); | ||
}, | ||
{ start: true, end: true, cancel: "generic" }, | ||
), | ||
}); | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -145,6 +145,27 @@ export async function createDetachedContainer( | |
}); | ||
} | ||
|
||
/** | ||
* Creates a new container using the specified code details but in an unattached state. While unattached, all | ||
* updates will only be local until the user explicitly attaches the container to a service provider. | ||
* @param createDetachedContainerProps - Services and properties necessary for creating detached container. | ||
* @legacy | ||
* @alpha | ||
*/ | ||
export function createDetachedContainerUninitialized( | ||
createDetachedContainerProps: ICreateDetachedContainerProps, | ||
): IContainer & { initialize: () => Promise<void> } { | ||
const loader = new Loader(createDetachedContainerProps); | ||
const container = loader.createDetachedContainerUninitialized( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd find a way not to use Loader in these new workflows. I.e. refactor code enough where loader is not present in this flow (calling APIs that came out of loader is Ok). |
||
createDetachedContainerProps.codeDetails, | ||
{ | ||
canReconnect: createDetachedContainerProps.allowReconnect, | ||
clientDetailsOverride: createDetachedContainerProps.clientDetailsOverride, | ||
}, | ||
); | ||
return container; | ||
} | ||
|
||
/** | ||
* Creates a new container using the specified snapshot but in an unattached state. While unattached, all | ||
* updates will only be local until the user explicitly attaches the container to a service provider. | ||
|
@@ -180,3 +201,19 @@ export async function loadExistingContainer( | |
loadExistingContainerProps.pendingLocalState, | ||
); | ||
} | ||
|
||
/** | ||
* Loads a container with an existing snapshot from the service. | ||
* @param loadExistingContainerProps - Services and properties necessary for loading an existing container. | ||
* @legacy | ||
* @alpha | ||
*/ | ||
export async function loadExistingContainerUninitialized( | ||
loadExistingContainerProps: ILoadExistingContainerProps, | ||
): Promise<IContainer & { initialize: () => Promise<void> }> { | ||
const loader = new Loader(loadExistingContainerProps); | ||
return loader.resolveUninitialized( | ||
loadExistingContainerProps.request, | ||
loadExistingContainerProps.pendingLocalState, | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -336,6 +336,21 @@ export class Loader implements IHostLoader { | |
clientDetailsOverride?: IClientDetails; | ||
}, | ||
): Promise<IContainer> { | ||
const container = this.createDetachedContainerUninitialized( | ||
codeDetails, | ||
createDetachedProps, | ||
); | ||
//* | ||
return container.initialize().then(() => container); | ||
} | ||
|
||
public createDetachedContainerUninitialized( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should be moving away from the loader, so i wouldn't expect any changes in the loader, the functions should do all the work |
||
codeDetails: IFluidCodeDetails, | ||
createDetachedProps?: { | ||
canReconnect?: boolean; | ||
clientDetailsOverride?: IClientDetails; | ||
}, | ||
): Container & { initialize: () => Promise<void> } { | ||
return Container.createDetached( | ||
{ | ||
...createDetachedProps, | ||
|
@@ -362,6 +377,21 @@ export class Loader implements IHostLoader { | |
} | ||
|
||
public async resolve(request: IRequest, pendingLocalState?: string): Promise<IContainer> { | ||
const eventName = pendingLocalState === undefined ? "Resolve" : "ResolveWithPendingState"; | ||
return PerformanceEvent.timedExecAsync(this.mc.logger, { eventName }, async () => { | ||
const container = await this.resolveCore( | ||
request, | ||
getAttachedContainerStateFromSerializedContainer(pendingLocalState), | ||
); | ||
await container.initialize(); | ||
return container; | ||
}); | ||
} | ||
|
||
public async resolveUninitialized( | ||
request: IRequest, | ||
pendingLocalState?: string, | ||
): Promise<Container & { initialize: () => Promise<void> }> { | ||
const eventName = pendingLocalState === undefined ? "Resolve" : "ResolveWithPendingState"; | ||
return PerformanceEvent.timedExecAsync(this.mc.logger, { eventName }, async () => { | ||
return this.resolveCore( | ||
|
@@ -374,7 +404,7 @@ export class Loader implements IHostLoader { | |
private async resolveCore( | ||
request: IRequest, | ||
pendingLocalState?: IPendingContainerState, | ||
): Promise<Container> { | ||
): Promise<Container & { initialize: () => Promise<void> }> { | ||
const resolvedAsFluid = await this.services.urlResolver.resolve(request); | ||
ensureResolvedUrlDefined(resolvedAsFluid); | ||
|
||
|
@@ -401,15 +431,15 @@ export class Loader implements IHostLoader { | |
request.headers[LoaderHeader.version] = | ||
parsed.version ?? request.headers[LoaderHeader.version]; | ||
|
||
return this.loadContainer(request, resolvedAsFluid, pendingLocalState); | ||
return this.loadContainerUninitialized(request, resolvedAsFluid, pendingLocalState); | ||
} | ||
|
||
private async loadContainer( | ||
private loadContainerUninitialized( | ||
request: IRequest, | ||
resolvedUrl: IResolvedUrl, | ||
pendingLocalState?: IPendingContainerState, | ||
): Promise<Container> { | ||
return Container.load( | ||
): Container & { initialize: () => Promise<void> } { | ||
return Container.loadUninitialized( | ||
{ | ||
resolvedUrl, | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assert text is hard to parse - it becomes clearer what it means only by reading the rest of the code.
I think we can avoid the need to have it, and the need to expose container in weird states.
How about the following:
This clearly requires proxying event registrations from shell to real container underneath.
But this kind of structure completely eliminates the risk of calling some public APIs on Container while it's not initialized. It also completely separates the workflows, allowing to remove APIs from Container (like initialize(), maybe others) that should not be there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Problem I faced with that is, do you also keep the shell around and continue to use it for events? It will actually be the same instance as the container returned by start.