1
0
Fork 0

add twirp client

pull/1486/head
Bethany 2023-08-04 09:23:14 -07:00
parent 3ebee1e8b4
commit 8a5343d54a
2 changed files with 148 additions and 0 deletions

View File

@ -0,0 +1,133 @@
import { HttpClient, HttpClientResponse, HttpCodes } from "@actions/http-client"
import { BearerCredentialHandler } from "@actions/http-client/lib/auth"
import { info } from "@actions/core"
import { ArtifactServiceClientJSON } from "src/generated"
import { getResultsServiceUrl, getRuntimeToken } from "./config"
// The twirp http client must implement this interface
interface Rpc {
request(
service: string,
method: string,
contentType: "application/json" | "application/protobuf",
data: object | Uint8Array
): Promise<object | Uint8Array>
}
class ArtifactHttpClient implements Rpc {
private httpClient: HttpClient
private baseUrl: string
private maxAttempts: number = 5
private baseRetryIntervalMilliseconds: number = 3000
private retryMultiplier: number = 1.5
constructor(userAgent: string) {
const token = getRuntimeToken()
this.httpClient = new HttpClient(
userAgent,
[new BearerCredentialHandler(token)],
)
this.baseUrl = getResultsServiceUrl()
}
// 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> {
let url = `${this.baseUrl}/twirp/${service}/${method}`
let headers = {
"Content-Type": contentType,
}
const response = await this.retryableRequest(this.httpClient.post(url, JSON.stringify(data), headers))
const body = await response.readBody()
return JSON.parse(body)
}
async retryableRequest(
operation: Promise<HttpClientResponse>
): Promise<HttpClientResponse> {
let attempt = 0
while (attempt < this.maxAttempts) {
let isRetryable = false
let errorMessage = ""
try {
const response = await operation
const statusCode = response.message.statusCode
if (this.isSuccessStatusCode(statusCode)) {
return response
}
isRetryable = this.isRetryableHttpStatusCode(statusCode)
errorMessage = `Failed request: (${statusCode}) ${response.message.statusMessage}`
} catch (error) {
isRetryable = true
errorMessage = error.message
}
if (!isRetryable) {
throw new Error(errorMessage)
}
info(
`Attempt ${attempt + 1} of ${this.maxAttempts} failed with error: ${errorMessage}. Retrying request...`
)
await this.sleep(this.getExponentialRetryTimeMilliseconds(attempt))
attempt++
}
throw new Error(`Failed to make request after ${this.maxAttempts} attempts`)
}
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,
413 // Payload Too Large
]
return retryableStatusCodes.includes(statusCode)
}
async sleep(milliseconds: number): Promise<void> {
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 createArtifactTwirpClient(type: "upload" | "download"): ArtifactServiceClientJSON {
const client = new ArtifactHttpClient(`@actions/artifact-${type}`)
return new ArtifactServiceClientJSON(client)
}

View File

@ -0,0 +1,15 @@
export function getRuntimeToken(): string {
const token = process.env['ACTIONS_RUNTIME_TOKEN']
if (!token) {
throw new Error('Unable to get ACTIONS_RUNTIME_TOKEN env variable')
}
return token
}
export function getResultsServiceUrl(): string {
const resultsUrl = process.env['ACTIONS_RESULTS_URL']
if (!resultsUrl) {
throw new Error('Unable to get ACTIONS_RESULTS_URL env variable')
}
return resultsUrl
}