diff --git a/packages/cache/__tests__/cacheHttpClient.test.ts b/packages/cache/__tests__/cacheHttpClient.test.ts index c3e52ff6..e2201cd1 100644 --- a/packages/cache/__tests__/cacheHttpClient.test.ts +++ b/packages/cache/__tests__/cacheHttpClient.test.ts @@ -1,8 +1,8 @@ -import { downloadCache } from '../src/internal/cacheHttpClient' -import { getCacheVersion } from '../src/internal/cacheUtils' -import { CompressionMethod } from '../src/internal/constants' +import {downloadCache} from '../src/internal/cacheHttpClient' +import {getCacheVersion} from '../src/internal/cacheUtils' +import {CompressionMethod} from '../src/internal/constants' import * as downloadUtils from '../src/internal/downloadUtils' -import { DownloadOptions, getDownloadOptions } from '../src/options' +import {DownloadOptions, getDownloadOptions} from '../src/options' jest.mock('../src/internal/downloadUtils') @@ -129,7 +129,7 @@ test('downloadCache passes options to download methods', async () => { const archiveLocation = 'http://foo.blob.core.windows.net/bar/baz' const archivePath = '/foo/bar' - const options: DownloadOptions = { downloadConcurrency: 4 } + const options: DownloadOptions = {downloadConcurrency: 4} await downloadCache(archiveLocation, archivePath, options) diff --git a/packages/cache/src/cache.ts b/packages/cache/src/cache.ts index ab5ccb9e..5fba1e82 100644 --- a/packages/cache/src/cache.ts +++ b/packages/cache/src/cache.ts @@ -4,8 +4,8 @@ import * as config from './internal/config' import * as utils from './internal/cacheUtils' import * as cacheHttpClient from './internal/cacheHttpClient' import * as cacheTwirpClient from './internal/shared/cacheTwirpClient' -import { DownloadOptions, UploadOptions } from './options' -import { createTar, extractTar, listTar } from './internal/tar' +import {DownloadOptions, UploadOptions} from './options' +import {createTar, extractTar, listTar} from './internal/tar' import { CreateCacheEntryRequest, CreateCacheEntryResponse, @@ -14,9 +14,9 @@ import { GetCacheEntryDownloadURLRequest, GetCacheEntryDownloadURLResponse } from './generated/results/api/v1/cache' -import { CacheFileSizeLimit } from './internal/constants' -import { UploadCacheFile } from './internal/blob/upload-cache' -import { DownloadCacheFile } from './internal/blob/download-cache' +import {CacheFileSizeLimit} from './internal/constants' +import {UploadCacheFile} from './internal/blob/upload-cache' +import {DownloadCacheFile} from './internal/blob/download-cache' export class ValidationError extends Error { constructor(message: string) { super(message) @@ -86,23 +86,35 @@ export async function restoreCache( const cacheServiceVersion: string = config.getCacheServiceVersion() console.debug(`Cache service version: ${cacheServiceVersion}`) switch (cacheServiceVersion) { - case "v2": - return await restoreCachev2(paths, primaryKey, restoreKeys, options, enableCrossOsArchive) - case "v1": + case 'v2': + return await restoreCachev2( + paths, + primaryKey, + restoreKeys, + options, + enableCrossOsArchive + ) + case 'v1': default: - return await restoreCachev1(paths, primaryKey, restoreKeys, options, enableCrossOsArchive) + return await restoreCachev1( + paths, + primaryKey, + restoreKeys, + options, + enableCrossOsArchive + ) } } /** * Restores cache using the legacy Cache Service - * - * @param paths - * @param primaryKey - * @param restoreKeys - * @param options - * @param enableCrossOsArchive - * @returns + * + * @param paths + * @param primaryKey + * @param restoreKeys + * @param options + * @param enableCrossOsArchive + * @returns */ async function restoreCachev1( paths: string[], @@ -238,12 +250,15 @@ async function restoreCachev2( version: utils.getCacheVersion( paths, compressionMethod, - enableCrossOsArchive, - ), + enableCrossOsArchive + ) } - core.debug(`GetCacheEntryDownloadURLRequest: ${JSON.stringify(twirpClient)}`) - const response: GetCacheEntryDownloadURLResponse = await twirpClient.GetCacheEntryDownloadURL(request) + core.debug( + `GetCacheEntryDownloadURLRequest: ${JSON.stringify(twirpClient)}` + ) + const response: GetCacheEntryDownloadURLResponse = + await twirpClient.GetCacheEntryDownloadURL(request) core.debug(`GetCacheEntryDownloadURLResponse: ${JSON.stringify(response)}`) if (!response.ok) { @@ -266,10 +281,7 @@ async function restoreCachev2( core.debug(`Starting download of artifact to: ${archivePath}`) - await DownloadCacheFile( - response.signedDownloadUrl, - archivePath - ) + await DownloadCacheFile(response.signedDownloadUrl, archivePath) const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath) core.info( @@ -320,9 +332,9 @@ export async function saveCache( const cacheServiceVersion: string = config.getCacheServiceVersion() console.debug(`Cache Service Version: ${cacheServiceVersion}`) switch (cacheServiceVersion) { - case "v2": + case 'v2': return await saveCachev2(paths, key, options, enableCrossOsArchive) - case "v1": + case 'v1': default: return await saveCachev1(paths, key, options, enableCrossOsArchive) } @@ -330,12 +342,12 @@ export async function saveCache( /** * Save cache using the legacy Cache Service - * - * @param paths - * @param key - * @param options - * @param enableCrossOsArchive - * @returns + * + * @param paths + * @param key + * @param options + * @param enableCrossOsArchive + * @returns */ async function saveCachev1( paths: string[], @@ -398,9 +410,9 @@ async function saveCachev1( } else if (reserveCacheResponse?.statusCode === 400) { throw new Error( reserveCacheResponse?.error?.message ?? - `Cache size of ~${Math.round( - archiveFileSize / (1024 * 1024) - )} MB (${archiveFileSize} B) is over the data cap limit, not saving cache.` + `Cache size of ~${Math.round( + archiveFileSize / (1024 * 1024) + )} MB (${archiveFileSize} B) is over the data cap limit, not saving cache.` ) } else { throw new ReserveCacheError( @@ -433,12 +445,12 @@ async function saveCachev1( /** * Save cache using the new Cache Service - * - * @param paths - * @param key - * @param options - * @param enableCrossOsArchive - * @returns + * + * @param paths + * @param key + * @param options + * @param enableCrossOsArchive + * @returns */ async function saveCachev2( paths: string[], @@ -500,7 +512,8 @@ async function saveCachev2( key: key, version: version } - const response: CreateCacheEntryResponse = await twirpClient.CreateCacheEntry(request) + const response: CreateCacheEntryResponse = + await twirpClient.CreateCacheEntry(request) if (!response.ok) { throw new ReserveCacheError( `Unable to reserve cache with key ${key}, another job may be creating this cache.` @@ -508,21 +521,21 @@ async function saveCachev2( } core.debug(`Saving Cache to: ${core.setSecret(response.signedUploadUrl)}`) - await UploadCacheFile( - response.signedUploadUrl, - archivePath, - ) + await UploadCacheFile(response.signedUploadUrl, archivePath) const finalizeRequest: FinalizeCacheEntryUploadRequest = { workflowRunBackendId: backendIds.workflowRunBackendId, workflowJobRunBackendId: backendIds.workflowJobRunBackendId, key: key, version: version, - sizeBytes: `${archiveFileSize}`, + sizeBytes: `${archiveFileSize}` } - const finalizeResponse: FinalizeCacheEntryUploadResponse = await twirpClient.FinalizeCacheEntryUpload(finalizeRequest) - core.debug(`FinalizeCacheEntryUploadResponse: ${JSON.stringify(finalizeResponse)}`) + const finalizeResponse: FinalizeCacheEntryUploadResponse = + await twirpClient.FinalizeCacheEntryUpload(finalizeRequest) + core.debug( + `FinalizeCacheEntryUploadResponse: ${JSON.stringify(finalizeResponse)}` + ) if (!finalizeResponse.ok) { throw new Error( @@ -544,4 +557,4 @@ async function saveCachev2( } return cacheId -} \ No newline at end of file +} diff --git a/packages/cache/src/internal/blob/download-cache.ts b/packages/cache/src/internal/blob/download-cache.ts index 1820cb70..966d4974 100644 --- a/packages/cache/src/internal/blob/download-cache.ts +++ b/packages/cache/src/internal/blob/download-cache.ts @@ -3,15 +3,15 @@ import * as core from '@actions/core' import { BlobClient, BlockBlobClient, - BlobDownloadOptions, + BlobDownloadOptions } from '@azure/storage-blob' export async function DownloadCacheFile( signedUploadURL: string, - archivePath: string, + archivePath: string ): Promise<{}> { const downloadOptions: BlobDownloadOptions = { - maxRetryRequests: 5, + maxRetryRequests: 5 } // TODO: tighten the configuration and pass the appropriate user-agent @@ -21,5 +21,10 @@ export async function DownloadCacheFile( core.debug(`BlobClient: ${JSON.stringify(blobClient)}`) core.debug(`blockBlobClient: ${JSON.stringify(blockBlobClient)}`) - return blockBlobClient.downloadToFile(archivePath, 0, undefined, downloadOptions) -} \ No newline at end of file + return blockBlobClient.downloadToFile( + archivePath, + 0, + undefined, + downloadOptions + ) +} diff --git a/packages/cache/src/internal/blob/upload-cache.ts b/packages/cache/src/internal/blob/upload-cache.ts index e4572d20..5cd5cd6a 100644 --- a/packages/cache/src/internal/blob/upload-cache.ts +++ b/packages/cache/src/internal/blob/upload-cache.ts @@ -7,15 +7,15 @@ import { export async function UploadCacheFile( signedUploadURL: string, - archivePath: string, + archivePath: string ): Promise<{}> { // TODO: tighten the configuration and pass the appropriate user-agent // Specify data transfer options const uploadOptions: BlockBlobParallelUploadOptions = { blockSize: 4 * 1024 * 1024, // 4 MiB max block size concurrency: 4, // maximum number of parallel transfer workers - maxSingleShotSize: 8 * 1024 * 1024, // 8 MiB initial transfer size - }; + maxSingleShotSize: 8 * 1024 * 1024 // 8 MiB initial transfer size + } const blobClient: BlobClient = new BlobClient(signedUploadURL) const blockBlobClient: BlockBlobClient = blobClient.getBlockBlobClient() @@ -23,5 +23,5 @@ export async function UploadCacheFile( core.debug(`BlobClient: ${JSON.stringify(blobClient)}`) core.debug(`blockBlobClient: ${JSON.stringify(blockBlobClient)}`) - return blockBlobClient.uploadFile(archivePath, uploadOptions); -} \ No newline at end of file + return blockBlobClient.uploadFile(archivePath, uploadOptions) +} diff --git a/packages/cache/src/internal/cacheHttpClient.ts b/packages/cache/src/internal/cacheHttpClient.ts index 98d6a3bb..051348ec 100644 --- a/packages/cache/src/internal/cacheHttpClient.ts +++ b/packages/cache/src/internal/cacheHttpClient.ts @@ -1,12 +1,12 @@ import * as core from '@actions/core' -import { HttpClient } from '@actions/http-client' -import { BearerCredentialHandler } from '@actions/http-client/lib/auth' +import {HttpClient} from '@actions/http-client' +import {BearerCredentialHandler} from '@actions/http-client/lib/auth' import { RequestOptions, TypedResponse } from '@actions/http-client/lib/interfaces' import * as fs from 'fs' -import { URL } from 'url' +import {URL} from 'url' import * as utils from './cacheUtils' import { ArtifactCacheEntry, @@ -33,7 +33,7 @@ import { retryHttpClientResponse, retryTypedResponse } from './requestUtils' -import { getCacheServiceURL } from './config' +import {getCacheServiceURL} from './config' function getCacheApiUrl(resource: string): string { const baseUrl: string = getCacheServiceURL() @@ -216,7 +216,8 @@ async function uploadChunk( end: number ): Promise { core.debug( - `Uploading chunk of size ${end - start + 1 + `Uploading chunk of size ${ + end - start + 1 } bytes at offset ${start} with content range: ${getContentRange( start, end @@ -312,7 +313,7 @@ async function commitCache( cacheId: number, filesize: number ): Promise> { - const commitCacheRequest: CommitCacheRequest = { size: filesize } + const commitCacheRequest: CommitCacheRequest = {size: filesize} return await retryTypedResponse('commitCache', async () => httpClient.postJson( getCacheApiUrl(`caches/${cacheId.toString()}`), diff --git a/packages/cache/src/internal/cacheUtils.ts b/packages/cache/src/internal/cacheUtils.ts index ef09969b..a7548171 100644 --- a/packages/cache/src/internal/cacheUtils.ts +++ b/packages/cache/src/internal/cacheUtils.ts @@ -246,4 +246,4 @@ export function getBackendIdsFromToken(): BackendIds { } throw InvalidJwtError -} \ No newline at end of file +} diff --git a/packages/cache/src/internal/config.ts b/packages/cache/src/internal/config.ts index d980de14..61d84677 100644 --- a/packages/cache/src/internal/config.ts +++ b/packages/cache/src/internal/config.ts @@ -7,17 +7,21 @@ export function getRuntimeToken(): string { } export function getCacheServiceVersion(): string { - return process.env['ACTIONS_CACHE_SERVICE_V2'] ? 'v2' : 'v1'; + return process.env['ACTIONS_CACHE_SERVICE_V2'] ? 'v2' : 'v1' } export function getCacheServiceURL(): string { const version = getCacheServiceVersion() switch (version) { case 'v1': - return process.env['ACTIONS_CACHE_URL'] || process.env['ACTIONS_RESULTS_URL'] || "" + return ( + process.env['ACTIONS_CACHE_URL'] || + process.env['ACTIONS_RESULTS_URL'] || + '' + ) case 'v2': - return process.env['ACTIONS_RESULTS_URL'] || "" + return process.env['ACTIONS_RESULTS_URL'] || '' default: throw new Error(`Unsupported cache service version: ${version}`) } -} \ No newline at end of file +} diff --git a/packages/cache/src/internal/constants.ts b/packages/cache/src/internal/constants.ts index bc4e1d7a..8c5d1ee4 100644 --- a/packages/cache/src/internal/constants.ts +++ b/packages/cache/src/internal/constants.ts @@ -37,4 +37,4 @@ export const TarFilename = 'cache.tar' export const ManifestFilename = 'manifest.txt' -export const CacheFileSizeLimit = 10 * Math.pow(1024, 3) // 10GiB per repository \ No newline at end of file +export const CacheFileSizeLimit = 10 * Math.pow(1024, 3) // 10GiB per repository diff --git a/packages/cache/src/internal/shared/cacheTwirpClient.ts b/packages/cache/src/internal/shared/cacheTwirpClient.ts index 29bb845a..9a0f0679 100644 --- a/packages/cache/src/internal/shared/cacheTwirpClient.ts +++ b/packages/cache/src/internal/shared/cacheTwirpClient.ts @@ -1,202 +1,203 @@ -import { info, debug } from '@actions/core' -import { getUserAgentString } from './user-agent' -import { NetworkError, UsageError } from './errors' -import { getRuntimeToken, getCacheServiceURL } from '../config' -import { BearerCredentialHandler } from '@actions/http-client/lib/auth' -import { HttpClient, HttpClientResponse, HttpCodes } from '@actions/http-client' -import { CacheServiceClientJSON } from '../../generated/results/api/v1/cache.twirp' +import {info, debug} from '@actions/core' +import {getUserAgentString} from './user-agent' +import {NetworkError, UsageError} from './errors' +import {getRuntimeToken, getCacheServiceURL} from '../config' +import {BearerCredentialHandler} from '@actions/http-client/lib/auth' +import {HttpClient, HttpClientResponse, HttpCodes} from '@actions/http-client' +import {CacheServiceClientJSON} from '../../generated/results/api/v1/cache.twirp' // The twirp http client must implement this interface interface Rpc { - request( - service: string, - method: string, - contentType: 'application/json' | 'application/protobuf', - data: object | Uint8Array - ): Promise + request( + service: string, + method: string, + contentType: 'application/json' | 'application/protobuf', + data: object | Uint8Array + ): Promise } /** * This class is a wrapper around the CacheServiceClientJSON class generated by Twirp. - * + * * It adds retry logic to the request method, which is not present in the generated client. - * + * * This class is used to interact with cache service v2. */ class CacheServiceClient implements Rpc { - private httpClient: HttpClient - private baseUrl: string - private maxAttempts = 5 - private baseRetryIntervalMilliseconds = 3000 - private retryMultiplier = 1.5 + private httpClient: HttpClient + private baseUrl: string + private maxAttempts = 5 + private baseRetryIntervalMilliseconds = 3000 + private retryMultiplier = 1.5 - constructor( - userAgent: string, - maxAttempts?: number, - baseRetryIntervalMilliseconds?: number, - retryMultiplier?: number - ) { - const token = getRuntimeToken() - this.baseUrl = getCacheServiceURL() - if (maxAttempts) { - this.maxAttempts = maxAttempts - } - if (baseRetryIntervalMilliseconds) { - this.baseRetryIntervalMilliseconds = baseRetryIntervalMilliseconds - } - if (retryMultiplier) { - this.retryMultiplier = retryMultiplier - } - - this.httpClient = new HttpClient(userAgent, [ - new BearerCredentialHandler(token) - ]) + constructor( + userAgent: string, + maxAttempts?: number, + baseRetryIntervalMilliseconds?: number, + retryMultiplier?: number + ) { + const token = getRuntimeToken() + this.baseUrl = getCacheServiceURL() + if (maxAttempts) { + this.maxAttempts = maxAttempts + } + if (baseRetryIntervalMilliseconds) { + this.baseRetryIntervalMilliseconds = baseRetryIntervalMilliseconds + } + if (retryMultiplier) { + this.retryMultiplier = retryMultiplier } - // This function satisfies the Rpc interface. It is compatible with the JSON - // JSON generated client. - async request( - service: string, - method: string, - contentType: 'application/json' | 'application/protobuf', - data: object | Uint8Array - ): Promise { - const url = new URL(`/twirp/${service}/${method}`, this.baseUrl).href - debug(`[Request] ${method} ${url}`) - const headers = { - 'Content-Type': contentType - } - try { - const { body } = await this.retryableRequest(async () => - this.httpClient.post(url, JSON.stringify(data), headers) - ) + this.httpClient = new HttpClient(userAgent, [ + new BearerCredentialHandler(token) + ]) + } - return body - } catch (error) { - throw new Error(`Failed to ${method}: ${error.message}`) - } + // This function satisfies the Rpc interface. It is compatible with the JSON + // JSON generated client. + async request( + service: string, + method: string, + contentType: 'application/json' | 'application/protobuf', + data: object | Uint8Array + ): Promise { + const url = new URL(`/twirp/${service}/${method}`, this.baseUrl).href + debug(`[Request] ${method} ${url}`) + const headers = { + 'Content-Type': contentType } + try { + const {body} = await this.retryableRequest(async () => + this.httpClient.post(url, JSON.stringify(data), headers) + ) - async retryableRequest( - operation: () => Promise - ): Promise<{ response: HttpClientResponse; body: object }> { - let attempt = 0 - let errorMessage = '' - let rawBody = '' - while (attempt < this.maxAttempts) { - let isRetryable = false + return body + } catch (error) { + throw new Error(`Failed to ${method}: ${error.message}`) + } + } - try { - const response = await operation() - const statusCode = response.message.statusCode - rawBody = await response.readBody() - debug(`[Response] - ${response.message.statusCode}`) - debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`) - const body = JSON.parse(rawBody) - debug(`Body: ${JSON.stringify(body, null, 2)}`) - if (this.isSuccessStatusCode(statusCode)) { - return { response, body } - } - isRetryable = this.isRetryableHttpStatusCode(statusCode) - errorMessage = `Failed request: (${statusCode}) ${response.message.statusMessage}` - if (body.msg) { - if (UsageError.isUsageErrorMessage(body.msg)) { - throw new UsageError() - } + async retryableRequest( + operation: () => Promise + ): Promise<{response: HttpClientResponse; body: object}> { + let attempt = 0 + let errorMessage = '' + let rawBody = '' + while (attempt < this.maxAttempts) { + let isRetryable = false - errorMessage = `${errorMessage}: ${body.msg}` - } - } catch (error) { - if (error instanceof SyntaxError) { - debug(`Raw Body: ${rawBody}`) - } + try { + const response = await operation() + const statusCode = response.message.statusCode + rawBody = await response.readBody() + debug(`[Response] - ${response.message.statusCode}`) + debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`) + const body = JSON.parse(rawBody) + debug(`Body: ${JSON.stringify(body, null, 2)}`) + if (this.isSuccessStatusCode(statusCode)) { + return {response, body} + } + isRetryable = this.isRetryableHttpStatusCode(statusCode) + errorMessage = `Failed request: (${statusCode}) ${response.message.statusMessage}` + if (body.msg) { + if (UsageError.isUsageErrorMessage(body.msg)) { + throw new UsageError() + } - if (error instanceof UsageError) { - throw error - } - - if (NetworkError.isNetworkErrorCode(error?.code)) { - throw new NetworkError(error?.code) - } - - isRetryable = true - errorMessage = error.message - } - - if (!isRetryable) { - throw new Error(`Received non-retryable error: ${errorMessage}`) - } - - if (attempt + 1 === this.maxAttempts) { - throw new Error( - `Failed to make request after ${this.maxAttempts} attempts: ${errorMessage}` - ) - } - - const retryTimeMilliseconds = - this.getExponentialRetryTimeMilliseconds(attempt) - info( - `Attempt ${attempt + 1} of ${this.maxAttempts - } failed with error: ${errorMessage}. Retrying request in ${retryTimeMilliseconds} ms...` - ) - await this.sleep(retryTimeMilliseconds) - attempt++ + errorMessage = `${errorMessage}: ${body.msg}` + } + } catch (error) { + if (error instanceof SyntaxError) { + debug(`Raw Body: ${rawBody}`) } - throw new Error(`Request failed`) - } - - isSuccessStatusCode(statusCode?: number): boolean { - if (!statusCode) return false - return statusCode >= 200 && statusCode < 300 - } - - isRetryableHttpStatusCode(statusCode?: number): boolean { - if (!statusCode) return false - - const retryableStatusCodes = [ - HttpCodes.BadGateway, - HttpCodes.GatewayTimeout, - HttpCodes.InternalServerError, - HttpCodes.ServiceUnavailable, - HttpCodes.TooManyRequests - ] - - return retryableStatusCodes.includes(statusCode) - } - - async sleep(milliseconds: number): Promise { - return new Promise(resolve => setTimeout(resolve, milliseconds)) - } - - getExponentialRetryTimeMilliseconds(attempt: number): number { - if (attempt < 0) { - throw new Error('attempt should be a positive integer') + if (error instanceof UsageError) { + throw error } - if (attempt === 0) { - return this.baseRetryIntervalMilliseconds + if (NetworkError.isNetworkErrorCode(error?.code)) { + throw new NetworkError(error?.code) } - const minTime = - this.baseRetryIntervalMilliseconds * this.retryMultiplier ** attempt - const maxTime = minTime * this.retryMultiplier + isRetryable = true + errorMessage = error.message + } - // returns a random number between minTime and maxTime (exclusive) - return Math.trunc(Math.random() * (maxTime - minTime) + minTime) + if (!isRetryable) { + throw new Error(`Received non-retryable error: ${errorMessage}`) + } + + if (attempt + 1 === this.maxAttempts) { + throw new Error( + `Failed to make request after ${this.maxAttempts} attempts: ${errorMessage}` + ) + } + + const retryTimeMilliseconds = + this.getExponentialRetryTimeMilliseconds(attempt) + info( + `Attempt ${attempt + 1} of ${ + this.maxAttempts + } failed with error: ${errorMessage}. Retrying request in ${retryTimeMilliseconds} ms...` + ) + await this.sleep(retryTimeMilliseconds) + attempt++ } + + throw new Error(`Request failed`) + } + + isSuccessStatusCode(statusCode?: number): boolean { + if (!statusCode) return false + return statusCode >= 200 && statusCode < 300 + } + + isRetryableHttpStatusCode(statusCode?: number): boolean { + if (!statusCode) return false + + const retryableStatusCodes = [ + HttpCodes.BadGateway, + HttpCodes.GatewayTimeout, + HttpCodes.InternalServerError, + HttpCodes.ServiceUnavailable, + HttpCodes.TooManyRequests + ] + + return retryableStatusCodes.includes(statusCode) + } + + async sleep(milliseconds: number): Promise { + return new Promise(resolve => setTimeout(resolve, milliseconds)) + } + + getExponentialRetryTimeMilliseconds(attempt: number): number { + if (attempt < 0) { + throw new Error('attempt should be a positive integer') + } + + if (attempt === 0) { + return this.baseRetryIntervalMilliseconds + } + + const minTime = + this.baseRetryIntervalMilliseconds * this.retryMultiplier ** attempt + const maxTime = minTime * this.retryMultiplier + + // returns a random number between minTime and maxTime (exclusive) + return Math.trunc(Math.random() * (maxTime - minTime) + minTime) + } } export function internalCacheTwirpClient(options?: { - maxAttempts?: number - retryIntervalMs?: number - retryMultiplier?: number + maxAttempts?: number + retryIntervalMs?: number + retryMultiplier?: number }): CacheServiceClientJSON { - const client = new CacheServiceClient( - getUserAgentString(), - options?.maxAttempts, - options?.retryIntervalMs, - options?.retryMultiplier - ) - return new CacheServiceClientJSON(client) + const client = new CacheServiceClient( + getUserAgentString(), + options?.maxAttempts, + options?.retryIntervalMs, + options?.retryMultiplier + ) + return new CacheServiceClientJSON(client) } diff --git a/packages/cache/src/internal/shared/errors.ts b/packages/cache/src/internal/shared/errors.ts index 24c38e0d..9ec29f6b 100644 --- a/packages/cache/src/internal/shared/errors.ts +++ b/packages/cache/src/internal/shared/errors.ts @@ -1,72 +1,72 @@ export class FilesNotFoundError extends Error { - files: string[] + files: string[] - constructor(files: string[] = []) { - let message = 'No files were found to upload' - if (files.length > 0) { - message += `: ${files.join(', ')}` - } - - super(message) - this.files = files - this.name = 'FilesNotFoundError' + constructor(files: string[] = []) { + let message = 'No files were found to upload' + if (files.length > 0) { + message += `: ${files.join(', ')}` } + + super(message) + this.files = files + this.name = 'FilesNotFoundError' + } } export class InvalidResponseError extends Error { - constructor(message: string) { - super(message) - this.name = 'InvalidResponseError' - } + constructor(message: string) { + super(message) + this.name = 'InvalidResponseError' + } } export class CacheNotFoundError extends Error { - constructor(message = 'Cache not found') { - super(message) - this.name = 'CacheNotFoundError' - } + constructor(message = 'Cache not found') { + super(message) + this.name = 'CacheNotFoundError' + } } export class GHESNotSupportedError extends Error { - constructor( - message = '@actions/cache v4.1.4+, actions/cache/save@v4+ and actions/cache/restore@v4+ are not currently supported on GHES.' - ) { - super(message) - this.name = 'GHESNotSupportedError' - } + constructor( + message = '@actions/cache v4.1.4+, actions/cache/save@v4+ and actions/cache/restore@v4+ are not currently supported on GHES.' + ) { + super(message) + this.name = 'GHESNotSupportedError' + } } export class NetworkError extends Error { - code: string + code: string - constructor(code: string) { - const message = `Unable to make request: ${code}\nIf you are using self-hosted runners, please make sure your runner has access to all GitHub endpoints: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#communication-between-self-hosted-runners-and-github` - super(message) - this.code = code - this.name = 'NetworkError' - } + constructor(code: string) { + const message = `Unable to make request: ${code}\nIf you are using self-hosted runners, please make sure your runner has access to all GitHub endpoints: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#communication-between-self-hosted-runners-and-github` + super(message) + this.code = code + this.name = 'NetworkError' + } - static isNetworkErrorCode = (code?: string): boolean => { - if (!code) return false - return [ - 'ECONNRESET', - 'ENOTFOUND', - 'ETIMEDOUT', - 'ECONNREFUSED', - 'EHOSTUNREACH' - ].includes(code) - } + static isNetworkErrorCode = (code?: string): boolean => { + if (!code) return false + return [ + 'ECONNRESET', + 'ENOTFOUND', + 'ETIMEDOUT', + 'ECONNREFUSED', + 'EHOSTUNREACH' + ].includes(code) + } } export class UsageError extends Error { - constructor() { - const message = `Cache storage quota has been hit. Unable to upload any new cache entries. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending` - super(message) - this.name = 'UsageError' - } + constructor() { + const message = `Cache storage quota has been hit. Unable to upload any new cache entries. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending` + super(message) + this.name = 'UsageError' + } - static isUsageErrorMessage = (msg?: string): boolean => { - if (!msg) return false - return msg.includes('insufficient usage') - } + static isUsageErrorMessage = (msg?: string): boolean => { + if (!msg) return false + return msg.includes('insufficient usage') + } } diff --git a/packages/cache/src/internal/shared/user-agent.ts b/packages/cache/src/internal/shared/user-agent.ts index 1fcb15bd..9d88a659 100644 --- a/packages/cache/src/internal/shared/user-agent.ts +++ b/packages/cache/src/internal/shared/user-agent.ts @@ -5,5 +5,5 @@ const packageJson = require('../../../package.json') * Ensure that this User Agent String is used in all HTTP calls so that we can monitor telemetry between different versions of this package */ export function getUserAgentString(): string { - return `@actions/cache-${packageJson.version}` + return `@actions/cache-${packageJson.version}` }