Files
toolkit/packages/cache/src/internal/cacheTwirpClient.ts
T

196 lines
6.6 KiB
TypeScript
Raw Normal View History

2024-09-24 03:17:44 -07:00
import { HttpClient, HttpClientResponse, HttpCodes } from '@actions/http-client'
import { BearerCredentialHandler } from '@actions/http-client/lib/auth'
import { info, debug } from '@actions/core'
import { CacheServiceClientJSON } from '../generated/results/api/v1/cache.twirp'
2024-09-24 03:29:14 -07:00
import { getRuntimeToken, getCacheServiceURL } from './config'
2024-05-29 08:31:54 -07:00
// import {getUserAgentString} from './user-agent'
// import {NetworkError, UsageError} from './errors'
// The twirp http client must implement this interface
interface Rpc {
2024-09-24 03:17:44 -07:00
request(
service: string,
method: string,
contentType: 'application/json' | 'application/protobuf',
data: object | Uint8Array
): Promise<object | Uint8Array>
2024-05-29 08:31:54 -07:00
}
2024-09-24 03:17:44 -07:00
class CacheServiceClient implements Rpc {
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()
2024-09-24 03:29:14 -07:00
this.baseUrl = getCacheServiceURL()
2024-09-24 03:17:44 -07:00
if (maxAttempts) {
this.maxAttempts = maxAttempts
2024-05-29 08:31:54 -07:00
}
2024-09-24 03:17:44 -07:00
if (baseRetryIntervalMilliseconds) {
this.baseRetryIntervalMilliseconds = baseRetryIntervalMilliseconds
2024-05-29 08:31:54 -07:00
}
2024-09-24 03:17:44 -07:00
if (retryMultiplier) {
this.retryMultiplier = retryMultiplier
2024-05-29 08:31:54 -07:00
}
2024-09-24 03:17:44 -07:00
this.httpClient = new HttpClient(userAgent, [
new BearerCredentialHandler(token)
])
2024-05-29 08:31:54 -07:00
}
2024-09-24 03:17:44 -07:00
// 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<object | Uint8Array> {
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)
)
return body
} catch (error) {
throw new Error(`Failed to ${method}: ${error.message}`)
}
}
2024-05-29 08:31:54 -07:00
2024-09-24 03:17:44 -07:00
async retryableRequest(
operation: () => Promise<HttpClientResponse>
): Promise<{ response: HttpClientResponse; body: object }> {
let attempt = 0
let errorMessage = ''
let rawBody = ''
while (attempt < this.maxAttempts) {
let isRetryable = false
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()
// }
errorMessage = `${errorMessage}: ${body.msg}`
}
} catch (error) {
if (error instanceof SyntaxError) {
debug(`Raw Body: ${rawBody}`)
}
// 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++
}
2024-05-29 08:31:54 -07:00
2024-09-24 03:17:44 -07:00
throw new Error(`Request failed`)
}
2024-05-29 08:31:54 -07:00
2024-09-24 03:17:44 -07:00
isSuccessStatusCode(statusCode?: number): boolean {
if (!statusCode) return false
return statusCode >= 200 && statusCode < 300
}
2024-05-29 08:31:54 -07:00
2024-09-24 03:17:44 -07:00
isRetryableHttpStatusCode(statusCode?: number): boolean {
if (!statusCode) return false
2024-05-29 08:31:54 -07:00
2024-09-24 03:17:44 -07:00
const retryableStatusCodes = [
HttpCodes.BadGateway,
HttpCodes.GatewayTimeout,
HttpCodes.InternalServerError,
HttpCodes.ServiceUnavailable,
HttpCodes.TooManyRequests
]
2024-05-29 08:31:54 -07:00
2024-09-24 03:17:44 -07:00
return retryableStatusCodes.includes(statusCode)
2024-05-29 08:31:54 -07:00
}
2024-09-24 03:17:44 -07:00
async sleep(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds))
2024-05-29 08:31:54 -07:00
}
2024-09-24 03:17:44 -07:00
getExponentialRetryTimeMilliseconds(attempt: number): number {
if (attempt < 0) {
throw new Error('attempt should be a positive integer')
}
2024-05-29 08:31:54 -07:00
2024-09-24 03:17:44 -07:00
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)
}
2024-05-29 08:31:54 -07:00
}
2024-09-24 03:17:44 -07:00
export function internalCacheTwirpClient(options?: {
maxAttempts?: number
retryIntervalMs?: number
retryMultiplier?: number
}): CacheServiceClientJSON {
const client = new CacheServiceClient(
'actions/cache',
options?.maxAttempts,
options?.retryIntervalMs,
options?.retryMultiplier
)
return new CacheServiceClientJSON(client)
2024-05-29 08:31:54 -07:00
}