mirror of
https://github.com/actions/checkout.git
synced 2026-05-13 16:38:07 +00:00
Handle checkout 500 retries with backoff
Switch the shared retry helper from randomized delays to exponential backoff so transient GitHub 500 errors are retried predictably. Add coverage for the backoff sequence and regenerate the bundled dist output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
900f2210b1
commit
f2248d03e5
@ -2,7 +2,6 @@ import * as core from '@actions/core'
|
|||||||
import {RetryHelper} from '../lib/retry-helper'
|
import {RetryHelper} from '../lib/retry-helper'
|
||||||
|
|
||||||
let info: string[]
|
let info: string[]
|
||||||
let retryHelper: any
|
|
||||||
|
|
||||||
describe('retry-helper tests', () => {
|
describe('retry-helper tests', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -10,8 +9,6 @@ describe('retry-helper tests', () => {
|
|||||||
jest.spyOn(core, 'info').mockImplementation((message: string) => {
|
jest.spyOn(core, 'info').mockImplementation((message: string) => {
|
||||||
info.push(message)
|
info.push(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
retryHelper = new RetryHelper(3, 0, 0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -25,14 +22,22 @@ describe('retry-helper tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('first attempt succeeds', async () => {
|
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 () => {
|
const actual = await retryHelper.execute(async () => {
|
||||||
return 'some result'
|
return 'some result'
|
||||||
})
|
})
|
||||||
expect(actual).toBe('some result')
|
expect(actual).toBe('some result')
|
||||||
expect(info).toHaveLength(0)
|
expect(info).toHaveLength(0)
|
||||||
|
expect(sleep).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('second attempt succeeds', async () => {
|
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
|
let attempts = 0
|
||||||
const actual = await retryHelper.execute(() => {
|
const actual = await retryHelper.execute(() => {
|
||||||
if (++attempts == 1) {
|
if (++attempts == 1) {
|
||||||
@ -45,10 +50,15 @@ describe('retry-helper tests', () => {
|
|||||||
expect(actual).toBe('some result')
|
expect(actual).toBe('some result')
|
||||||
expect(info).toHaveLength(2)
|
expect(info).toHaveLength(2)
|
||||||
expect(info[0]).toBe('some error')
|
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 () => {
|
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
|
let attempts = 0
|
||||||
const actual = await retryHelper.execute(() => {
|
const actual = await retryHelper.execute(() => {
|
||||||
if (++attempts < 3) {
|
if (++attempts < 3) {
|
||||||
@ -61,12 +71,18 @@ describe('retry-helper tests', () => {
|
|||||||
expect(actual).toBe('some result')
|
expect(actual).toBe('some result')
|
||||||
expect(info).toHaveLength(4)
|
expect(info).toHaveLength(4)
|
||||||
expect(info[0]).toBe('some error 1')
|
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[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 () => {
|
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 attempts = 0
|
||||||
let error: Error = null as unknown as Error
|
let error: Error = null as unknown as Error
|
||||||
try {
|
try {
|
||||||
@ -80,8 +96,42 @@ describe('retry-helper tests', () => {
|
|||||||
expect(attempts).toBe(3)
|
expect(attempts).toBe(3)
|
||||||
expect(info).toHaveLength(4)
|
expect(info).toHaveLength(4)
|
||||||
expect(info[0]).toBe('some error 1')
|
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[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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
10
dist/index.js
vendored
10
dist/index.js
vendored
@ -2567,7 +2567,7 @@ class RetryHelper {
|
|||||||
core.info(err === null || err === void 0 ? void 0 : err.message);
|
core.info(err === null || err === void 0 ? void 0 : err.message);
|
||||||
}
|
}
|
||||||
// Sleep
|
// Sleep
|
||||||
const seconds = this.getSleepAmount();
|
const seconds = this.getSleepAmount(attempt);
|
||||||
core.info(`Waiting ${seconds} seconds before trying again`);
|
core.info(`Waiting ${seconds} seconds before trying again`);
|
||||||
yield this.sleep(seconds);
|
yield this.sleep(seconds);
|
||||||
attempt++;
|
attempt++;
|
||||||
@ -2576,9 +2576,11 @@ class RetryHelper {
|
|||||||
return yield action();
|
return yield action();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
getSleepAmount() {
|
getSleepAmount(attempt) {
|
||||||
return (Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) +
|
if (this.minSeconds === 0) {
|
||||||
this.minSeconds);
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.min(this.minSeconds * Math.pow(2, attempt - 1), this.maxSeconds);
|
||||||
}
|
}
|
||||||
sleep(seconds) {
|
sleep(seconds) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export class RetryHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sleep
|
// Sleep
|
||||||
const seconds = this.getSleepAmount()
|
const seconds = this.getSleepAmount(attempt)
|
||||||
core.info(`Waiting ${seconds} seconds before trying again`)
|
core.info(`Waiting ${seconds} seconds before trying again`)
|
||||||
await this.sleep(seconds)
|
await this.sleep(seconds)
|
||||||
attempt++
|
attempt++
|
||||||
@ -43,11 +43,12 @@ export class RetryHelper {
|
|||||||
return await action()
|
return await action()
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSleepAmount(): number {
|
private getSleepAmount(attempt: number): number {
|
||||||
return (
|
if (this.minSeconds === 0) {
|
||||||
Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) +
|
return 0
|
||||||
this.minSeconds
|
}
|
||||||
)
|
|
||||||
|
return Math.min(this.minSeconds * Math.pow(2, attempt - 1), this.maxSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sleep(seconds: number): Promise<void> {
|
private async sleep(seconds: number): Promise<void> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user