1
0
Fork 0

only retry downloadtool on 500s and 408 and 429 (#373)

pull/375/head
eric sciple 2020-03-09 14:35:53 -04:00 committed by GitHub
parent 82fbe5da0f
commit 5859d7172e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 136 additions and 38 deletions

View File

@ -1,5 +1,9 @@
# @actions/tool-cache Releases # @actions/tool-cache Releases
### 1.3.3
- [Update downloadTool to only retry 500s and 408 and 429](https://github.com/actions/toolkit/pull/373)
### 1.3.2 ### 1.3.2
- [Update downloadTool with better error handling and retries](https://github.com/actions/toolkit/pull/369) - [Update downloadTool with better error handling and retries](https://github.com/actions/toolkit/pull/369)

View File

@ -66,7 +66,7 @@ describe('retry-helper tests', () => {
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) expect(info[3]).toMatch(/Waiting .+ seconds before trying again/)
}) })
it('all attempts fail succeeds', async () => { it('all attempts fail', async () => {
let attempts = 0 let attempts = 0
let error: Error = (null as unknown) as Error let error: Error = (null as unknown) as Error
try { try {
@ -84,4 +84,42 @@ describe('retry-helper tests', () => {
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]).toMatch(/Waiting .+ seconds before trying again/)
}) })
it('checks retryable after first attempt', async () => {
let attempts = 0
let error: Error = (null as unknown) as Error
try {
await retryHelper.execute(
async () => {
throw new Error(`some error ${++attempts}`)
},
() => false
)
} catch (err) {
error = err
}
expect(error.message).toBe('some error 1')
expect(attempts).toBe(1)
expect(info).toHaveLength(0)
})
it('checks retryable after second attempt', async () => {
let attempts = 0
let error: Error = (null as unknown) as Error
try {
await retryHelper.execute(
async () => {
throw new Error(`some error ${++attempts}`)
},
(e: Error) => e.message === 'some error 1'
)
} catch (err) {
error = err
}
expect(error.message).toBe('some error 2')
expect(attempts).toBe(2)
expect(info).toHaveLength(2)
expect(info[0]).toBe('some error 1')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
})
}) })

View File

@ -619,6 +619,39 @@ describe('@actions/tool-cache', function() {
expect(err.toString()).toContain('502') expect(err.toString()).toContain('502')
} }
}) })
it('retries 429s', async function() {
nock('http://example.com')
.get('/too-many-requests-429')
.times(2)
.reply(429, undefined)
nock('http://example.com')
.get('/too-many-requests-429')
.reply(500, undefined)
try {
const statusCodeUrl = 'http://example.com/too-many-requests-429'
await tc.downloadTool(statusCodeUrl)
} catch (err) {
expect(err.toString()).toContain('500')
}
})
it("doesn't retry 404", async function() {
nock('http://example.com')
.get('/not-found-404')
.reply(404, undefined)
nock('http://example.com')
.get('/not-found-404')
.reply(500, undefined)
try {
const statusCodeUrl = 'http://example.com/not-found-404'
await tc.downloadTool(statusCodeUrl)
} catch (err) {
expect(err.toString()).toContain('404')
}
})
}) })
/** /**

View File

@ -1,6 +1,6 @@
{ {
"name": "@actions/tool-cache", "name": "@actions/tool-cache",
"version": "1.3.2", "version": "1.3.3",
"description": "Actions tool-cache lib", "description": "Actions tool-cache lib",
"keywords": [ "keywords": [
"github", "github",

View File

@ -21,13 +21,20 @@ export class RetryHelper {
} }
} }
async execute<T>(action: () => Promise<T>): Promise<T> { async execute<T>(
action: () => Promise<T>,
isRetryable?: (e: Error) => boolean
): Promise<T> {
let attempt = 1 let attempt = 1
while (attempt < this.maxAttempts) { while (attempt < this.maxAttempts) {
// Try // Try
try { try {
return await action() return await action()
} catch (err) { } catch (err) {
if (isRetryable && !isRetryable(err)) {
throw err
}
core.info(err.message) core.info(err.message)
} }

View File

@ -23,30 +23,6 @@ export class HTTPError extends Error {
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
const userAgent = 'actions/tool-cache' const userAgent = 'actions/tool-cache'
// On load grab temp directory and cache directory and remove them from env (currently don't want to expose this)
let tempDirectory: string = process.env['RUNNER_TEMP'] || ''
let cacheRoot: string = process.env['RUNNER_TOOL_CACHE'] || ''
// If directories not found, place them in common temp locations
if (!tempDirectory || !cacheRoot) {
let baseLocation: string
if (IS_WINDOWS) {
// On windows use the USERPROFILE env variable
baseLocation = process.env['USERPROFILE'] || 'C:\\'
} else {
if (process.platform === 'darwin') {
baseLocation = '/Users'
} else {
baseLocation = '/home'
}
}
if (!tempDirectory) {
tempDirectory = path.join(baseLocation, 'actions', 'temp')
}
if (!cacheRoot) {
cacheRoot = path.join(baseLocation, 'actions', 'cache')
}
}
/** /**
* Download a tool from an url and stream it into a file * Download a tool from an url and stream it into a file
* *
@ -58,23 +34,40 @@ export async function downloadTool(
url: string, url: string,
dest?: string dest?: string
): Promise<string> { ): Promise<string> {
dest = dest || path.join(tempDirectory, uuidV4()) dest = dest || path.join(_getTempDirectory(), uuidV4())
await io.mkdirP(path.dirname(dest)) await io.mkdirP(path.dirname(dest))
core.debug(`Downloading ${url}`) core.debug(`Downloading ${url}`)
core.debug(`Destination ${dest}`) core.debug(`Destination ${dest}`)
const maxAttempts = 3 const maxAttempts = 3
const minSeconds = getGlobal<number>( const minSeconds = _getGlobal<number>(
'TEST_DOWNLOAD_TOOL_RETRY_MIN_SECONDS', 'TEST_DOWNLOAD_TOOL_RETRY_MIN_SECONDS',
10 10
) )
const maxSeconds = getGlobal<number>( const maxSeconds = _getGlobal<number>(
'TEST_DOWNLOAD_TOOL_RETRY_MAX_SECONDS', 'TEST_DOWNLOAD_TOOL_RETRY_MAX_SECONDS',
20 20
) )
const retryHelper = new RetryHelper(maxAttempts, minSeconds, maxSeconds) const retryHelper = new RetryHelper(maxAttempts, minSeconds, maxSeconds)
return await retryHelper.execute( return await retryHelper.execute(
async () => await downloadToolAttempt(url, dest || '') async () => {
return await downloadToolAttempt(url, dest || '')
},
(err: Error) => {
if (err instanceof HTTPError && err.httpStatusCode) {
// Don't retry anything less than 500, except 408 Request Timeout and 429 Too Many Requests
if (
err.httpStatusCode < 500 &&
err.httpStatusCode !== 408 &&
err.httpStatusCode !== 429
) {
return false
}
}
// Otherwise retry
return true
}
) )
} }
@ -98,7 +91,7 @@ async function downloadToolAttempt(url: string, dest: string): Promise<string> {
// Download the response body // Download the response body
const pipeline = util.promisify(stream.pipeline) const pipeline = util.promisify(stream.pipeline)
const responseMessageFactory = getGlobal<() => stream.Readable>( const responseMessageFactory = _getGlobal<() => stream.Readable>(
'TEST_DOWNLOAD_TOOL_RESPONSE_MESSAGE_FACTORY', 'TEST_DOWNLOAD_TOOL_RESPONSE_MESSAGE_FACTORY',
() => response.message () => response.message
) )
@ -417,7 +410,12 @@ export function find(
let toolPath = '' let toolPath = ''
if (versionSpec) { if (versionSpec) {
versionSpec = semver.clean(versionSpec) || '' versionSpec = semver.clean(versionSpec) || ''
const cachePath = path.join(cacheRoot, toolName, versionSpec, arch) const cachePath = path.join(
_getCacheDirectory(),
toolName,
versionSpec,
arch
)
core.debug(`checking cache: ${cachePath}`) core.debug(`checking cache: ${cachePath}`)
if (fs.existsSync(cachePath) && fs.existsSync(`${cachePath}.complete`)) { if (fs.existsSync(cachePath) && fs.existsSync(`${cachePath}.complete`)) {
core.debug(`Found tool in cache ${toolName} ${versionSpec} ${arch}`) core.debug(`Found tool in cache ${toolName} ${versionSpec} ${arch}`)
@ -439,7 +437,7 @@ export function findAllVersions(toolName: string, arch?: string): string[] {
const versions: string[] = [] const versions: string[] = []
arch = arch || os.arch() arch = arch || os.arch()
const toolPath = path.join(cacheRoot, toolName) const toolPath = path.join(_getCacheDirectory(), toolName)
if (fs.existsSync(toolPath)) { if (fs.existsSync(toolPath)) {
const children: string[] = fs.readdirSync(toolPath) const children: string[] = fs.readdirSync(toolPath)
@ -459,7 +457,7 @@ export function findAllVersions(toolName: string, arch?: string): string[] {
async function _createExtractFolder(dest?: string): Promise<string> { async function _createExtractFolder(dest?: string): Promise<string> {
if (!dest) { if (!dest) {
// create a temp dir // create a temp dir
dest = path.join(tempDirectory, uuidV4()) dest = path.join(_getTempDirectory(), uuidV4())
} }
await io.mkdirP(dest) await io.mkdirP(dest)
return dest return dest
@ -471,7 +469,7 @@ async function _createToolPath(
arch?: string arch?: string
): Promise<string> { ): Promise<string> {
const folderPath = path.join( const folderPath = path.join(
cacheRoot, _getCacheDirectory(),
tool, tool,
semver.clean(version) || version, semver.clean(version) || version,
arch || '' arch || ''
@ -486,7 +484,7 @@ async function _createToolPath(
function _completeToolPath(tool: string, version: string, arch?: string): void { function _completeToolPath(tool: string, version: string, arch?: string): void {
const folderPath = path.join( const folderPath = path.join(
cacheRoot, _getCacheDirectory(),
tool, tool,
semver.clean(version) || version, semver.clean(version) || version,
arch || '' arch || ''
@ -533,10 +531,28 @@ function _evaluateVersions(versions: string[], versionSpec: string): string {
return version return version
} }
/**
* Gets RUNNER_TOOL_CACHE
*/
function _getCacheDirectory(): string {
const cacheDirectory = process.env['RUNNER_TOOL_CACHE'] || ''
ok(cacheDirectory, 'Expected RUNNER_TOOL_CACHE to be defined')
return cacheDirectory
}
/**
* Gets RUNNER_TEMP
*/
function _getTempDirectory(): string {
const tempDirectory = process.env['RUNNER_TEMP'] || ''
ok(tempDirectory, 'Expected RUNNER_TEMP to be defined')
return tempDirectory
}
/** /**
* Gets a global variable * Gets a global variable
*/ */
function getGlobal<T>(key: string, defaultValue: T): T { function _getGlobal<T>(key: string, defaultValue: T): T {
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
const value = (global as any)[key] as T | undefined const value = (global as any)[key] as T | undefined
/* eslint-enable @typescript-eslint/no-explicit-any */ /* eslint-enable @typescript-eslint/no-explicit-any */