-
Notifications
You must be signed in to change notification settings - Fork 1.7k
handle response body error #369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import * as core from '@actions/core' | ||
| import {RetryHelper} from '../src/retry-helper' | ||
|
|
||
| let info: string[] | ||
| let retryHelper: RetryHelper | ||
|
|
||
| describe('retry-helper tests', () => { | ||
| beforeAll(() => { | ||
| // Mock @actions/core info() | ||
| jest.spyOn(core, 'info').mockImplementation((message: string) => { | ||
| info.push(message) | ||
| }) | ||
|
|
||
| retryHelper = new RetryHelper(3, 0, 0) | ||
| }) | ||
|
|
||
| beforeEach(() => { | ||
| // Reset info | ||
| info = [] | ||
| }) | ||
|
|
||
| afterAll(() => { | ||
| // Restore | ||
| jest.restoreAllMocks() | ||
| }) | ||
|
|
||
| it('first attempt succeeds', async () => { | ||
| const actual = await retryHelper.execute(async () => { | ||
| return 'some result' | ||
| }) | ||
| expect(actual).toBe('some result') | ||
| expect(info).toHaveLength(0) | ||
| }) | ||
|
|
||
| it('second attempt succeeds', async () => { | ||
| let attempts = 0 | ||
| const actual = await retryHelper.execute(async () => { | ||
| if (++attempts === 1) { | ||
| throw new Error('some error') | ||
| } | ||
|
|
||
| return Promise.resolve('some result') | ||
| }) | ||
| expect(attempts).toBe(2) | ||
| expect(actual).toBe('some result') | ||
| expect(info).toHaveLength(2) | ||
| expect(info[0]).toBe('some error') | ||
| expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) | ||
| }) | ||
|
|
||
| it('third attempt succeeds', async () => { | ||
| let attempts = 0 | ||
| const actual = await retryHelper.execute(async () => { | ||
| if (++attempts < 3) { | ||
| throw new Error(`some error ${attempts}`) | ||
| } | ||
|
|
||
| return Promise.resolve('some result') | ||
| }) | ||
| expect(attempts).toBe(3) | ||
| 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[2]).toBe('some error 2') | ||
| expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) | ||
| }) | ||
|
|
||
| it('all attempts fail succeeds', async () => { | ||
| let attempts = 0 | ||
| let error: Error = (null as unknown) as Error | ||
| try { | ||
| await retryHelper.execute(() => { | ||
| throw new Error(`some error ${++attempts}`) | ||
| }) | ||
| } catch (err) { | ||
| error = err | ||
| } | ||
| expect(error.message).toBe('some error 3') | ||
| 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[2]).toBe('some error 2') | ||
| expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import * as core from '@actions/core' | ||
|
|
||
| /** | ||
| * Internal class for retries | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Private for now. If expose a class for consumers we probably want to put more thought into the interface and behaviors. And probably do exponent backoff in that case. |
||
| */ | ||
| export class RetryHelper { | ||
| private maxAttempts: number | ||
| private minSeconds: number | ||
| private maxSeconds: number | ||
|
|
||
| constructor(maxAttempts: number, minSeconds: number, maxSeconds: number) { | ||
| if (maxAttempts < 1) { | ||
| throw new Error('max attempts should be greater than or equal to 1') | ||
| } | ||
|
|
||
| this.maxAttempts = maxAttempts | ||
| this.minSeconds = Math.floor(minSeconds) | ||
| this.maxSeconds = Math.floor(maxSeconds) | ||
| if (this.minSeconds > this.maxSeconds) { | ||
| throw new Error('min seconds should be less than or equal to max seconds') | ||
| } | ||
| } | ||
|
|
||
| async execute<T>(action: () => Promise<T>): Promise<T> { | ||
| let attempt = 1 | ||
| while (attempt < this.maxAttempts) { | ||
| // Try | ||
| try { | ||
| return await action() | ||
| } catch (err) { | ||
| core.info(err.message) | ||
| } | ||
|
|
||
| // Sleep | ||
| const seconds = this.getSleepAmount() | ||
| core.info(`Waiting ${seconds} seconds before trying again`) | ||
| await this.sleep(seconds) | ||
| attempt++ | ||
| } | ||
|
|
||
| // Last attempt | ||
| return await action() | ||
| } | ||
|
|
||
| private getSleepAmount(): number { | ||
| return ( | ||
| Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) + | ||
| this.minSeconds | ||
| ) | ||
| } | ||
|
|
||
| private async sleep(seconds: number): Promise<void> { | ||
| return new Promise(resolve => setTimeout(resolve, seconds * 1000)) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.