From fa06583ba81f2cd728fd6136d6fa74eca8996e9f Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 25 Jul 2019 10:16:34 -0700 Subject: [PATCH 1/5] Added retry logic in function app listing --- src/services/apimService.ts | 4 +- src/services/functionAppService.ts | 76 ++++++++++++++++++++---------- src/shared/utils.test.ts | 63 +++++++++++++++++++++++++ src/shared/utils.ts | 35 ++++++++++++++ 4 files changed, 153 insertions(+), 25 deletions(-) diff --git a/src/services/apimService.ts b/src/services/apimService.ts index 9257b132..f66a722b 100644 --- a/src/services/apimService.ts +++ b/src/services/apimService.ts @@ -216,7 +216,9 @@ export class ApimService extends BaseService { responses: options.operation.responses || [], }; - const operationUrl = `${service.gatewayUrl}/${api.path}${operationConfig.urlTemplate}`; + // Ensure a single path seperator in the operation path + const operationPath = `/${api.path}/${operationConfig.urlTemplate}`.replace(/\/+/g, "/"); + const operationUrl = `${service.gatewayUrl}${operationPath}`; this.log(`--> Deploying API operation ${options.function}: ${operationConfig.method.toUpperCase()} ${operationUrl}`); const operation = await client.apiOperation.createOrUpdate( diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index b0eb5cd5..75f4bf44 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -10,6 +10,7 @@ import { Guard } from "../shared/guard"; import { ArmService } from "./armService"; import { AzureBlobStorageService } from "./azureBlobStorageService"; import { BaseService } from "./baseService"; +import { Utils } from "../shared/utils"; export class FunctionAppService extends BaseService { private webClient: WebSiteManagementClient; @@ -90,27 +91,53 @@ export class FunctionAppService extends BaseService { Guard.null(functionApp); const getTokenUrl = `${this.baseUrl}${functionApp.id}/functions?api-version=2016-08-01`; - const response = await this.sendApiRequest("GET", getTokenUrl); + try { + const response = await Utils.runWithRetry(async () => { + const listFunctionsResponse = await this.sendApiRequest("GET", getTokenUrl); - if (response.status !== 200) { + if (listFunctionsResponse.status !== 200 || listFunctionsResponse.data.value.length === 0) { + this.log("-> Function App not ready. Retrying..."); + throw new Error(listFunctionsResponse.data); + } + + return listFunctionsResponse; + }, 10, 5000); + + return response.data.value.map((functionConfig) => functionConfig.properties); + } + catch (e) { + this.log("Unable to retrieve function app list"); return []; } - - return response.data.value.map((functionConfig) => functionConfig.properties); } + /** + * Gets the configuration of the specified function within the function app + * @param functionApp The parent function app + * @param functionName The name of hte function + */ public async getFunction(functionApp: Site, functionName: string): Promise { Guard.null(functionApp); Guard.empty(functionName); const getFunctionUrl = `${this.baseUrl}${functionApp.id}/functions/${functionName}?api-version=2016-08-01`; - const response = await this.sendApiRequest("GET", getFunctionUrl); - if (response.status !== 200) { + try { + const response = await Utils.runWithRetry(async () => { + const getFunctionResponse = await this.sendApiRequest("GET", getFunctionUrl); + + if (getFunctionResponse.status !== 200) { + this.log("-> Function app not ready. Retrying...") + throw new Error(response.data); + } + + return getFunctionResponse; + }, 10, 5000); + + return response.data.properties; + } catch (e) { return null; } - - return response.data.properties; } public async uploadFunctions(functionApp: Site): Promise { @@ -122,6 +149,23 @@ export class FunctionAppService extends BaseService { const uploadFunctionApp = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile); const uploadBlobStorage = this.uploadZippedArtifactToBlobStorage(functionZipFile); await Promise.all([uploadFunctionApp, uploadBlobStorage]); + + + this.log("Deployed serverless functions:") + const serverlessFunctions = this.serverless.service.getAllFunctions(); + const deployedFunctions = await this.listFunctions(functionApp); + + // List functions that are part of the serverless yaml config + deployedFunctions.forEach((functionConfig) => { + if (serverlessFunctions.includes(functionConfig.name)) { + const httpConfig = this.getFunctionHttpTriggerConfig(functionApp, functionConfig); + + if (httpConfig) { + const method = httpConfig.methods[0].toUpperCase(); + this.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`); + } + } + }); } /** @@ -166,23 +210,7 @@ export class FunctionAppService extends BaseService { }; await this.sendFile(requestOptions, functionZipFile); - this.log("-> Function package uploaded successfully"); - const serverlessFunctions = this.serverless.service.getAllFunctions(); - const deployedFunctions = await this.listFunctions(functionApp); - - this.log("Deployed serverless functions:") - deployedFunctions.forEach((functionConfig) => { - // List functions that are part of the serverless yaml config - if (serverlessFunctions.includes(functionConfig.name)) { - const httpConfig = this.getFunctionHttpTriggerConfig(functionApp, functionConfig); - - if (httpConfig) { - const method = httpConfig.methods[0].toUpperCase(); - this.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`); - } - } - }); } /** diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts index 668d6e8a..7940bc7a 100644 --- a/src/shared/utils.test.ts +++ b/src/shared/utils.test.ts @@ -105,4 +105,67 @@ describe("utils", () => { } ); }); + + describe("runWithRetry", () => { + it("returns values after 1st run", async () => { + const expected = "success"; + let lastRetry = 0; + + const result = await Utils.runWithRetry((retry) => { + lastRetry = retry; + return Promise.resolve(expected); + }); + + expect(lastRetry).toEqual(1); + expect(result).toEqual(expected); + }); + + it("returns values after successfully retry (reject promise)", async () => { + const expected = "success"; + let lastRetry = 0; + + const result = await Utils.runWithRetry((retry) => { + lastRetry = retry; + if (retry === 1) { + return Promise.reject("rejected"); + } + + return Promise.resolve(expected); + }); + + expect(lastRetry).toEqual(2); + expect(result).toEqual(expected); + }); + + it("returns values after successfully retry (throw error)", async () => { + const expected = "success"; + let lastRetry = 0; + + const result = await Utils.runWithRetry((retry) => { + lastRetry = retry; + if (retry === 1) { + throw new Error("Ooops!") + } + + return Promise.resolve(expected); + }); + + expect(lastRetry).toEqual(2); + expect(result).toEqual(expected); + }); + it("throws error after reties", async () => { + const maxRetries = 5; + let lastRetry = 0; + + const test = async () => { + await Utils.runWithRetry((retry) => { + lastRetry = retry; + return Promise.reject("rejected"); + }, maxRetries, 100); + }; + + await expect(test()).rejects.toEqual("rejected"); + expect(lastRetry).toEqual(maxRetries); + }); + }); }); diff --git a/src/shared/utils.ts b/src/shared/utils.ts index bf3717ff..2c0e636a 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -160,4 +160,39 @@ export class Utils { return settings && settings.direction === "out"; }); } + + /** + * Runs an operation with auto retry policy + * @param operation The operation to run + * @param maxRetries The max number or retreis + * @param retryWaitInterval The time to wait between retries + */ + public static async runWithRetry(operation: (retry?: number) => Promise, maxRetries: number = 3, retryWaitInterval: number = 1000) { + let retry = 0; + let error = null; + + while (retry < maxRetries) { + try { + retry++; + return await operation(retry); + } + catch (e) { + error = e; + } + + await Utils.wait(retryWaitInterval); + } + + return Promise.reject(error); + } + + /** + * Waits for the specified amount of time. + * @param time The amount of time to wait (default = 1000ms) + */ + private static wait(time: number = 1000) { + return new Promise((resolve) => { + setTimeout(resolve, time); + }); + } } From 4aaa14c81ed66e731365911801ebcadd25b0c766 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 25 Jul 2019 10:19:48 -0700 Subject: [PATCH 2/5] Updated app fucntion list log error message --- src/services/functionAppService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 75f4bf44..0df51a0b 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -106,7 +106,7 @@ export class FunctionAppService extends BaseService { return response.data.value.map((functionConfig) => functionConfig.properties); } catch (e) { - this.log("Unable to retrieve function app list"); + this.log("-> Unable to retrieve function app list"); return []; } } From c5b28b65237a923d784435c89f2d0299fd782deb Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 2 Aug 2019 09:05:29 -0700 Subject: [PATCH 3/5] fix: Fail if unable to retrieve list of functions --- src/services/functionAppService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 0df51a0b..1340d20f 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -107,7 +107,7 @@ export class FunctionAppService extends BaseService { } catch (e) { this.log("-> Unable to retrieve function app list"); - return []; + throw e; } } From 6cdc50e190946c04f4e1fc8f1324382d5b37ac86 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 2 Aug 2019 09:12:11 -0700 Subject: [PATCH 4/5] fix: Moved retryCount and retryInterval into constants --- src/services/functionAppService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 1340d20f..a94f575b 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -13,6 +13,8 @@ import { BaseService } from "./baseService"; import { Utils } from "../shared/utils"; export class FunctionAppService extends BaseService { + private static readonly retryCount: number = 10; + private static readonly retryInterval: number = 5000; private webClient: WebSiteManagementClient; private blobService: AzureBlobStorageService; @@ -101,7 +103,7 @@ export class FunctionAppService extends BaseService { } return listFunctionsResponse; - }, 10, 5000); + }, FunctionAppService.retryCount, FunctionAppService.retryInterval); return response.data.value.map((functionConfig) => functionConfig.properties); } @@ -132,7 +134,7 @@ export class FunctionAppService extends BaseService { } return getFunctionResponse; - }, 10, 5000); + }, FunctionAppService.retryCount, FunctionAppService.retryInterval); return response.data.properties; } catch (e) { From a51424bf7404ac94270fb2ea30d6b3ff187c8ae3 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 2 Aug 2019 09:18:30 -0700 Subject: [PATCH 5/5] test: Verify Utils.wait(...) --- src/shared/utils.test.ts | 20 ++++++++++++++++++++ src/shared/utils.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts index 7940bc7a..517c60b8 100644 --- a/src/shared/utils.test.ts +++ b/src/shared/utils.test.ts @@ -168,4 +168,24 @@ describe("utils", () => { expect(lastRetry).toEqual(maxRetries); }); }); + + describe("wait", () => { + const setTimeoutMock = jest.fn((resolve) => resolve()); + + beforeEach(() => { + global.setTimeout = setTimeoutMock; + }); + + it("waits 1000 by default", async () => { + await Utils.wait(); + + expect(setTimeoutMock).toBeCalledWith(expect.any(Function), 1000); + }); + + it("waits specified time", async () => { + await Utils.wait(2000); + + expect(setTimeoutMock).toBeCalledWith(expect.any(Function), 2000); + }); + }); }); diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 2c0e636a..3d091fc3 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -190,7 +190,7 @@ export class Utils { * Waits for the specified amount of time. * @param time The amount of time to wait (default = 1000ms) */ - private static wait(time: number = 1000) { + public static wait(time: number = 1000) { return new Promise((resolve) => { setTimeout(resolve, time); });