mirror of https://github.com/actions/toolkit
Add twirp client with retry logic
parent
39a7ba7bbd
commit
898dd8c2a1
|
@ -10,6 +10,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
|
"@actions/http-client": "^2.1.0",
|
||||||
"@azure/storage-blob": "^12.15.0",
|
"@azure/storage-blob": "^12.15.0",
|
||||||
"@types/node": "^20.4.5",
|
"@types/node": "^20.4.5",
|
||||||
"archiver": "^5.3.1"
|
"archiver": "^5.3.1"
|
||||||
|
@ -31,9 +32,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/http-client": {
|
"node_modules/@actions/http-client": {
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.0.tgz",
|
||||||
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
"integrity": "sha512-BonhODnXr3amchh4qkmjPMUO8mFi/zLaaCeCAJZqch8iQqyDnVIkySjB38VHAC8IJ+bnlgfOqlhpyCUZHlQsqw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tunnel": "^0.0.6"
|
"tunnel": "^0.0.6"
|
||||||
}
|
}
|
||||||
|
@ -1070,9 +1071,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@actions/http-client": {
|
"@actions/http-client": {
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.0.tgz",
|
||||||
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
"integrity": "sha512-BonhODnXr3amchh4qkmjPMUO8mFi/zLaaCeCAJZqch8iQqyDnVIkySjB38VHAC8IJ+bnlgfOqlhpyCUZHlQsqw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tunnel": "^0.0.6"
|
"tunnel": "^0.0.6"
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
|
"@actions/http-client": "^2.1.0",
|
||||||
"@azure/storage-blob": "^12.15.0",
|
"@azure/storage-blob": "^12.15.0",
|
||||||
"@types/node": "^20.4.5",
|
"@types/node": "^20.4.5",
|
||||||
"archiver": "^5.3.1"
|
"archiver": "^5.3.1"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// @generated by protobuf-ts 2.2.3-alpha.1 with parameter client_none,generate_dependencies
|
// @generated by protobuf-ts 2.9.1 with parameter client_none,generate_dependencies
|
||||||
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
||||||
// tslint:disable
|
// tslint:disable
|
||||||
//
|
//
|
|
@ -1,4 +1,4 @@
|
||||||
// @generated by protobuf-ts 2.2.3-alpha.1 with parameter client_none,generate_dependencies
|
// @generated by protobuf-ts 2.9.1 with parameter client_none,generate_dependencies
|
||||||
// @generated from protobuf file "google/protobuf/wrappers.proto" (package "google.protobuf", syntax proto3)
|
// @generated from protobuf file "google/protobuf/wrappers.proto" (package "google.protobuf", syntax proto3)
|
||||||
// tslint:disable
|
// tslint:disable
|
||||||
//
|
//
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from "../generated/google/protobuf/timestamp";
|
||||||
|
export * from "../generated/google/protobuf/wrappers";
|
||||||
|
export * from "../generated/results/api/v1/artifact";
|
||||||
|
export * from "../generated/results/api/v1/artifact.twirp";
|
|
@ -1,4 +1,4 @@
|
||||||
// @generated by protobuf-ts 2.2.3-alpha.1 with parameter client_none,generate_dependencies
|
// @generated by protobuf-ts 2.9.1 with parameter client_none,generate_dependencies
|
||||||
// @generated from protobuf file "results/api/v1/artifact.proto" (package "github.actions.results.api.v1", syntax proto3)
|
// @generated from protobuf file "results/api/v1/artifact.proto" (package "github.actions.results.api.v1", syntax proto3)
|
||||||
// tslint:disable
|
// tslint:disable
|
||||||
import { ServiceType } from "@protobuf-ts/runtime-rpc";
|
import { ServiceType } from "@protobuf-ts/runtime-rpc";
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { HttpCodes, HttpClient, HttpClientResponse } from '@actions/http-client'
|
||||||
|
import { BearerCredentialHandler } from '@actions/http-client/lib/auth'
|
||||||
|
import { info } from '@actions/core'
|
||||||
|
import { getRuntimeToken, getResultsServiceUrl, getRetryMultiplier, getInitialRetryIntervalInMilliseconds, getRetryLimit } from './config'
|
||||||
|
|
||||||
|
interface Rpc { request(
|
||||||
|
service: string,
|
||||||
|
method: string,
|
||||||
|
contentType: "application/json" | "application/protobuf",
|
||||||
|
data: object | Uint8Array
|
||||||
|
): Promise<object | Uint8Array>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArtifactHttpClient implements Rpc {
|
||||||
|
private httpClient: HttpClient
|
||||||
|
private baseUrl: string
|
||||||
|
|
||||||
|
constructor(userAgent: string) {
|
||||||
|
this.httpClient = new HttpClient(userAgent, [
|
||||||
|
new BearerCredentialHandler(getRuntimeToken())
|
||||||
|
])
|
||||||
|
this.baseUrl = getResultsServiceUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 resp = await this.retry(
|
||||||
|
`${method}`,
|
||||||
|
this.httpClient.post(url, JSON.stringify(data), headers),
|
||||||
|
)
|
||||||
|
const body = await resp.readBody()
|
||||||
|
return JSON.parse(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
async retry(name: string, operation: Promise<HttpClientResponse>): Promise<HttpClientResponse> {
|
||||||
|
let response: HttpClientResponse | undefined = undefined
|
||||||
|
let statusCode: number | undefined = undefined
|
||||||
|
let isRetryable = false
|
||||||
|
let errorMessage = ''
|
||||||
|
let attempt = 1
|
||||||
|
const maxAttempts = getRetryLimit()
|
||||||
|
|
||||||
|
while (attempt <= maxAttempts) {
|
||||||
|
try {
|
||||||
|
response = await operation
|
||||||
|
statusCode = response.message.statusCode
|
||||||
|
|
||||||
|
if (this.isSuccessStatusCode(statusCode)) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
isRetryable = this.isRetryableStatusCode(statusCode)
|
||||||
|
errorMessage = `Artifact service responded with ${statusCode}`
|
||||||
|
} catch (error) {
|
||||||
|
isRetryable = true
|
||||||
|
errorMessage = error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRetryable) {
|
||||||
|
info(`${name} - Error is not retryable`)
|
||||||
|
if (response) {
|
||||||
|
this.displayHttpDiagnostics(response)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
info(
|
||||||
|
`${name} - Attempt ${attempt} of ${maxAttempts} failed with error: ${errorMessage}`
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.sleep(this.getExponentialRetryTimeInMilliseconds(attempt))
|
||||||
|
attempt++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
this.displayHttpDiagnostics(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Error(`${name} failed: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
isSuccessStatusCode(statusCode?: number): boolean {
|
||||||
|
if (!statusCode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return statusCode >= 200 && statusCode < 300
|
||||||
|
}
|
||||||
|
|
||||||
|
isRetryableStatusCode(statusCode: number | undefined): 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
displayHttpDiagnostics(response: HttpClientResponse): void {
|
||||||
|
info(
|
||||||
|
`##### Begin Diagnostic HTTP information #####
|
||||||
|
Status Code: ${response.message.statusCode}
|
||||||
|
Status Message: ${response.message.statusMessage}
|
||||||
|
Header Information: ${JSON.stringify(response.message.headers, undefined, 2)}
|
||||||
|
###### End Diagnostic HTTP information ######`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getExponentialRetryTimeInMilliseconds(
|
||||||
|
retryCount: number
|
||||||
|
): number {
|
||||||
|
if (retryCount < 0) {
|
||||||
|
throw new Error('RetryCount should not be negative')
|
||||||
|
} else if (retryCount === 0) {
|
||||||
|
return getInitialRetryIntervalInMilliseconds()
|
||||||
|
}
|
||||||
|
|
||||||
|
const minTime =
|
||||||
|
getInitialRetryIntervalInMilliseconds() * getRetryMultiplier() * retryCount
|
||||||
|
const maxTime = minTime * getRetryMultiplier()
|
||||||
|
|
||||||
|
// returns a random number between the minTime (inclusive) and the maxTime (exclusive)
|
||||||
|
return Math.trunc(Math.random() * (maxTime - minTime) + minTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
async sleep(milliseconds: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,4 +32,19 @@ export function getWorkSpaceDirectory(): string {
|
||||||
|
|
||||||
export function getRetentionDays(): string | undefined {
|
export function getRetentionDays(): string | undefined {
|
||||||
return process.env['GITHUB_RETENTION_DAYS']
|
return process.env['GITHUB_RETENTION_DAYS']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInitialRetryIntervalInMilliseconds(): number {
|
||||||
|
return 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
// With exponential backoff, the larger the retry count, the larger the wait time before another attempt
|
||||||
|
// The retry multiplier controls by how much the backOff time increases depending on the number of retries
|
||||||
|
export function getRetryMultiplier(): number {
|
||||||
|
return 1.5
|
||||||
|
}
|
||||||
|
|
||||||
|
// The maximum number of retries that can be attempted before an upload or download fails
|
||||||
|
export function getRetryLimit(): number {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { ArtifactHttpClient } from '../../artifact-http-client'
|
||||||
|
import { ArtifactServiceClientJSON } from '../../../generated'
|
||||||
|
|
||||||
|
export async function twirpTest(){
|
||||||
|
const artifactClient = new ArtifactHttpClient('@actions/artifact-upload')
|
||||||
|
const jsonClient = new ArtifactServiceClientJSON(artifactClient)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createResp = await jsonClient.CreateArtifact({workflowRunBackendId: "ce7f54c7-61c7-4aae-887f-30da475f5f1a", workflowJobRunBackendId: "ca395085-040a-526b-2ce8-bdc85f692774", name: Math.random().toString(), version: 4})
|
||||||
|
|
||||||
|
if (!createResp.ok) {
|
||||||
|
console.log("CreateArtifact failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(createResp.signedUploadUrl)
|
||||||
|
|
||||||
|
const finalizeResp = await jsonClient.FinalizeArtifact({workflowRunBackendId: "ce7f54c7-61c7-4aae-887f-30da475f5f1a", workflowJobRunBackendId: "ca395085-040a-526b-2ce8-bdc85f692774", name: Math.random().toString(), size: BigInt(5)})
|
||||||
|
|
||||||
|
if (!finalizeResp.ok) {
|
||||||
|
console.log("FinalizeArtifact failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("FinalizeArtifact succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
twirpTest()
|
|
@ -3,9 +3,14 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"outDir": "./lib",
|
"outDir": "./lib",
|
||||||
"rootDir": "./src"
|
"rootDir": "./src",
|
||||||
|
"lib": [
|
||||||
|
"es2020"
|
||||||
|
],
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2020"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src"
|
"./src"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue