diff --git a/__test__/retry-helper.test.ts b/__test__/retry-helper.test.ts index a5d3f79..02c7ed3 100644 --- a/__test__/retry-helper.test.ts +++ b/__test__/retry-helper.test.ts @@ -2,7 +2,6 @@ import * as core from '@actions/core' import {RetryHelper} from '../lib/retry-helper' let info: string[] -let retryHelper: any describe('retry-helper tests', () => { beforeAll(() => { @@ -10,8 +9,6 @@ describe('retry-helper tests', () => { jest.spyOn(core, 'info').mockImplementation((message: string) => { info.push(message) }) - - retryHelper = new RetryHelper(3, 0, 0) }) beforeEach(() => { @@ -25,14 +22,22 @@ describe('retry-helper tests', () => { }) it('first attempt succeeds', async () => { + const retryHelper: any = new RetryHelper(3, 1, 10) + const sleep = jest.fn().mockResolvedValue(undefined) + retryHelper.sleep = sleep + const actual = await retryHelper.execute(async () => { return 'some result' }) expect(actual).toBe('some result') expect(info).toHaveLength(0) + expect(sleep).not.toHaveBeenCalled() }) it('second attempt succeeds', async () => { + const retryHelper: any = new RetryHelper(3, 1, 10) + const sleep = jest.fn().mockResolvedValue(undefined) + retryHelper.sleep = sleep let attempts = 0 const actual = await retryHelper.execute(() => { if (++attempts == 1) { @@ -45,10 +50,15 @@ describe('retry-helper tests', () => { expect(actual).toBe('some result') expect(info).toHaveLength(2) expect(info[0]).toBe('some error') - expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) + expect(info[1]).toBe('Waiting 1 seconds before trying again') + expect(sleep).toHaveBeenCalledTimes(1) + expect(sleep).toHaveBeenCalledWith(1) }) it('third attempt succeeds', async () => { + const retryHelper: any = new RetryHelper(3, 1, 10) + const sleep = jest.fn().mockResolvedValue(undefined) + retryHelper.sleep = sleep let attempts = 0 const actual = await retryHelper.execute(() => { if (++attempts < 3) { @@ -61,12 +71,18 @@ describe('retry-helper tests', () => { expect(actual).toBe('some result') expect(info).toHaveLength(4) expect(info[0]).toBe('some error 1') - expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) + expect(info[1]).toBe('Waiting 1 seconds before trying again') expect(info[2]).toBe('some error 2') - expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) + expect(info[3]).toBe('Waiting 2 seconds before trying again') + expect(sleep).toHaveBeenCalledTimes(2) + expect(sleep).toHaveBeenNthCalledWith(1, 1) + expect(sleep).toHaveBeenNthCalledWith(2, 2) }) it('all attempts fail succeeds', async () => { + const retryHelper: any = new RetryHelper(3, 1, 10) + const sleep = jest.fn().mockResolvedValue(undefined) + retryHelper.sleep = sleep let attempts = 0 let error: Error = null as unknown as Error try { @@ -80,8 +96,42 @@ describe('retry-helper tests', () => { expect(attempts).toBe(3) expect(info).toHaveLength(4) expect(info[0]).toBe('some error 1') - expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) + expect(info[1]).toBe('Waiting 1 seconds before trying again') expect(info[2]).toBe('some error 2') - expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) + expect(info[3]).toBe('Waiting 2 seconds before trying again') + expect(sleep).toHaveBeenCalledTimes(2) + expect(sleep).toHaveBeenNthCalledWith(1, 1) + expect(sleep).toHaveBeenNthCalledWith(2, 2) + }) + + it('server-side 500 errors are retried with exponential backoff', async () => { + const retryHelper: any = new RetryHelper(4, 2, 10) + const sleep = jest.fn().mockResolvedValue(undefined) + retryHelper.sleep = sleep + let attempts = 0 + + const actual = await retryHelper.execute(() => { + if (++attempts < 3) { + const error: Error & {status?: number} = new Error( + `server error ${attempts}` + ) + error.status = 500 + throw error + } + + return Promise.resolve('some result') + }) + + expect(actual).toBe('some result') + expect(attempts).toBe(3) + expect(info).toEqual([ + 'server error 1', + 'Waiting 2 seconds before trying again', + 'server error 2', + 'Waiting 4 seconds before trying again' + ]) + expect(sleep).toHaveBeenCalledTimes(2) + expect(sleep).toHaveBeenNthCalledWith(1, 2) + expect(sleep).toHaveBeenNthCalledWith(2, 4) }) }) diff --git a/dist/index.js b/dist/index.js index 57729b2..84cfd27 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2567,7 +2567,7 @@ class RetryHelper { core.info(err === null || err === void 0 ? void 0 : err.message); } // Sleep - const seconds = this.getSleepAmount(); + const seconds = this.getSleepAmount(attempt); core.info(`Waiting ${seconds} seconds before trying again`); yield this.sleep(seconds); attempt++; @@ -2576,9 +2576,11 @@ class RetryHelper { return yield action(); }); } - getSleepAmount() { - return (Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) + - this.minSeconds); + getSleepAmount(attempt) { + if (this.minSeconds === 0) { + return 0; + } + return Math.min(this.minSeconds * Math.pow(2, attempt - 1), this.maxSeconds); } sleep(seconds) { return __awaiter(this, void 0, void 0, function* () { diff --git a/src/retry-helper.ts b/src/retry-helper.ts index 323e75d..517b23e 100644 --- a/src/retry-helper.ts +++ b/src/retry-helper.ts @@ -33,7 +33,7 @@ export class RetryHelper { } // Sleep - const seconds = this.getSleepAmount() + const seconds = this.getSleepAmount(attempt) core.info(`Waiting ${seconds} seconds before trying again`) await this.sleep(seconds) attempt++ @@ -43,11 +43,12 @@ export class RetryHelper { return await action() } - private getSleepAmount(): number { - return ( - Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) + - this.minSeconds - ) + private getSleepAmount(attempt: number): number { + if (this.minSeconds === 0) { + return 0 + } + + return Math.min(this.minSeconds * Math.pow(2, attempt - 1), this.maxSeconds) } private async sleep(seconds: number): Promise {