diff --git a/__test__/retry-helper.test.ts b/__test__/retry-helper.test.ts index 2cd5026..38f466e 100644 --- a/__test__/retry-helper.test.ts +++ b/__test__/retry-helper.test.ts @@ -23,11 +23,9 @@ jest.unstable_mockModule('@actions/core', () => ({ // Dynamic imports after mocking const {RetryHelper} = await import('../src/retry-helper.js') -let retryHelper: any - describe('retry-helper tests', () => { beforeAll(() => { - retryHelper = new RetryHelper(3, 0, 0) + // @actions/core is mocked at module load above; nothing to set up here. }) beforeEach(() => { @@ -40,14 +38,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) { @@ -60,10 +66,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) { @@ -76,12 +87,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 { @@ -95,8 +112,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 b381bd2..60524d0 100644 --- a/dist/index.js +++ b/dist/index.js @@ -35491,7 +35491,7 @@ class retry_helper_RetryHelper { info(err?.message); } // Sleep - const seconds = this.getSleepAmount(); + const seconds = this.getSleepAmount(attempt); info(`Waiting ${seconds} seconds before trying again`); await this.sleep(seconds); attempt++; @@ -35499,9 +35499,11 @@ class retry_helper_RetryHelper { // Last attempt return await 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); } async sleep(seconds) { return new Promise(resolve => setTimeout(resolve, seconds * 1000)); 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 {