Skip to content
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

[core-lro] Set isCancelled when operation status is cancelled #21893

Merged
merged 12 commits into from
May 18, 2022
431 changes: 287 additions & 144 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions sdk/core/core-lro/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
# Release History

## 2.3.0 (Unreleased)
## 2.3.0-beta.1 (2022-05-18)

### Features Added

- `lroEngine` now supports cancellation of the long-running operation.

### Breaking Changes

### Bugs Fixed

### Other Changes

- Removed the unused dependency `@azure/core-tracing`.
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/core-lro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@azure/core-lro",
"author": "Microsoft Corporation",
"sdk-type": "client",
"version": "2.3.0",
"version": "2.3.0-beta.1",
"description": "Isomorphic client library for supporting long-running operations in node.js and browser.",
"tags": [
"isomorphic",
Expand Down
50 changes: 21 additions & 29 deletions sdk/core/core-lro/src/lroEngine/bodyPolling.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import {
LroBody,
LroResponse,
LroStatus,
RawResponse,
failureStates,
successStates,
} from "./models";
import { isUnexpectedPollingResponse } from "./requestUtils";

function getProvisioningState(rawResponse: RawResponse): string {
const { properties, provisioningState } = (rawResponse.body as LroBody) ?? {};
const state: string | undefined = properties?.provisioningState ?? provisioningState;
return typeof state === "string" ? state.toLowerCase() : "succeeded";
}

export function isBodyPollingDone(rawResponse: RawResponse): boolean {
const state = getProvisioningState(rawResponse);
if (isUnexpectedPollingResponse(rawResponse) || failureStates.includes(state)) {
throw new Error(`The long running operation has failed. The provisioning state: ${state}.`);
}
return successStates.includes(state);
}
import { LroResponse, LroStatus } from "./models";
import { getProvisioningState, isCanceled, isPollingDone } from "./requestUtils";
import { PollOperationState } from "../pollOperation";

/**
* Creates a polling strategy based on BodyPolling which uses the provisioning state
* from the result to determine the current operation state
*/
export function processBodyPollingOperationResult<TResult>(
response: LroResponse<TResult>
): LroStatus<TResult> {
return {
...response,
done: isBodyPollingDone(response.rawResponse),
export function processBodyPollingOperationResult<
TResult,
TState extends PollOperationState<TResult>
>(state: TState): (response: LroResponse<TResult>) => LroStatus<TResult> {
return (response: LroResponse<TResult>): LroStatus<TResult> => {
const status = getProvisioningState(response.rawResponse);
return {
...response,
done:
isCanceled({
state,
status,
}) ||
isPollingDone({
rawResponse: response.rawResponse,
status,
}),
};
};
}
36 changes: 23 additions & 13 deletions sdk/core/core-lro/src/lroEngine/locationPolling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,20 @@ import {
LroResponse,
LroStatus,
RawResponse,
failureStates,
successStates,
} from "./models";
import { isUnexpectedPollingResponse } from "./requestUtils";
import { isCanceled, isPollingDone } from "./requestUtils";
import { PollOperationState } from "../pollOperation";

function isPollingDone(rawResponse: RawResponse): boolean {
if (isUnexpectedPollingResponse(rawResponse) || rawResponse.statusCode === 202) {
return false;
}
function getStatus(rawResponse: RawResponse): string {
const { status } = (rawResponse.body as LroBody) ?? {};
const state = typeof status === "string" ? status.toLowerCase() : "succeeded";
if (isUnexpectedPollingResponse(rawResponse) || failureStates.includes(state)) {
throw new Error(`The long running operation has failed. The provisioning state: ${state}.`);
return typeof status === "string" ? status.toLowerCase() : "succeeded";
}

function isLocationPollingDone(rawResponse: RawResponse, status: string): boolean {
if (rawResponse.statusCode === 202) {
return false;
}
return successStates.includes(state);
return isPollingDone({ rawResponse, status });
}

/**
Expand All @@ -44,13 +43,24 @@ async function sendFinalRequest<TResult>(
}
}

export function processLocationPollingOperationResult<TResult>(
export function processLocationPollingOperationResult<
TResult,
TState extends PollOperationState<TResult>
>(
lro: LongRunningOperation<TResult>,
state: TState,
resourceLocation?: string,
lroResourceLocationConfig?: LroResourceLocationConfig
): (response: LroResponse<TResult>) => LroStatus<TResult> {
return (response: LroResponse<TResult>): LroStatus<TResult> => {
if (isPollingDone(response.rawResponse)) {
const status = getStatus(response.rawResponse);
if (
isCanceled({
state,
status,
}) ||
isLocationPollingDone(response.rawResponse, status)
) {
if (resourceLocation === undefined) {
return { ...response, done: true };
} else {
Expand Down
10 changes: 2 additions & 8 deletions sdk/core/core-lro/src/lroEngine/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,12 @@ export interface LroEngineOptions<TResult, TState> {
isDone?: (lastResponse: unknown, state: TState) => boolean;

/**
* A function to cancel the LRO.
* A function that takes the mutable state as input and attempts to cancel the
* LRO.
*/
cancel?: (state: TState) => Promise<void>;
}

export const successStates = ["succeeded"];
export const failureStates = ["failed", "canceled", "cancelled"];
/**
* The LRO states that signal that the LRO has completed.
*/
export const terminalStates = successStates.concat(failureStates);

/**
* The potential location of the result of the LRO if specified by the LRO extension in the swagger.
*/
Expand Down
14 changes: 12 additions & 2 deletions sdk/core/core-lro/src/lroEngine/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ export class GenericPollOperation<TResult, TState extends PollOperationState<TRe
...response,
done: isDone(response.flatResponse, this.state),
})
: createGetLroStatusFromResponse(this.lro, state.config, this.lroResourceLocationConfig);
: createGetLroStatusFromResponse(
this.lro,
state.config,
this.state,
this.lroResourceLocationConfig
);
this.poll = createPoll(this.lro);
}
if (!state.pollingURL) {
Expand Down Expand Up @@ -122,8 +127,13 @@ export class GenericPollOperation<TResult, TState extends PollOperationState<TRe
}

async cancel(): Promise<PollOperation<TState, TResult>> {
this.state.isCancelled = true;
await this.cancelOp?.(this.state);
/**
* When `cancelOperation` is called, polling stops so it is important that
* `isCancelled` is set now because the polling logic will not be able to
* set it itself because it will not fire.
*/
this.state.isCancelled = true;
return this;
}

Expand Down
33 changes: 32 additions & 1 deletion sdk/core/core-lro/src/lroEngine/requestUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { LroConfig, RawResponse } from "./models";
import { LroBody, LroConfig, RawResponse } from "./models";
import { PollOperationState } from "../pollOperation";

/**
* Detects where the continuation token is and returns it. Notice that azure-asyncoperation
Expand Down Expand Up @@ -106,3 +107,33 @@ export function isUnexpectedPollingResponse(rawResponse: RawResponse): boolean {
}
return false;
}

export function isCanceled<TResult, TState extends PollOperationState<TResult>>(operation: {
state: TState;
status: string;
}): boolean {
const { state, status } = operation;
if (["canceled", "cancelled"].includes(status)) {
state.isCancelled = true;
throw new Error(`The long-running operation has been canceled.`);
}
return false;
}

export function isSucceededStatus(status: string): boolean {
return status === "succeeded";
}

export function isPollingDone(result: { rawResponse: RawResponse; status: string }): boolean {
const { rawResponse, status } = result;
if (isUnexpectedPollingResponse(rawResponse) || status === "failed") {
throw new Error(`The long-running operation has failed.`);
}
return isSucceededStatus(status);
}

export function getProvisioningState(rawResponse: RawResponse): string {
const { properties, provisioningState } = (rawResponse.body as LroBody) ?? {};
const state: string | undefined = properties?.provisioningState ?? provisioningState;
return typeof state === "string" ? state.toLowerCase() : "succeeded";
}
23 changes: 18 additions & 5 deletions sdk/core/core-lro/src/lroEngine/stateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,39 @@ import {
PollerConfig,
ResumablePollOperationState,
} from "./models";
import { getPollingUrl, inferLroMode, isUnexpectedInitialResponse } from "./requestUtils";
import { isBodyPollingDone, processBodyPollingOperationResult } from "./bodyPolling";
import {
getPollingUrl,
getProvisioningState,
inferLroMode,
isPollingDone,
isUnexpectedInitialResponse,
} from "./requestUtils";
import { PollOperationState } from "../pollOperation";
import { logger } from "./logger";
import { processBodyPollingOperationResult } from "./bodyPolling";
import { processLocationPollingOperationResult } from "./locationPolling";
import { processPassthroughOperationResult } from "./passthrough";

/**
* creates a stepping function that maps an LRO state to another.
*/
export function createGetLroStatusFromResponse<TResult>(
export function createGetLroStatusFromResponse<TResult, TState extends PollOperationState<TResult>>(
lroPrimitives: LongRunningOperation<TResult>,
config: LroConfig,
state: TState,
lroResourceLocationConfig?: LroResourceLocationConfig
): GetLroStatusFromResponse<TResult> {
switch (config.mode) {
case "Location": {
return processLocationPollingOperationResult(
lroPrimitives,
state,
config.resourceLocation,
lroResourceLocationConfig
);
}
case "Body": {
return processBodyPollingOperationResult;
return processBodyPollingOperationResult(state);
}
default: {
return processPassthroughOperationResult;
Expand Down Expand Up @@ -103,7 +112,11 @@ export function createInitializeState<TResult>(
/** short circuit polling if body polling is done in the initial request */
if (
state.config.mode === undefined ||
(state.config.mode === "Body" && isBodyPollingDone(state.initialRawResponse))
(state.config.mode === "Body" &&
isPollingDone({
rawResponse: state.initialRawResponse,
status: getProvisioningState(state.initialRawResponse),
}))
) {
state.result = response.flatResponse as TResult;
state.isCompleted = true;
Expand Down
Loading