Repository navigation
Expand file tree
/
Copy pathdaily_aic_api_budget.cjs
More file actions
138 lines (127 loc) · 6.33 KB
/
Copy pathdaily_aic_api_budget.cjs
File metadata and controls
138 lines (127 loc) · 6.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// @ts-check
const { getGitHubAPIQuota, getGitHubAPIDiagnostic } = require("./github_api_diagnostics.cjs");
const SAFE_HEADERS = ["x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-used", "x-ratelimit-reset", "x-ratelimit-resource", "retry-after"];
const API_READ_OPERATIONS = Object.freeze({
CURRENT_RUN: "current_run",
WORKFLOW_HISTORY: "workflow_history",
REPOSITORY_HISTORY: "repository_history",
RUN_JOBS: "run_jobs",
ARTIFACT_LIST: "artifact_list",
ARTIFACT_REDIRECT: "artifact_redirect",
ARTIFACT_DOWNLOAD: "artifact_download",
});
const READ_OPERATIONS = new Set(Object.values(API_READ_OPERATIONS));
const READ_FAILURES = new WeakMap();
function getAPIReadFailure(error) {
// Artifact transport wrappers retain the original failure as their cause.
for (let depth = 0; depth < 5 && error && typeof error === "object"; depth++, error = error.cause) {
const failure = READ_FAILURES.get(error);
if (failure) return failure;
}
return null;
}
function safeResponseHeaders(headers) {
return Object.fromEntries(
SAFE_HEADERS.flatMap(name => {
const value = typeof headers?.get === "function" ? headers.get(name) : headers?.[name];
return value == null ? [] : [[name, String(value)]];
})
);
}
function apiError(status, headers, message) {
const error = new Error(message);
return Object.assign(error, { status, response: { status, headers: safeResponseHeaders(headers) } });
}
function retryNotBefore(headers, now = Date.now(), quotaStopped = false) {
return getGitHubAPIQuota(headers, now, quotaStopped).retryNotBefore;
}
function getErrorStatus(error) {
return Number(error?.status || error?.response?.status || 0);
}
function isQuotaError(error) {
const headers = safeResponseHeaders(error?.response?.headers);
const status = getErrorStatus(error);
return error?.code === "DAILY_AIC_QUOTA_STOP" || status === 429 || (status === 403 && (headers["x-ratelimit-remaining"] === "0" || headers["retry-after"] != null)) || getGitHubAPIDiagnostic(error)?.quota === true;
}
function isTransientError(error) {
if (isQuotaError(error) || safeResponseHeaders(error?.response?.headers)["retry-after"] != null) return false;
const status = getErrorStatus(error);
if (status >= 400) return status >= 500 && status < 600;
const codes = ["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "UND_ERR_SOCKET"];
return ["TimeoutError", "AbortError"].includes(error?.name) || codes.includes(error?.code) || (!!error?.cause && isTransientError(error.cause));
}
/**
* Observe business responses, including native-fetch artifact requests. Do not use
* /rate_limit as the authority for a different request's quota or retry deadline.
* Retry transient reads only; shared quota and server retry restrictions stop immediately.
*/
function createAPIBudget(reserve = 100) {
let requests = 0;
let latest = {};
const budget = {
observe(response) {
requests++;
latest = safeResponseHeaders(response.headers);
const remaining = latest["x-ratelimit-remaining"];
const limited = response.status === 429 || (response.status === 403 && (remaining === "0" || latest["retry-after"] != null));
if (limited || (remaining != null && Number(remaining) <= reserve)) {
const error = Object.assign(apiError(response.status, latest, "Daily AIC inspection stopped to preserve GitHub API quota"), { code: "DAILY_AIC_QUOTA_STOP" });
const diagnostic = getGitHubAPIDiagnostic(error);
if (diagnostic) error.message += `: ${diagnostic.message}`;
throw error;
}
},
async request(operation, observe = true, readOperation) {
if (readOperation !== undefined && !READ_OPERATIONS.has(readOperation)) {
throw new TypeError("Unknown daily AIC read operation");
}
const operationName = readOperation ?? "unknown";
for (let attempt = 0; ; attempt++) {
try {
let response;
try {
response = await operation();
} catch (error) {
if (observe && error?.response) budget.observe(error.response);
throw error;
}
if (observe) budget.observe(response);
if (response.status >= 500 && response.status < 600) {
throw apiError(response.status, response.headers, "Daily AIC read failed with a server error");
}
if (attempt > 0) {
core.info(`[daily-workflow-aic] Transient read recovered: ${JSON.stringify({ attempts: attempt + 1, status: response.status || null, requests, operation: operationName })}`);
}
return response;
} catch (error) {
const transient = isTransientError(error);
const status = getErrorStatus(error) || null;
if (attempt >= 2 || !transient) {
const reason = isQuotaError(error) ? "quota_stop" : safeResponseHeaders(error?.response?.headers)["retry-after"] != null ? "retry_restricted" : transient ? "retry_exhausted" : "non_transient";
const failure = Object.freeze({ attempts: attempt + 1, status: status !== null && Number.isInteger(status) && status >= 100 && status <= 599 ? status : null, reason, requests, operation: operationName });
if (error && typeof error === "object") READ_FAILURES.set(error, failure);
core.info(`[daily-workflow-aic] Stopped daily AIC read: ${JSON.stringify(failure)}`);
const diagnostic = getGitHubAPIDiagnostic(error);
if (diagnostic) core.info(`[daily-workflow-aic] ${operationName}: ${diagnostic.message}`);
throw error;
}
const delayMs = 1000 * 2 ** attempt;
core.info(`[daily-workflow-aic] Retrying transient read: ${JSON.stringify({ attempt: attempt + 1, maxAttempts: 3, status, delayMs, operation: operationName })}`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
},
snapshot() {
const metadata = getGitHubAPIQuota(latest);
return {
requests,
remaining: metadata.remaining ?? 0,
limit: metadata.limit ?? 0,
used: metadata.used ?? 0,
reset: metadata.reset,
};
},
};
return budget;
}
module.exports = { API_READ_OPERATIONS, safeResponseHeaders, apiError, retryNotBefore, createAPIBudget, getErrorStatus, isQuotaError, isTransientError, getAPIReadFailure };