diff --git a/sdk/core/core-lro/src/lroEngine/bodyPolling.ts b/sdk/core/core-lro/src/lroEngine/bodyPolling.ts deleted file mode 100644 index cf965fadcc7f..000000000000 --- a/sdk/core/core-lro/src/lroEngine/bodyPolling.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -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, - TState extends PollOperationState ->(state: TState): (response: LroResponse) => LroStatus { - return (response: LroResponse): LroStatus => { - const status = getProvisioningState(response.rawResponse); - return { - ...response, - done: - isCanceled({ - state, - status, - }) || - isPollingDone({ - rawResponse: response.rawResponse, - status, - }), - }; - }; -} diff --git a/sdk/core/core-lro/src/lroEngine/impl.ts b/sdk/core/core-lro/src/lroEngine/impl.ts new file mode 100644 index 000000000000..128df110bf5b --- /dev/null +++ b/sdk/core/core-lro/src/lroEngine/impl.ts @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + GetLroStatusFromResponse, + LongRunningOperation, + LroBody, + LroInfo, + LroResourceLocationConfig, + LroResponse, + LroStatus, + PollerConfig, + RawResponse, + ResumablePollOperationState, +} from "./models"; +import { PollOperationState } from "../pollOperation"; +import { logger } from "./logger"; + +export function throwIfUndefined( + input: T | undefined, + options: { errorMessage?: string } = {} +): T { + if (input === undefined) { + throw new Error(options.errorMessage ?? "undefined variable"); + } + return input; +} + +export function updatePollingUrl(inputs: { rawResponse: RawResponse; info: LroInfo }): void { + const { info, rawResponse } = inputs; + switch (info.mode) { + case "OperationLocation": { + const operationLocation = getOperationLocation(rawResponse); + const azureAsyncOperation = getAzureAsyncOperation(rawResponse); + info.pollingUrl = + getOperationLocationPollingUrl({ operationLocation, azureAsyncOperation }) ?? + throwIfUndefined(info.pollingUrl); + break; + } + case "ResourceLocation": { + info.pollingUrl = getLocation(rawResponse) ?? throwIfUndefined(info.pollingUrl); + break; + } + } +} + +function getOperationLocationPollingUrl(inputs: { + operationLocation?: string; + azureAsyncOperation?: string; +}): string | undefined { + const { azureAsyncOperation, operationLocation } = inputs; + return operationLocation ?? azureAsyncOperation; +} + +function getLocation(rawResponse: RawResponse): string | undefined { + return rawResponse.headers["location"]; +} + +function getOperationLocation(rawResponse: RawResponse): string | undefined { + return rawResponse.headers["operation-location"]; +} + +function getAzureAsyncOperation(rawResponse: RawResponse): string | undefined { + return rawResponse.headers["azure-asyncoperation"]; +} + +function findResourceLocation(inputs: { + requestMethod: string; + location?: string; + requestPath: string; + lroResourceLocationConfig?: LroResourceLocationConfig; +}): string | undefined { + const { location, requestMethod, requestPath, lroResourceLocationConfig } = inputs; + switch (requestMethod) { + case "PUT": { + return requestPath; + } + case "DELETE": { + return undefined; + } + default: { + switch (lroResourceLocationConfig) { + case "azure-async-operation": { + return undefined; + } + case "original-uri": { + return requestPath; + } + case "location": + default: { + return location; + } + } + } + } +} + +function inferLroMode(inputs: { + rawResponse: RawResponse; + requestPath: string; + requestMethod: string; + lroResourceLocationConfig?: LroResourceLocationConfig; +}): LroInfo { + const { rawResponse, requestMethod, requestPath, lroResourceLocationConfig } = inputs; + const operationLocation = getOperationLocation(rawResponse); + const azureAsyncOperation = getAzureAsyncOperation(rawResponse); + const location = getLocation(rawResponse); + if (operationLocation !== undefined || azureAsyncOperation !== undefined) { + return { + mode: "OperationLocation", + pollingUrl: operationLocation ?? azureAsyncOperation, + resourceLocation: findResourceLocation({ + requestMethod, + location, + requestPath, + lroResourceLocationConfig, + }), + }; + } else if (location !== undefined) { + return { + mode: "ResourceLocation", + pollingUrl: location, + }; + } else if (requestMethod === "PUT") { + return { + mode: "Body", + pollingUrl: requestPath, + }; + } else { + return { + mode: "None", + }; + } +} + +class SimpleRestError extends Error { + public statusCode?: number; + constructor(message: string, statusCode: number) { + super(message); + this.name = "RestError"; + this.statusCode = statusCode; + + Object.setPrototypeOf(this, SimpleRestError.prototype); + } +} + +function throwIfError(rawResponse: RawResponse): void { + const code = rawResponse.statusCode; + if (code >= 400) { + throw new SimpleRestError( + `Received unexpected HTTP status code ${code} while polling. This may indicate a server issue.`, + code + ); + } +} + +function getStatus(rawResponse: RawResponse): string { + const { status } = (rawResponse.body as LroBody) ?? {}; + return typeof status === "string" ? status.toLowerCase() : "succeeded"; +} + +function getProvisioningState(rawResponse: RawResponse): string { + const { properties, provisioningState } = (rawResponse.body as LroBody) ?? {}; + const state = properties?.provisioningState ?? provisioningState; + return typeof state === "string" ? state.toLowerCase() : "succeeded"; +} + +function isCanceled>(operation: { + state: TState; + status: string; +}): boolean { + const { state, status } = operation; + if (["canceled", "cancelled"].includes(status)) { + state.isCancelled = true; + return true; + } + return false; +} + +function isTerminal>(operation: { + state: TState; + status: string; +}): boolean { + const { state, status } = operation; + if (status === "failed") { + throw new Error(`The long-running operation has failed.`); + } + return status === "succeeded" || isCanceled({ state, status }); +} + +function isDone>(result: { + rawResponse: RawResponse; + state: TState; + info: LroInfo; + responseKind?: "Initial" | "Polling"; +}): boolean { + const { rawResponse, state, info, responseKind = "Polling" } = result; + throwIfError(rawResponse); + switch (info.mode) { + case "OperationLocation": { + return responseKind === "Polling" && isTerminal({ state, status: getStatus(rawResponse) }); + } + case "Body": { + return isTerminal({ state, status: getProvisioningState(rawResponse) }); + } + case "ResourceLocation": { + return responseKind === "Polling" && rawResponse.statusCode !== 202; + } + case "None": { + return true; + } + } +} + +/** + * Creates a polling operation. + */ +export function createPoll( + lroPrimitives: LongRunningOperation +): ( + pollingURL: string, + pollerConfig: PollerConfig, + getLroStatusFromResponse: GetLroStatusFromResponse +) => Promise> { + return async ( + path: string, + pollerConfig: PollerConfig, + getLroStatusFromResponse: GetLroStatusFromResponse + ): Promise> => { + const response = await lroPrimitives.sendPollRequest(path); + const retryAfter: string | undefined = response.rawResponse.headers["retry-after"]; + if (retryAfter !== undefined) { + // Retry-After header value is either in HTTP date format, or in seconds + const retryAfterInSeconds = parseInt(retryAfter); + pollerConfig.intervalInMs = isNaN(retryAfterInSeconds) + ? calculatePollingIntervalFromDate(new Date(retryAfter), pollerConfig.intervalInMs) + : retryAfterInSeconds * 1000; + } + return getLroStatusFromResponse(response); + }; +} + +function calculatePollingIntervalFromDate( + retryAfterDate: Date, + defaultIntervalInMs: number +): number { + const timeNow = Math.floor(new Date().getTime()); + const retryAfterTime = retryAfterDate.getTime(); + if (timeNow < retryAfterTime) { + return retryAfterTime - timeNow; + } + return defaultIntervalInMs; +} + +export function buildResult>(inputs: { + response: TResult; + state: TState; + processResult?: (result: unknown, state: TState) => TResult; +}): TResult { + const { processResult, response, state } = inputs; + return processResult ? processResult(response, state) : response; +} + +/** + * Creates a callback to be used to initialize the polling operation state. + */ +export function createStateInitializer< + TResult, + TState extends PollOperationState +>(inputs: { + state: ResumablePollOperationState; + requestPath: string; + requestMethod: string; + lroResourceLocationConfig?: LroResourceLocationConfig; + processResult?: (result: unknown, state: TState) => TResult; +}): (response: LroResponse) => void { + const { requestMethod, requestPath, state, lroResourceLocationConfig, processResult } = inputs; + return (response: LroResponse): void => { + state.initialRawResponse = response.rawResponse; + state.isStarted = true; + state.config = inferLroMode({ + rawResponse: state.initialRawResponse, + requestPath, + requestMethod, + lroResourceLocationConfig, + }); + /** short circuit before polling */ + if ( + isDone({ + rawResponse: state.initialRawResponse, + state, + info: state.config, + responseKind: "Initial", + }) + ) { + state.result = buildResult({ + response: response.flatResponse, + state: state as TState, + processResult, + }); + state.isCompleted = true; + } + logger.verbose(`LRO: initial state: ${JSON.stringify(state)}`); + }; +} + +export function createGetLroStatusFromResponse< + TResult, + TState extends PollOperationState +>(inputs: { + lro: LongRunningOperation; + state: TState; + info: LroInfo; +}): (response: LroResponse) => LroStatus { + const { lro, state, info } = inputs; + const location = info.resourceLocation; + return (response: LroResponse): LroStatus => { + const isTerminalStatus = isDone({ + info, + rawResponse: response.rawResponse, + state, + }); + return { + ...response, + done: isTerminalStatus && !location, + next: !(isTerminalStatus && location) + ? undefined + : () => + lro.sendPollRequest(location).then((res) => ({ + ...res, + done: true, + })), + }; + }; +} diff --git a/sdk/core/core-lro/src/lroEngine/locationPolling.ts b/sdk/core/core-lro/src/lroEngine/locationPolling.ts deleted file mode 100644 index 78a2ede42452..000000000000 --- a/sdk/core/core-lro/src/lroEngine/locationPolling.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { - LongRunningOperation, - LroBody, - LroResourceLocationConfig, - LroResponse, - LroStatus, - RawResponse, -} from "./models"; -import { isCanceled, isPollingDone } from "./requestUtils"; -import { PollOperationState } from "../pollOperation"; - -function getStatus(rawResponse: RawResponse): string { - const { status } = (rawResponse.body as LroBody) ?? {}; - return typeof status === "string" ? status.toLowerCase() : "succeeded"; -} - -function isLocationPollingDone(rawResponse: RawResponse, status: string): boolean { - if (rawResponse.statusCode === 202) { - return false; - } - return isPollingDone({ rawResponse, status }); -} - -/** - * Sends a request to the URI of the provisioned resource if needed. - */ -async function sendFinalRequest( - lro: LongRunningOperation, - resourceLocation: string, - lroResourceLocationConfig?: LroResourceLocationConfig -): Promise | undefined> { - switch (lroResourceLocationConfig) { - case "original-uri": - return lro.sendPollRequest(lro.requestPath); - case "azure-async-operation": - return undefined; - case "location": - default: - return lro.sendPollRequest(resourceLocation ?? lro.requestPath); - } -} - -export function processLocationPollingOperationResult< - TResult, - TState extends PollOperationState ->( - lro: LongRunningOperation, - state: TState, - resourceLocation?: string, - lroResourceLocationConfig?: LroResourceLocationConfig -): (response: LroResponse) => LroStatus { - return (response: LroResponse): LroStatus => { - const status = getStatus(response.rawResponse); - if ( - isCanceled({ - state, - status, - }) || - isLocationPollingDone(response.rawResponse, status) - ) { - if (resourceLocation === undefined) { - return { ...response, done: true }; - } else { - return { - ...response, - done: false, - next: async () => { - const finalResponse = await sendFinalRequest( - lro, - resourceLocation, - lroResourceLocationConfig - ); - return { - ...(finalResponse ?? response), - done: true, - }; - }, - }; - } - } - return { - ...response, - done: false, - }; - }; -} diff --git a/sdk/core/core-lro/src/lroEngine/models.ts b/sdk/core/core-lro/src/lroEngine/models.ts index 9361fabbbe7c..6770ffa315eb 100644 --- a/sdk/core/core-lro/src/lroEngine/models.ts +++ b/sdk/core/core-lro/src/lroEngine/models.ts @@ -81,25 +81,24 @@ export interface LroResponse { rawResponse: RawResponse; } -/** The type of which LRO implementation being followed by a specific API. */ -export type LroMode = "Location" | "Body"; - -/** - * The configuration of a LRO to determine how to perform polling and checking whether the operation has completed. - */ -export interface LroConfig { - /** The LRO mode */ - mode?: LroMode; - /** The path of a provisioned resource */ +export interface LroInfo { + /** The polling URL */ + pollingUrl?: string; + /** The resource location URL */ resourceLocation?: string; + /** The LRO mode */ + mode: "OperationLocation" | "ResourceLocation" | "Body" | "None"; } /** * Type of a polling operation state that can actually be resumed. */ export type ResumablePollOperationState = PollOperationState & { + /** The response received when initiating the LRO */ initialRawResponse?: RawResponse; - config?: LroConfig; + /** The LRO configuration */ + config?: LroInfo; + /** @deprecated use state.config.pollingUrl instead */ pollingURL?: string; }; @@ -110,7 +109,7 @@ export interface PollerConfig { /** * The type of a terminal state of an LRO. */ -export interface LroTerminalState extends LroResponse { +interface LroTerminalState extends LroResponse { /** * Whether the operation has finished. */ @@ -120,7 +119,7 @@ export interface LroTerminalState extends LroResponse { /** * The type of an in-progress state of an LRO. */ -export interface LroInProgressState extends LroResponse { +interface LroInProgressState extends LroResponse { /** * Whether the operation has finished. */ diff --git a/sdk/core/core-lro/src/lroEngine/operation.ts b/sdk/core/core-lro/src/lroEngine/operation.ts index 9d0bff77aa11..09fe333fefd8 100644 --- a/sdk/core/core-lro/src/lroEngine/operation.ts +++ b/sdk/core/core-lro/src/lroEngine/operation.ts @@ -12,9 +12,15 @@ import { ResumablePollOperationState, } from "./models"; import { PollOperation, PollOperationState } from "../pollOperation"; -import { createGetLroStatusFromResponse, createInitializeState, createPoll } from "./stateMachine"; +import { + buildResult, + createGetLroStatusFromResponse, + createPoll, + createStateInitializer, + throwIfUndefined, + updatePollingUrl, +} from "./impl"; import { AbortSignalLike } from "@azure/abort-controller"; -import { getPollingUrl } from "./requestUtils"; import { logger } from "./logger"; export class GenericPollOperation> @@ -64,55 +70,59 @@ export class GenericPollOperation | undefined = undefined; if (!state.isStarted) { - const initializeState = createInitializeState( + const initializeState = createStateInitializer({ state, - this.lro.requestPath, - this.lro.requestMethod - ); + requestPath: this.lro.requestPath, + requestMethod: this.lro.requestMethod, + lroResourceLocationConfig: this.lroResourceLocationConfig, + processResult: this.processResult, + }); lastResponse = await this.lro.sendInitialRequest(); initializeState(lastResponse); } if (!state.isCompleted) { - if (!this.poll || !this.getLroStatusFromResponse) { - if (!state.config) { - throw new Error( - "Bad state: LRO mode is undefined. Please check if the serialized state is well-formed." - ); - } + const config = throwIfUndefined(state.config, { + errorMessage: + "Bad state: LRO mode is undefined. Check if the serialized state is well-formed.", + }); + if (!this.poll) { + this.poll = createPoll(this.lro); + } + if (!this.getLroStatusFromResponse) { const isDone = this.isDone; this.getLroStatusFromResponse = isDone ? (response: LroResponse) => ({ ...response, done: isDone(response.flatResponse, this.state), }) - : createGetLroStatusFromResponse( - this.lro, - state.config, - this.state, - this.lroResourceLocationConfig - ); - this.poll = createPoll(this.lro); - } - if (!state.pollingURL) { - throw new Error( - "Bad state: polling URL is undefined. Please check if the serialized state is well-formed." - ); + : createGetLroStatusFromResponse({ + lro: this.lro, + info: config, + state: this.state, + }); } const currentState = await this.poll( - state.pollingURL, + throwIfUndefined(config.pollingUrl), this.pollerConfig!, this.getLroStatusFromResponse ); logger.verbose(`LRO: polling response: ${JSON.stringify(currentState.rawResponse)}`); if (currentState.done) { - state.result = this.processResult - ? this.processResult(currentState.flatResponse, state) - : currentState.flatResponse; + state.result = buildResult({ + response: currentState.flatResponse, + state, + processResult: this.processResult, + }); state.isCompleted = true; } else { this.poll = currentState.next ?? this.poll; - state.pollingURL = getPollingUrl(currentState.rawResponse, state.pollingURL); + updatePollingUrl({ + rawResponse: currentState.rawResponse, + info: config, + }); + /** for backward compatability */ + state.pollingURL = config.pollingUrl; } lastResponse = currentState; } diff --git a/sdk/core/core-lro/src/lroEngine/passthrough.ts b/sdk/core/core-lro/src/lroEngine/passthrough.ts deleted file mode 100644 index b141cea5a1e0..000000000000 --- a/sdk/core/core-lro/src/lroEngine/passthrough.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { LroResponse, LroStatus } from "./models"; - -export function processPassthroughOperationResult( - response: LroResponse -): LroStatus { - return { - ...response, - done: true, - }; -} diff --git a/sdk/core/core-lro/src/lroEngine/requestUtils.ts b/sdk/core/core-lro/src/lroEngine/requestUtils.ts deleted file mode 100644 index 50c2e57fd719..000000000000 --- a/sdk/core/core-lro/src/lroEngine/requestUtils.ts +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { LroBody, LroConfig, RawResponse } from "./models"; -import { PollOperationState } from "../pollOperation"; - -/** - * Detects where the continuation token is and returns it. Notice that azure-asyncoperation - * must be checked first before the other location headers because there are scenarios - * where both azure-asyncoperation and location could be present in the same response but - * azure-asyncoperation should be the one to use for polling. - */ -export function getPollingUrl(rawResponse: RawResponse, defaultPath: string): string { - return ( - getAzureAsyncOperation(rawResponse) ?? - getOperationLocation(rawResponse) ?? - getLocation(rawResponse) ?? - defaultPath - ); -} - -function getLocation(rawResponse: RawResponse): string | undefined { - return rawResponse.headers["location"]; -} - -function getOperationLocation(rawResponse: RawResponse): string | undefined { - return rawResponse.headers["operation-location"]; -} - -function getAzureAsyncOperation(rawResponse: RawResponse): string | undefined { - return rawResponse.headers["azure-asyncoperation"]; -} - -function findResourceLocation( - requestMethod: string, - rawResponse: RawResponse, - requestPath: string -): string | undefined { - switch (requestMethod) { - case "PUT": { - return requestPath; - } - case "POST": - case "GET": - case "PATCH": { - return getLocation(rawResponse); - } - default: { - return undefined; - } - } -} - -export function inferLroMode( - requestPath: string, - requestMethod: string, - rawResponse: RawResponse -): LroConfig { - if ( - getAzureAsyncOperation(rawResponse) !== undefined || - getOperationLocation(rawResponse) !== undefined - ) { - return { - mode: "Location", - resourceLocation: findResourceLocation(requestMethod, rawResponse, requestPath), - }; - } else if (getLocation(rawResponse) !== undefined) { - return { - mode: "Location", - }; - } else if (["PUT", "PATCH"].includes(requestMethod)) { - return { - mode: "Body", - }; - } - return {}; -} - -class SimpleRestError extends Error { - public statusCode?: number; - constructor(message: string, statusCode: number) { - super(message); - this.name = "RestError"; - this.statusCode = statusCode; - - Object.setPrototypeOf(this, SimpleRestError.prototype); - } -} - -export function isUnexpectedInitialResponse(rawResponse: RawResponse): boolean { - const code = rawResponse.statusCode; - if (![203, 204, 202, 201, 200].includes(code)) { - throw new SimpleRestError( - `Received unexpected HTTP status code ${code} in the initial response. This may indicate a server issue.`, - code - ); - } - return false; -} - -export function isUnexpectedPollingResponse(rawResponse: RawResponse): boolean { - const code = rawResponse.statusCode; - if (![202, 201, 200].includes(code)) { - throw new SimpleRestError( - `Received unexpected HTTP status code ${code} while polling. This may indicate a server issue.`, - code - ); - } - return false; -} - -export function isCanceled>(operation: { - state: TState; - status: string; -}): boolean { - const { state, status } = operation; - if (["canceled", "cancelled"].includes(status)) { - state.isCancelled = true; - return true; - } - 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"; -} diff --git a/sdk/core/core-lro/src/lroEngine/stateMachine.ts b/sdk/core/core-lro/src/lroEngine/stateMachine.ts deleted file mode 100644 index ec0f828c3912..000000000000 --- a/sdk/core/core-lro/src/lroEngine/stateMachine.ts +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { - GetLroStatusFromResponse, - LongRunningOperation, - LroConfig, - LroResourceLocationConfig, - LroResponse, - LroStatus, - PollerConfig, - ResumablePollOperationState, -} from "./models"; -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>( - lroPrimitives: LongRunningOperation, - config: LroConfig, - state: TState, - lroResourceLocationConfig?: LroResourceLocationConfig -): GetLroStatusFromResponse { - switch (config.mode) { - case "Location": { - return processLocationPollingOperationResult( - lroPrimitives, - state, - config.resourceLocation, - lroResourceLocationConfig - ); - } - case "Body": { - return processBodyPollingOperationResult(state); - } - default: { - return processPassthroughOperationResult; - } - } -} - -/** - * Creates a polling operation. - */ -export function createPoll( - lroPrimitives: LongRunningOperation -): ( - pollingURL: string, - pollerConfig: PollerConfig, - getLroStatusFromResponse: GetLroStatusFromResponse -) => Promise> { - return async ( - path: string, - pollerConfig: PollerConfig, - getLroStatusFromResponse: GetLroStatusFromResponse - ): Promise> => { - const response = await lroPrimitives.sendPollRequest(path); - const retryAfter: string | undefined = response.rawResponse.headers["retry-after"]; - if (retryAfter !== undefined) { - // Retry-After header value is either in HTTP date format, or in seconds - const retryAfterInSeconds = parseInt(retryAfter); - pollerConfig.intervalInMs = isNaN(retryAfterInSeconds) - ? calculatePollingIntervalFromDate(new Date(retryAfter), pollerConfig.intervalInMs) - : retryAfterInSeconds * 1000; - } - return getLroStatusFromResponse(response); - }; -} - -function calculatePollingIntervalFromDate( - retryAfterDate: Date, - defaultIntervalInMs: number -): number { - const timeNow = Math.floor(new Date().getTime()); - const retryAfterTime = retryAfterDate.getTime(); - if (timeNow < retryAfterTime) { - return retryAfterTime - timeNow; - } - return defaultIntervalInMs; -} - -/** - * Creates a callback to be used to initialize the polling operation state. - * @param state - of the polling operation - * @param operationSpec - of the LRO - * @param callback - callback to be called when the operation is done - * @returns callback that initializes the state of the polling operation - */ -export function createInitializeState( - state: ResumablePollOperationState, - requestPath: string, - requestMethod: string -): (response: LroResponse) => boolean { - return (response: LroResponse): boolean => { - if (isUnexpectedInitialResponse(response.rawResponse)) return true; - state.initialRawResponse = response.rawResponse; - state.isStarted = true; - state.pollingURL = getPollingUrl(state.initialRawResponse, requestPath); - state.config = inferLroMode(requestPath, requestMethod, state.initialRawResponse); - /** short circuit polling if body polling is done in the initial request */ - if ( - state.config.mode === undefined || - (state.config.mode === "Body" && - isPollingDone({ - rawResponse: state.initialRawResponse, - status: getProvisioningState(state.initialRawResponse), - })) - ) { - state.result = response.flatResponse as TResult; - state.isCompleted = true; - } - logger.verbose(`LRO: initial state: ${JSON.stringify(state)}`); - return Boolean(state.isCompleted); - }; -} diff --git a/sdk/core/core-lro/test/engine.spec.ts b/sdk/core/core-lro/test/engine.spec.ts index a1b022a008cd..5d6539e2e4b4 100644 --- a/sdk/core/core-lro/test/engine.spec.ts +++ b/sdk/core/core-lro/test/engine.spec.ts @@ -67,58 +67,48 @@ describe("Lro Engine", function () { it("should handle post202NoRetry204", async () => { const path = "/post/202/noretry/204"; const pollingPath = "/post/newuri/202/noretry/204"; - await assertError( - runLro({ - routes: [ - { - method: "POST", - path, - status: 202, - headers: { - location: path, - }, - }, - { - method: "GET", - path, - status: 202, - headers: { - location: pollingPath, - }, + const response = await runLro({ + routes: [ + { + method: "POST", + path, + status: 202, + headers: { + location: path, }, - { - method: "GET", - path: pollingPath, - status: 204, + }, + { + method: "GET", + path, + status: 202, + headers: { + location: pollingPath, }, - ], - }), - { - messagePattern: - /Received unexpected HTTP status code 204 while polling. This may indicate a server issue./, - } - ); + }, + { + method: "GET", + path: pollingPath, + status: 204, + }, + ], + }); + assert.equal(response.statusCode, 204); }); it("should handle deleteNoHeaderInRetry", async () => { const pollingPath = "/delete/noheader/operationresults/123"; - await assertError( - runLro({ - routes: [ - { - method: "DELETE", - status: 200, - headers: { Location: pollingPath }, - }, - { method: "GET", path: pollingPath, status: 202 }, - { method: "GET", path: pollingPath, status: 204 }, - ], - }), - { - messagePattern: - /Received unexpected HTTP status code 204 while polling. This may indicate a server issue./, - } - ); + const response = await runLro({ + routes: [ + { + method: "DELETE", + status: 200, + headers: { Location: pollingPath }, + }, + { method: "GET", path: pollingPath, status: 202 }, + { method: "GET", path: pollingPath, status: 204 }, + ], + }); + assert.equal(response.statusCode, 204); }); it("should handle put202Retry200", async () => { @@ -241,37 +231,32 @@ describe("Lro Engine", function () { it("should handle delete202NoRetry204", async () => { const path = "/delete/202/noretry/204"; const newPath = "/delete/newuri/202/noretry/204"; - await assertError( - runLro({ - routes: [ - { - method: "DELETE", - path, - status: 202, - headers: { - location: path, - }, - }, - { - method: "GET", - path, - status: 202, - headers: { - location: newPath, - }, + const response = await runLro({ + routes: [ + { + method: "DELETE", + path, + status: 202, + headers: { + location: path, }, - { - method: "GET", - path: newPath, - status: 204, + }, + { + method: "GET", + path, + status: 202, + headers: { + location: newPath, }, - ], - }), - { - messagePattern: - /Received unexpected HTTP status code 204 while polling. This may indicate a server issue./, - } - ); + }, + { + method: "GET", + path: newPath, + status: 204, + }, + ], + }); + assert.equal(response.statusCode, 204); }); it("should handle deleteProvisioning202Accepted200Succeeded", async () => { @@ -300,7 +285,7 @@ describe("Lro Engine", function () { it("should handle deleteProvisioning202DeletingFailed200", async () => { const path = "/delete/provisioning/202/deleting/200/failed"; - const result = await runLro({ + const response = await runLro({ routes: [ { method: "DELETE", @@ -320,12 +305,13 @@ describe("Lro Engine", function () { }, ], }); - assert.equal(result.properties?.provisioningState, "Failed"); + assert.equal(response.statusCode, 200); + assert.equal(response.properties?.provisioningState, "Failed"); }); it("should handle deleteProvisioning202Deletingcanceled200", async () => { const path = "/delete/provisioning/202/deleting/200/canceled"; - const result = await runLro({ + const response = await runLro({ routes: [ { method: "DELETE", @@ -345,7 +331,8 @@ describe("Lro Engine", function () { }, ], }); - assert.equal(result.properties?.provisioningState, "Canceled"); + assert.equal(response.statusCode, 200); + assert.equal(response.properties?.provisioningState, "Canceled"); }); }); @@ -606,6 +593,39 @@ describe("Lro Engine", function () { assert.equal(result.name, "foo"); }); + it("should handle postUpdatedPollingUrl", async () => { + const operationLocationPath1 = "path1"; + const operationLocationPath2 = "path2"; + const result = await runLro({ + routes: [ + { + method: "POST", + status: 200, + headers: { + [headerName]: operationLocationPath1, + }, + }, + { + method: "GET", + path: operationLocationPath1, + status: 200, + body: `{ "status": "running" }`, + headers: { + [headerName]: operationLocationPath2, + }, + }, + { + method: "GET", + path: operationLocationPath2, + status: 200, + body: `{ "status": "succeeded", "id": "100", "name": "foo" }`, + }, + ], + }); + assert.equal(result.id, "100"); + assert.equal(result.name, "foo"); + }); + it("should handle postDoubleHeadersFinalAzureHeaderGet", async () => { const locationPath = `/LROPostDoubleHeadersFinalAzureHeaderGet/location`; const operationLocationPath = `/LROPostDoubleHeadersFinalAzureHeaderGet/asyncOperationUrl`; @@ -2008,7 +2028,7 @@ describe("Lro Engine", function () { }); describe("process result", () => { - it("The final result can be processed using processResult", async () => { + it("From a location response", async () => { const locationPath = "/postlocation/noretry/succeeded/operationResults/foo/200/"; const pollingPath = "/postasync/noretry/succeeded/operationResults/foo/200/"; const headerName = "Operation-Location"; @@ -2051,6 +2071,28 @@ describe("Lro Engine", function () { assert.equal(serializedState, poller.toString()); assert.ok(state.initialRawResponse); assert.ok(state.pollingURL); + assert.ok(state.config.pollingUrl); + assert.equal((result as any).id, "100"); + return { ...(result as any), id: "200" }; + }, + }); + const result = await poller.pollUntilDone(); + assert.deepInclude(result, { id: "200", name: "foo" }); + }); + + it("From the initial response", async () => { + const poller = createPoller({ + routes: [ + { + method: "PUT", + status: 200, + body: `{"properties":{"provisioningState":"Succeeded"},"id":"100","name":"foo"}`, + }, + ], + processResult: (result: unknown, state: any) => { + const serializedState = JSON.stringify({ state: state }); + assert.equal(serializedState, poller.toString()); + assert.ok(state.initialRawResponse); assert.equal((result as any).id, "100"); return { ...(result as any), id: "200" }; }, diff --git a/sdk/core/core-lro/test/utils/coreRestPipelineLro.ts b/sdk/core/core-lro/test/utils/coreRestPipelineLro.ts index 57f5458c2dbf..034bebd19a8d 100644 --- a/sdk/core/core-lro/test/utils/coreRestPipelineLro.ts +++ b/sdk/core/core-lro/test/utils/coreRestPipelineLro.ts @@ -4,7 +4,7 @@ import { LongRunningOperation, LroResponse } from "../../src"; import { PipelineRequest } from "@azure/core-rest-pipeline"; -export type SendOperationFn = (request: PipelineRequest) => Promise>; +type SendOperationFn = (request: PipelineRequest) => Promise>; export class CoreRestPipelineLro implements LongRunningOperation { constructor( diff --git a/sdk/core/core-lro/test/utils/router.ts b/sdk/core/core-lro/test/utils/router.ts index 4f93b82d7d3e..3f40e7d5d871 100644 --- a/sdk/core/core-lro/test/utils/router.ts +++ b/sdk/core/core-lro/test/utils/router.ts @@ -81,7 +81,7 @@ function createClient(routes: RouteProcessor[]): HttpClient { }; } -export type Response = LroBody & { statusCode: number }; +type Response = LroBody & { statusCode: number }; function createSendOp(settings: { client: HttpClient; diff --git a/sdk/core/core-lro/test/utils/testOperation.ts b/sdk/core/core-lro/test/utils/testOperation.ts index 8523af86ac32..5bf50f21ae48 100644 --- a/sdk/core/core-lro/test/utils/testOperation.ts +++ b/sdk/core/core-lro/test/utils/testOperation.ts @@ -19,7 +19,7 @@ export interface TestOperationState extends PollOperationState { unsupportedCancel?: boolean; } -export interface TestOperation extends PollOperation {} +interface TestOperation extends PollOperation {} async function update( this: TestOperation,