-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[core-lro] Proposed LRO Engine design #15949
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
Merged
Merged
Changes from 7 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
d4cc28e
[core-lro] Proposed LRO Engine design
deyaaeldeen 1529790
address Jeff's feedback
deyaaeldeen 9667339
Merge remote-tracking branch 'upstream/main' into lro-design
deyaaeldeen d63c5f4
format
deyaaeldeen 0194cd9
use specific paths in imports
deyaaeldeen df1eaa9
adding tests
deyaaeldeen ea160a8
fix linting
deyaaeldeen e723ae6
edits
deyaaeldeen 0fceb82
address feedback
deyaaeldeen 74cb3fe
minor edits
deyaaeldeen 49e17f6
fix linting
deyaaeldeen 26368ea
Merge remote-tracking branch 'upstream/main' into lro-design
deyaaeldeen 8ac6a2d
fix build
deyaaeldeen 53e8d00
feedback
deyaaeldeen ab68252
format
deyaaeldeen 3fb9120
simplify design
deyaaeldeen 4653fd2
merge upstream/main
deyaaeldeen 6df8c9d
format
deyaaeldeen 324a2bf
address ffeedback
deyaaeldeen 5d52703
standardize the type for initializeState to match that of isDone
deyaaeldeen 828c351
update api view
deyaaeldeen 3fe1de0
edit
deyaaeldeen 82d693e
format
deyaaeldeen ed0f724
address feedback
deyaaeldeen 17feb5e
update api view
deyaaeldeen 9a689c9
update the design doc
deyaaeldeen 8e775b6
edit
deyaaeldeen 42a94b8
more simplifications
deyaaeldeen efa6cf2
format
deyaaeldeen bc3638e
edit
deyaaeldeen 5abc805
removing unnecessary deps
deyaaeldeen 8b8145e
revert change in unrelated tests
deyaaeldeen 2a36388
update the design doc to match recent simplifications in code gen
deyaaeldeen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| # Modular Support for Long-Running Operations | ||
|
|
||
| Long-running operations (LROs) are operations that the service _could_ take a long time to finish processing and they follow a common convention: | ||
|
|
||
| - the customer first send a an initiation request to the service, which in turn sends back a response, from which the customer can learn how to poll for the status of the operation, if it has not been completed already, | ||
| - using their learnings, the customer polls the status of the operation until it is done, | ||
| - again, using their learnings, the customer can now get the desired result of the operation once its status says it is done. | ||
|
|
||
| Ideally, we can write an algorithm that implements this convention once and use it in all Azure clients for LRO APIs, however, in reality, this convention is implemented differently across Azure services. The good news is that the TypeScript Autorest extension is AFAIK able to generate code that implements those different ones, but this implementation has a couple limitations: | ||
|
|
||
| 1. it is located in a few files that the code generator copies into every generated package that has LROs. So if in the future there is a bug fix needed in the LRO logic, the package has to be regenerated with the fix. | ||
| 2. it works only with clients that use `@azure/core-client`, so clients that use `@azure-rest/core-client` or `@azure/core-http` can not use this implementation as is. | ||
|
|
||
| In order to fix limitation #1, the most straightforward thing to do is to move those files into `@azure/core-lro`, but without fixing limitation #2 first, `@azure/core-lro` will have to depend on `@azure/core-client` in this case which will force clients that depend on `@azure/core-lro` but not necessarily depend on `@azure/core-client` to transitively depend on the latter, posing concerns about unnecessarily larger bundle sizes. | ||
|
|
||
| This document presents a design that fixes limitation #2 and naturally fixes limitation #1 too. | ||
|
sadasant marked this conversation as resolved.
|
||
|
|
||
| ## Things to know before reading | ||
|
|
||
| - Some details not related to the high-level concept are not illustrated; the scope of this is limited to the high level shape and paradigms for the feature area. | ||
|
|
||
| ## Terminology | ||
|
|
||
| - **Azure Async Operation**, **Body**, and **Location** are names for the LRO implementations currently supported in the TypeScript Autorest extension. They vary in how to calculate the path to poll from, the algorithm for detecting whether the operation has finished, and the location to retrieve the desired results from. Currently, these pieces of information can be calculated from the response received after sending the initiation request. | ||
|
|
||
| ## Why this is needed | ||
|
|
||
| The China team is currently waiting for fixing limitation #1 which they regard as a blocker for GAing the TypeScript Autorest extension. Furthermore, having this LRO implementation being part of `@azure/core-lro` and not tied to `@azure/core-client` will significantly help streamline the underway effort to add convenience helpers for LROs in `@azure-rest` clients. | ||
|
|
||
| ## Proposed design | ||
|
|
||
| This document presents a design of a LRO engine to be part of `@azure/core-lro` and could be used by any client regardless of how it is implemented. Furthermore, specific implementations of the engine are also provided to be auto-generated by Autorest. | ||
|
|
||
| The design consists of three main pieces: | ||
|
|
||
| - an interface, named `LongRunningOperation<T>` which groups various primitives need to implement LROs | ||
| - a class, named `LroEngine`, that implements the LRO engine and its constructor takes as input an object that implements `LongRunningOperation<T>` | ||
| - classes that implement `LongRunningOperation<T>` for `@azure/core-http` and `@azure/core-client`. @joheredi also created one for `@azure-rest/core-client` in https://github.com/Azure/azure-sdk-for-js/pull/15898 | ||
|
|
||
| ### `LongRunningOperation<T>` | ||
|
|
||
| This interface contains the following three methods: **sendInitialRequest**, **sendPollRequest**, and **retrieveAzureAsyncResource**. I propose to make this interface exported by `@azure/core-lro`. | ||
|
|
||
| #### `sendInitialRequest` | ||
|
|
||
| This method should be implemented to send the initial request to start the operation and it has the following signature: | ||
|
|
||
| ```ts | ||
| sendInitialRequest: (initializeState: (rawResponse: RawResponse, flatResponse: unknown) => boolean) => Promise<LroResponse<T>> | ||
| ``` | ||
|
|
||
| The `initializeState` parameter is a function that will initialize the state of the polling operation and will return a Boolean that indicates whether the operation has already been completed. This Boolean is especially useful in `@azure/core-client`'s engine because at this point, the customer-provided callback should be called. The implementation for `@azure/core-http` looks like this: | ||
|
|
||
| ```ts | ||
| public async sendInitialRequest( | ||
| initializeState: ( | ||
| rawResponse: RawResponse, | ||
| flatResponse: unknown | ||
| ) => boolean | ||
| ): Promise<LroResponse<T>> { | ||
| const response = await this.sendOperation(this.args, this.spec); // the class will have sendOperation, args, and spec as private fields | ||
| initializeState(response.rawResponse, response.flatResponse); | ||
| return response; | ||
| } | ||
| ``` | ||
|
|
||
| #### `sendPollRequest` | ||
|
|
||
| This method should be implemented to send a polling request, a request the service should respond to with the status of the operation, and it has the following signature: | ||
|
|
||
| ```ts | ||
| sendPollRequest: (config: LroConfig, path: string) => Promise<LroStatus<T>> | ||
| ``` | ||
|
|
||
| This method takes two things as input, it takes the request path as you would expect but also takes a parameter of type `LroConfig`. The latter is an object that holds meta information about how the Lro should work and clients could need access to this information in order to do things like doing custom de-serialization in certain cases where the swagger does not describe all response shapes. The `LroConfig` interface is defined as follows: | ||
|
|
||
| ```ts | ||
| export interface LroConfig { | ||
| mode?: LroMode; | ||
| resourceLocation?: string; | ||
| } | ||
| ``` | ||
|
|
||
| The information in it is currently populated from the response of the initial request. `mode` specifies which LRO implementation this particular API follows (see the [Terminology](#terminology) section). Furthermore, `resourceLocation` is a path where the desired result of the LRO could potentially be located. | ||
|
|
||
| Implementing `sendPollRequest` is tricky because it returns a `LroStatus` which has a `done` field and it is not easy to figure out whether the LRO has finished, so this design exposes a helper function, called `createGetLroStatusFromResponse`, which the implementation should call to get a `getLroStatusFromResponse`, which in turn should be called on the response to detect whether the operation has finished at this point. Here is what the implementation looks like for `@azure/core-http`, ignoring the custom de-serialization stuff: | ||
|
|
||
| ```ts | ||
| public async sendPollRequest( | ||
| config: LroConfig, | ||
| path: string | ||
| ): Promise<LroStatus<T>> { | ||
| const getLroStatusFromResponse = createGetLroStatusFromResponse(this, config, this.finalStateVia); | ||
| const { flatResponse, rawResponse } = await sendOperation(this.args, { | ||
| ...this.spec, | ||
| httpMethod: "GET", | ||
| path: path) | ||
| }); | ||
| return getLroStatusFromResponse(rawResponse, flatResponse as T); | ||
| } | ||
| ``` | ||
|
|
||
| #### `retrieveAzureAsyncResource` | ||
|
|
||
| This method is only needed to potentially retrieve the provisioned azure resource in the _Azure Async Operation_ mode after the operation has finished and it should be different from the `sendPollRequest` one in two things: | ||
|
|
||
| - should set the obscure `shouldDeserialize` option needed for doing custom de-serialization on the result if it is supported by the client | ||
| - we already know the operation has finished at this point, so it just return an object of type `LroResponse` instead of `LroStatus`. | ||
|
|
||
| It has the following signature: | ||
|
|
||
| ```ts | ||
| retrieveAzureAsyncResource: (path?: string) => Promise<LroResponse<T>> | ||
| ``` | ||
|
|
||
| Note that `path` is optional here because `resourceLocation` is also optional. Here is the implementation for `@azure/core-http`: | ||
|
|
||
| ```ts | ||
| public async retrieveAzureAsyncResource( | ||
| path?: string | ||
| ): Promise<LroStatus<T>> { | ||
| const updatedArgs = { ...this.args }; | ||
| if (updatedArgs.options) { | ||
| (updatedArgs.options as any).shouldDeserialize = true; | ||
| } | ||
| return sendOperation(updatedArgs, { | ||
| ...this.spec, | ||
| httpMethod: "GET", | ||
| ...(path && { path }) | ||
| }); | ||
| } | ||
| ``` | ||
|
|
||
| ### `LroEngine` | ||
|
|
||
| I propose to make this class also exported by `@azure/core-lro`. This class has the following type signature: | ||
|
|
||
| ```ts | ||
| class LroEngine<TResult, TState extends PollOperationState<TResult>> extends Poller<TState, TResult> | ||
| ``` | ||
|
|
||
| so a client can instantiate the state of the polling operation with a custom type that extends the standard `PollOperationState`. This flexibility can open the door for this class to be used in Track 2 packages where it is common for LROs to have other interesting information as part of their state and customers might want to have access to it, e.g. the last time the operation was updated. | ||
|
|
||
| The class also has the following constructor: | ||
|
|
||
| ```ts | ||
| constructor(lro: LongRunningOperation<TResult>, options?: LroEngineOptions, ); | ||
| ``` | ||
|
|
||
| Currently `options` has `intervalInMs` to control the polling interval and `resumeFrom` to enable resuming from a serialized state. If there are new arguments to be added to the class, they could be added to the options type. For instance, we might decide to enable customers to provide a custom LRO implementation in the form of a custom [`update`](https://github.com/Azure/azure-sdk-for-js/blob/2d2c6561cac330b8720763db88705fad4e867bda/sdk/core/core-lro/src/pollOperation.ts#L63) method, such a method could be part of the options. | ||
|
|
||
| ### `coreClientLro` and `coreHttpLro` | ||
|
|
||
| I propose to make these classes auto-generated by Autorest. These classes implement `LongRunningOperation<T>` and various bits of `coreHttpLro` have been showed earlier. These classes will need access to a few pieces: operation specification and operation arguments and a primitive function that can take them as input to send a request and also converts the response into one of type `LroResponse<T>` which has both the flattened and the raw responses. Furthermore, those classes can also, optionally, take as input an object of type `LroResourceLocationConfig`, which could determine where to find the results of the LRO after the operation finished. Typically, Autorest figures out the value for `LroResourceLocationConfig` from the `x-ms-long-running-operation-options` swagger extension. | ||
|
|
||
| ## Usage examples | ||
|
|
||
| ### Create an object of `coreClientLro` | ||
|
|
||
| ```ts | ||
| const directSendOperation = async ( | ||
| args: coreClient.OperationArguments, | ||
| spec: coreClient.OperationSpec | ||
| ): Promise<unknown> => { | ||
| return this.client.sendOperationRequest(args, spec); | ||
|
deyaaeldeen marked this conversation as resolved.
|
||
| }; | ||
| const sendOperation = async ( | ||
| args: coreClient.OperationArguments, | ||
| spec: coreClient.OperationSpec | ||
| ) => { | ||
| let currentRawResponse: coreClient.FullOperationResponse | undefined = undefined; | ||
| const providedCallback = args.options?.onResponse; | ||
| const callback: coreClient.RawResponseCallback = ( | ||
| rawResponse: coreClient.FullOperationResponse, | ||
| flatResponse: unknown | ||
| ) => { | ||
| currentRawResponse = rawResponse; | ||
| providedCallback?.(rawResponse, flatResponse); | ||
| }; | ||
| const updatedArgs = { | ||
| ...args, | ||
| options: { | ||
| ...args.options, | ||
| onResponse: callback | ||
| } | ||
| }; | ||
| const flatResponse = await directSendOperation(updatedArgs, spec); | ||
| return { | ||
| flatResponse, | ||
| rawResponse: { | ||
| statusCode: currentRawResponse!.status, | ||
| body: currentRawResponse!.parsedBody, | ||
| headers: currentRawResponse!.headers.toJSON() | ||
| } | ||
| }; | ||
| }; | ||
|
|
||
| const lro = new CoreClientLro( | ||
| sendOperation, | ||
| { options }, // arguments are just the operation options | ||
| spec | ||
| ); | ||
| ``` | ||
|
|
||
| ### Using `LroEngine` | ||
|
|
||
| ```ts | ||
| const pollerEngine = new LroEngine({ intervalInMs: 2000 }, lro); // lro was instantiated in the previous section | ||
| const result = pollerEngine.pollUntilDone(); | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.