feat(relay): add policy overrides and idempotency-aware retry gating
This commit is contained in:
@@ -13,6 +13,7 @@ const DEFAULT_TIMEOUT_MS = 8000;
|
||||
const DEFAULT_MAX_RETRIES = 2;
|
||||
const DEFAULT_RETRY_BASE_DELAY_MS = 200;
|
||||
const DEFAULT_RETRY_MAX_DELAY_MS = 1200;
|
||||
const DEFAULT_ALLOW_NON_IDEMPOTENT_RETRIES = false;
|
||||
const MAX_TIMEOUT_MS = 30000;
|
||||
const MAX_RETRIES = 4;
|
||||
const MAX_RETRY_DELAY_MS = 5000;
|
||||
@@ -29,11 +30,24 @@ const RETRYABLE_ERROR_CODES = new Set([
|
||||
"EPIPE"
|
||||
]);
|
||||
|
||||
const IDEMPOTENT_RETRY_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
||||
|
||||
const parsePositiveInt = (value, fallback) => {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
};
|
||||
|
||||
const parseBoolean = (value, fallback) => {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "true") return true;
|
||||
if (normalized === "false") return false;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const clamp = (value, min, max) => {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
};
|
||||
@@ -65,6 +79,24 @@ const resolveNumericOverride = ({
|
||||
return clamp(Math.floor(overrideValue), lowerBound, max);
|
||||
};
|
||||
|
||||
const resolveBooleanOverride = ({ overrideValue, fallbackValue }) => {
|
||||
if (typeof overrideValue === "boolean") return overrideValue;
|
||||
return fallbackValue;
|
||||
};
|
||||
|
||||
const resolveRelayMethod = (value) => {
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
return "GET";
|
||||
}
|
||||
|
||||
return value.trim().toUpperCase();
|
||||
};
|
||||
|
||||
const canRetryForMethod = ({ method, allowNonIdempotentRetries }) => {
|
||||
if (allowNonIdempotentRetries) return true;
|
||||
return IDEMPOTENT_RETRY_METHODS.has(method);
|
||||
};
|
||||
|
||||
const getRelayConfig = () => {
|
||||
return {
|
||||
timeoutMs: sanitizeNumberConfig({
|
||||
@@ -93,7 +125,11 @@ const getRelayConfig = () => {
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
allowZero: true
|
||||
})
|
||||
}),
|
||||
allowNonIdempotentRetries: parseBoolean(
|
||||
process.env.RELAY_ALLOW_NON_IDEMPOTENT_RETRIES,
|
||||
DEFAULT_ALLOW_NON_IDEMPOTENT_RETRIES
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -122,8 +158,15 @@ const getStatusClass = (status) => {
|
||||
return `${Math.floor(status / 100)}xx`;
|
||||
};
|
||||
|
||||
const shouldRetryRelayError = ({ error, attempt, maxRetries }) => {
|
||||
const shouldRetryRelayError = ({
|
||||
error,
|
||||
attempt,
|
||||
maxRetries,
|
||||
method,
|
||||
allowNonIdempotentRetries
|
||||
}) => {
|
||||
if (attempt >= maxRetries) return false;
|
||||
if (!canRetryForMethod({ method, allowNonIdempotentRetries })) return false;
|
||||
|
||||
const status = error?.response?.status;
|
||||
if (NON_RETRYABLE_STATUS_CODES.has(status)) return false;
|
||||
@@ -139,12 +182,14 @@ export const relayGet = async ({
|
||||
res,
|
||||
errorResponse,
|
||||
transformData,
|
||||
requestOptionsBuilder
|
||||
requestOptionsBuilder,
|
||||
relayPolicy
|
||||
}) => {
|
||||
try {
|
||||
const { data, accessToken } = await relayGetData({
|
||||
queryUrl,
|
||||
requestOptionsBuilder
|
||||
requestOptionsBuilder,
|
||||
relayPolicy
|
||||
});
|
||||
|
||||
return respondSuccess(
|
||||
@@ -164,12 +209,14 @@ export const relayGet = async ({
|
||||
export const relayGetData = async ({
|
||||
queryUrl,
|
||||
requestOptionsBuilder,
|
||||
accessToken
|
||||
accessToken,
|
||||
relayPolicy
|
||||
}) => {
|
||||
return forwardGetData({
|
||||
queryUrl,
|
||||
requestOptionsBuilder,
|
||||
accessToken
|
||||
accessToken,
|
||||
relayPolicy
|
||||
});
|
||||
};
|
||||
|
||||
@@ -182,19 +229,26 @@ export const forwardGetData = async ({
|
||||
timeoutMs,
|
||||
maxRetries,
|
||||
retryBaseDelayMs,
|
||||
retryMaxDelayMs
|
||||
retryMaxDelayMs,
|
||||
method,
|
||||
relayPolicy
|
||||
}) => {
|
||||
const relayConfig = getRelayConfig();
|
||||
const relayPolicyConfig = relayPolicy || {};
|
||||
|
||||
const resolvedMethod = resolveRelayMethod(
|
||||
relayPolicyConfig.method || method
|
||||
);
|
||||
|
||||
const resolvedTimeoutMs = resolveNumericOverride({
|
||||
overrideValue: timeoutMs,
|
||||
overrideValue: relayPolicyConfig.timeoutMs ?? timeoutMs,
|
||||
fallbackValue: relayConfig.timeoutMs,
|
||||
min: 100,
|
||||
max: MAX_TIMEOUT_MS
|
||||
});
|
||||
|
||||
const resolvedMaxRetries = resolveNumericOverride({
|
||||
overrideValue: maxRetries,
|
||||
overrideValue: relayPolicyConfig.maxRetries ?? maxRetries,
|
||||
fallbackValue: relayConfig.maxRetries,
|
||||
min: 0,
|
||||
max: MAX_RETRIES,
|
||||
@@ -202,7 +256,7 @@ export const forwardGetData = async ({
|
||||
});
|
||||
|
||||
const resolvedRetryBaseDelayMs = resolveNumericOverride({
|
||||
overrideValue: retryBaseDelayMs,
|
||||
overrideValue: relayPolicyConfig.retryBaseDelayMs ?? retryBaseDelayMs,
|
||||
fallbackValue: relayConfig.retryBaseDelayMs,
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
@@ -210,13 +264,20 @@ export const forwardGetData = async ({
|
||||
});
|
||||
|
||||
const resolvedRetryMaxDelayMs = resolveNumericOverride({
|
||||
overrideValue: retryMaxDelayMs,
|
||||
overrideValue: relayPolicyConfig.retryMaxDelayMs ?? retryMaxDelayMs,
|
||||
fallbackValue: relayConfig.retryMaxDelayMs,
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
allowZero: true
|
||||
});
|
||||
|
||||
const resolvedAllowNonIdempotentRetries = resolveBooleanOverride({
|
||||
overrideValue:
|
||||
relayPolicyConfig.allowNonIdempotentRetries ??
|
||||
relayPolicyConfig.allowRetriesForNonIdempotent,
|
||||
fallbackValue: relayConfig.allowNonIdempotentRetries
|
||||
});
|
||||
|
||||
const tokenAccessToken =
|
||||
typeof accessToken === "string" && accessToken.length > 0
|
||||
? accessToken
|
||||
@@ -246,10 +307,12 @@ export const forwardGetData = async ({
|
||||
relayRequestId,
|
||||
queryUrl,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
method: resolvedMethod,
|
||||
timeoutMs: resolvedTimeoutMs,
|
||||
maxRetries: resolvedMaxRetries,
|
||||
retryBaseDelayMs: resolvedRetryBaseDelayMs,
|
||||
retryMaxDelayMs: resolvedRetryMaxDelayMs
|
||||
retryMaxDelayMs: resolvedRetryMaxDelayMs,
|
||||
allowNonIdempotentRetries: resolvedAllowNonIdempotentRetries
|
||||
});
|
||||
|
||||
let lastError;
|
||||
@@ -279,7 +342,9 @@ export const forwardGetData = async ({
|
||||
const retryEligible = shouldRetryRelayError({
|
||||
error,
|
||||
attempt,
|
||||
maxRetries: resolvedMaxRetries
|
||||
maxRetries: resolvedMaxRetries,
|
||||
method: resolvedMethod,
|
||||
allowNonIdempotentRetries: resolvedAllowNonIdempotentRetries
|
||||
});
|
||||
|
||||
if (!retryEligible) {
|
||||
@@ -288,6 +353,7 @@ export const forwardGetData = async ({
|
||||
relayRequestId,
|
||||
queryUrl,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
method: resolvedMethod,
|
||||
attempt: attempt + 1,
|
||||
maxAttempts: resolvedMaxRetries + 1,
|
||||
attemptsMade: attempt + 1,
|
||||
@@ -311,6 +377,7 @@ export const forwardGetData = async ({
|
||||
relayRequestId,
|
||||
queryUrl,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
method: resolvedMethod,
|
||||
attempt: attempt + 1,
|
||||
maxAttempts: resolvedMaxRetries + 1,
|
||||
attemptsMade: attempt + 1,
|
||||
|
||||
Reference in New Issue
Block a user