Serializing Azure long running operation for later reuse

619 Views Asked by At

I'm trying to use Azure SDK for javascript (@azure/arm-sql version 8.0.0) to copy SQL database but I don't want to wait until the operation is done. Instead, I'd like to exit once the request is created and later (let's say each minute) check whether the operation has finished. The SDK seems to support my use case through functions:

getPollState()

Get an LROPollState object that can be used to poll this LRO in a different context (such as on a different process or a different machine). If the LRO couldn't produce an LRO polling strategy, then this will return undefined.

and restoreLROPoller()

Restore an LROPoller from the provided LROPollState. This method can be used to recreate an LROPoller on a different process or machine.

However, the documentation doesn't specify how the state should be serialized/transferred over the wire. I naively tried to serialize it into JSON but when I run the snippet below, I get the following error:

TypeError: operationSpec.serializer.deserialize is not a function occurred in deserializing the responseBody - {"name":"b9952e45-85ff-41f8-b01c-83050c9d9a2c","status":"InProgress","startTime":"2021-10-14T15:38:01.59Z"}

Here is a simplified code snippet:

import { SqlManagementClient } from "@azure/arm-sql";
import { DefaultAzureCredential } from "@azure/identity";
import { LROPoller } from "@azure/ms-rest-azure-js";

const subscription = "<subscription ID>";
const rg = "myResourceGroup";
const server = "mySqlServer";
const dbName = "myDb";
const credentials = new DefaultAzureCredential();

const sqlClient = new SqlManagementClient(credentials, subscription);
const originalDb = await sqlClient.databases.get(rg, server, dbName);
const operation: LROPoller = await sqlClient.databases.beginCreateOrUpdate(rg, server, dbName + "_copy", {
    location: "westeurope",
    createMode: "Copy",
    sourceDatabaseId: originalDb.id
});

const operationState = operation.getPollState()!;
const serializedState = JSON.stringify(operationState);

// The program would save the state somewhere and exit now, but let's make it simple.

const deserializedState = JSON.parse(serializedState);
const restoredOperation: LROPoller = sqlClient.restoreLROPoller(deserializedState);

// Following line throws the exception
// TypeError: operationSpec.serializer.deserialize is not a function occurred in deserializing the responseBody…
await restoredOperation.poll();

So my question is how can I save the operation state in a way that I can later reuse it.

1

There are 1 best solutions below

0
On

For those who might want to achieve something similar, here is the workaround. However, I still want to get rid of extra code and use SDK functionality itself, so if anyone can answer the original question, I'd be more than happy.

Here is a file AzureOperations.ts with helper functions

import { TokenCredential } from "@azure/core-auth";
import { LROPoller } from "@azure/ms-rest-azure-js";
import fetch from "node-fetch";

export interface AzureOperationReference {
    statusUrl: string
}

export interface AzureOperation {
    status: "InProgress" | "Succeeded" | "Failed" | "Canceled"

    error?: {
        code: string,
        message: string
    }
}

export const createAzureOperationReference = (operation: LROPoller): AzureOperationReference => {
    const asyncOperationHeader = "Azure-AsyncOperation";
    const headers = operation.getInitialResponse().headers;
    if (!headers.contains(asyncOperationHeader)) {
        throw new Error(`Given operation is currently not supported because it does not contain header '${asyncOperationHeader}'. If you want to track this operation, implement logic that uses header 'Location' first.`);
    }

    return {
        statusUrl: headers.get(asyncOperationHeader)!
    };
};

export const createAzureOperationChecker = (operationReference: AzureOperationReference, credentials: TokenCredential) => {
    let token: string = null!;
    let tokenExpiration = 0;
    let previousOperation: AzureOperation = null!;
    let retryAfter = 0;

    return async () => {
        const now = new Date().getTime();
        if (now < retryAfter) {
            return previousOperation;
        }

        if (tokenExpiration < now) {
            const newToken = await credentials.getToken("https://management.azure.com/.default");
            if (newToken === null) {
                throw new Error("Cannot obtain new Azure access token.");
            }

            tokenExpiration = newToken.expiresOnTimestamp;
            token = newToken.token;
        }

        const response = await fetch(operationReference.statusUrl, {
            method: "GET",
            headers: {
                Authorization: `Bearer ${token}`
            }
        });

        const retryLimitInMiliseconds = Number(response.headers.get("Retry-After")) * 1000;
        retryAfter = new Date().getTime() + retryLimitInMiliseconds;

        return previousOperation = await response.json() as AzureOperation;
    }
}

Then you can import and use them to track pending operations:

// unimportant code removed for brevity
import { createAzureOperationReference, createAzureOperationChecker } from "./AzureOperations.js";

const credentials = new DefaultAzureCredential();
const operation: LROPoller = await sqlClient.databases.beginCreateOrUpdate(…);

// You can serialize this reference as json and store it wherever you want
const reference = createAzureOperationReference(operation);


// You can deserialize it later and use it to fetch the operation status
const checkOperation = createAzureOperationChecker(reference, credentials);
const operationStatus = await checkOperation();
console.log(operationStatus.status);