Skip to content

Commit 51f5be7

Browse files
[core-lro] Set isCancelled when operation status is cancelled (Azure#21893)
* [core-lro] Set isCancelled when status is cancelled * don't check for isCanceled in TA test * fix lint * address feedback and handle cancellation uniformly * address feedback * add tests * edit * revert behavioral change * Update sdk/textanalytics/ai-text-analytics/package.json Co-authored-by: Will Temple <witemple@microsoft.com>
1 parent 4c5b90b commit 51f5be7

File tree

12 files changed

+503
-270
lines changed

12 files changed

+503
-270
lines changed

common/config/rush/pnpm-lock.yaml

Lines changed: 287 additions & 144 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/core/core-lro/CHANGELOG.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
# Release History
22

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

55
### Features Added
66

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

9-
### Breaking Changes
10-
11-
### Bugs Fixed
12-
139
### Other Changes
1410

1511
- Removed the unused dependency `@azure/core-tracing`.

sdk/core/core-lro/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@azure/core-lro",
33
"author": "Microsoft Corporation",
44
"sdk-type": "client",
5-
"version": "2.3.0",
5+
"version": "2.3.0-beta.1",
66
"description": "Isomorphic client library for supporting long-running operations in node.js and browser.",
77
"tags": [
88
"isomorphic",
Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,31 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import {
5-
LroBody,
6-
LroResponse,
7-
LroStatus,
8-
RawResponse,
9-
failureStates,
10-
successStates,
11-
} from "./models";
12-
import { isUnexpectedPollingResponse } from "./requestUtils";
13-
14-
function getProvisioningState(rawResponse: RawResponse): string {
15-
const { properties, provisioningState } = (rawResponse.body as LroBody) ?? {};
16-
const state: string | undefined = properties?.provisioningState ?? provisioningState;
17-
return typeof state === "string" ? state.toLowerCase() : "succeeded";
18-
}
19-
20-
export function isBodyPollingDone(rawResponse: RawResponse): boolean {
21-
const state = getProvisioningState(rawResponse);
22-
if (isUnexpectedPollingResponse(rawResponse) || failureStates.includes(state)) {
23-
throw new Error(`The long running operation has failed. The provisioning state: ${state}.`);
24-
}
25-
return successStates.includes(state);
26-
}
4+
import { LroResponse, LroStatus } from "./models";
5+
import { getProvisioningState, isCanceled, isPollingDone } from "./requestUtils";
6+
import { PollOperationState } from "../pollOperation";
277

288
/**
299
* Creates a polling strategy based on BodyPolling which uses the provisioning state
3010
* from the result to determine the current operation state
3111
*/
32-
export function processBodyPollingOperationResult<TResult>(
33-
response: LroResponse<TResult>
34-
): LroStatus<TResult> {
35-
return {
36-
...response,
37-
done: isBodyPollingDone(response.rawResponse),
12+
export function processBodyPollingOperationResult<
13+
TResult,
14+
TState extends PollOperationState<TResult>
15+
>(state: TState): (response: LroResponse<TResult>) => LroStatus<TResult> {
16+
return (response: LroResponse<TResult>): LroStatus<TResult> => {
17+
const status = getProvisioningState(response.rawResponse);
18+
return {
19+
...response,
20+
done:
21+
isCanceled({
22+
state,
23+
status,
24+
}) ||
25+
isPollingDone({
26+
rawResponse: response.rawResponse,
27+
status,
28+
}),
29+
};
3830
};
3931
}

sdk/core/core-lro/src/lroEngine/locationPolling.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,20 @@ import {
88
LroResponse,
99
LroStatus,
1010
RawResponse,
11-
failureStates,
12-
successStates,
1311
} from "./models";
14-
import { isUnexpectedPollingResponse } from "./requestUtils";
12+
import { isCanceled, isPollingDone } from "./requestUtils";
13+
import { PollOperationState } from "../pollOperation";
1514

16-
function isPollingDone(rawResponse: RawResponse): boolean {
17-
if (isUnexpectedPollingResponse(rawResponse) || rawResponse.statusCode === 202) {
18-
return false;
19-
}
15+
function getStatus(rawResponse: RawResponse): string {
2016
const { status } = (rawResponse.body as LroBody) ?? {};
21-
const state = typeof status === "string" ? status.toLowerCase() : "succeeded";
22-
if (isUnexpectedPollingResponse(rawResponse) || failureStates.includes(state)) {
23-
throw new Error(`The long running operation has failed. The provisioning state: ${state}.`);
17+
return typeof status === "string" ? status.toLowerCase() : "succeeded";
18+
}
19+
20+
function isLocationPollingDone(rawResponse: RawResponse, status: string): boolean {
21+
if (rawResponse.statusCode === 202) {
22+
return false;
2423
}
25-
return successStates.includes(state);
24+
return isPollingDone({ rawResponse, status });
2625
}
2726

2827
/**
@@ -44,13 +43,24 @@ async function sendFinalRequest<TResult>(
4443
}
4544
}
4645

47-
export function processLocationPollingOperationResult<TResult>(
46+
export function processLocationPollingOperationResult<
47+
TResult,
48+
TState extends PollOperationState<TResult>
49+
>(
4850
lro: LongRunningOperation<TResult>,
51+
state: TState,
4952
resourceLocation?: string,
5053
lroResourceLocationConfig?: LroResourceLocationConfig
5154
): (response: LroResponse<TResult>) => LroStatus<TResult> {
5255
return (response: LroResponse<TResult>): LroStatus<TResult> => {
53-
if (isPollingDone(response.rawResponse)) {
56+
const status = getStatus(response.rawResponse);
57+
if (
58+
isCanceled({
59+
state,
60+
status,
61+
}) ||
62+
isLocationPollingDone(response.rawResponse, status)
63+
) {
5464
if (resourceLocation === undefined) {
5565
return { ...response, done: true };
5666
} else {

sdk/core/core-lro/src/lroEngine/models.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,12 @@ export interface LroEngineOptions<TResult, TState> {
3333
isDone?: (lastResponse: unknown, state: TState) => boolean;
3434

3535
/**
36-
* A function to cancel the LRO.
36+
* A function that takes the mutable state as input and attempts to cancel the
37+
* LRO.
3738
*/
3839
cancel?: (state: TState) => Promise<void>;
3940
}
4041

41-
export const successStates = ["succeeded"];
42-
export const failureStates = ["failed", "canceled", "cancelled"];
43-
/**
44-
* The LRO states that signal that the LRO has completed.
45-
*/
46-
export const terminalStates = successStates.concat(failureStates);
47-
4842
/**
4943
* The potential location of the result of the LRO if specified by the LRO extension in the swagger.
5044
*/

sdk/core/core-lro/src/lroEngine/operation.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ export class GenericPollOperation<TResult, TState extends PollOperationState<TRe
8686
...response,
8787
done: isDone(response.flatResponse, this.state),
8888
})
89-
: createGetLroStatusFromResponse(this.lro, state.config, this.lroResourceLocationConfig);
89+
: createGetLroStatusFromResponse(
90+
this.lro,
91+
state.config,
92+
this.state,
93+
this.lroResourceLocationConfig
94+
);
9095
this.poll = createPoll(this.lro);
9196
}
9297
if (!state.pollingURL) {
@@ -122,8 +127,13 @@ export class GenericPollOperation<TResult, TState extends PollOperationState<TRe
122127
}
123128

124129
async cancel(): Promise<PollOperation<TState, TResult>> {
125-
this.state.isCancelled = true;
126130
await this.cancelOp?.(this.state);
131+
/**
132+
* When `cancelOperation` is called, polling stops so it is important that
133+
* `isCancelled` is set now because the polling logic will not be able to
134+
* set it itself because it will not fire.
135+
*/
136+
this.state.isCancelled = true;
127137
return this;
128138
}
129139

sdk/core/core-lro/src/lroEngine/requestUtils.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { LroConfig, RawResponse } from "./models";
4+
import { LroBody, LroConfig, RawResponse } from "./models";
5+
import { PollOperationState } from "../pollOperation";
56

67
/**
78
* Detects where the continuation token is and returns it. Notice that azure-asyncoperation
@@ -106,3 +107,33 @@ export function isUnexpectedPollingResponse(rawResponse: RawResponse): boolean {
106107
}
107108
return false;
108109
}
110+
111+
export function isCanceled<TResult, TState extends PollOperationState<TResult>>(operation: {
112+
state: TState;
113+
status: string;
114+
}): boolean {
115+
const { state, status } = operation;
116+
if (["canceled", "cancelled"].includes(status)) {
117+
state.isCancelled = true;
118+
throw new Error(`The long-running operation has been canceled.`);
119+
}
120+
return false;
121+
}
122+
123+
export function isSucceededStatus(status: string): boolean {
124+
return status === "succeeded";
125+
}
126+
127+
export function isPollingDone(result: { rawResponse: RawResponse; status: string }): boolean {
128+
const { rawResponse, status } = result;
129+
if (isUnexpectedPollingResponse(rawResponse) || status === "failed") {
130+
throw new Error(`The long-running operation has failed.`);
131+
}
132+
return isSucceededStatus(status);
133+
}
134+
135+
export function getProvisioningState(rawResponse: RawResponse): string {
136+
const { properties, provisioningState } = (rawResponse.body as LroBody) ?? {};
137+
const state: string | undefined = properties?.provisioningState ?? provisioningState;
138+
return typeof state === "string" ? state.toLowerCase() : "succeeded";
139+
}

sdk/core/core-lro/src/lroEngine/stateMachine.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,39 @@ import {
1111
PollerConfig,
1212
ResumablePollOperationState,
1313
} from "./models";
14-
import { getPollingUrl, inferLroMode, isUnexpectedInitialResponse } from "./requestUtils";
15-
import { isBodyPollingDone, processBodyPollingOperationResult } from "./bodyPolling";
14+
import {
15+
getPollingUrl,
16+
getProvisioningState,
17+
inferLroMode,
18+
isPollingDone,
19+
isUnexpectedInitialResponse,
20+
} from "./requestUtils";
21+
import { PollOperationState } from "../pollOperation";
1622
import { logger } from "./logger";
23+
import { processBodyPollingOperationResult } from "./bodyPolling";
1724
import { processLocationPollingOperationResult } from "./locationPolling";
1825
import { processPassthroughOperationResult } from "./passthrough";
1926

2027
/**
2128
* creates a stepping function that maps an LRO state to another.
2229
*/
23-
export function createGetLroStatusFromResponse<TResult>(
30+
export function createGetLroStatusFromResponse<TResult, TState extends PollOperationState<TResult>>(
2431
lroPrimitives: LongRunningOperation<TResult>,
2532
config: LroConfig,
33+
state: TState,
2634
lroResourceLocationConfig?: LroResourceLocationConfig
2735
): GetLroStatusFromResponse<TResult> {
2836
switch (config.mode) {
2937
case "Location": {
3038
return processLocationPollingOperationResult(
3139
lroPrimitives,
40+
state,
3241
config.resourceLocation,
3342
lroResourceLocationConfig
3443
);
3544
}
3645
case "Body": {
37-
return processBodyPollingOperationResult;
46+
return processBodyPollingOperationResult(state);
3847
}
3948
default: {
4049
return processPassthroughOperationResult;
@@ -103,7 +112,11 @@ export function createInitializeState<TResult>(
103112
/** short circuit polling if body polling is done in the initial request */
104113
if (
105114
state.config.mode === undefined ||
106-
(state.config.mode === "Body" && isBodyPollingDone(state.initialRawResponse))
115+
(state.config.mode === "Body" &&
116+
isPollingDone({
117+
rawResponse: state.initialRawResponse,
118+
status: getProvisioningState(state.initialRawResponse),
119+
}))
107120
) {
108121
state.result = response.flatResponse as TResult;
109122
state.isCompleted = true;

0 commit comments

Comments
 (0)