feat(resilience): add hierarchical concurrency admission

This commit is contained in:
Ravi Tharuma
2026-08-24 05:05:34 +02:00
committed by Markus Hartung
parent 943b9aaa84
commit 2b9f25accb
15 changed files with 485 additions and 248 deletions

View File

@@ -58,6 +58,7 @@ export const DEFAULT_RESILIENCE_SETTINGS: ResilienceSettings = {
requestsPerMinute: DEFAULT_API_LIMITS.requestsPerMinute,
minTimeBetweenRequestsMs: DEFAULT_API_LIMITS.minTimeBetweenRequests,
concurrentRequests: DEFAULT_API_LIMITS.concurrentRequests,
globalConcurrentRequests: 0,
maxWaitMs: DEFAULT_REQUEST_QUEUE_MAX_WAIT_MS,
maxQueueDepth: DEFAULT_REQUEST_QUEUE_MAX_DEPTH,
},
@@ -208,6 +209,8 @@ function buildLegacyFallback(settings: JsonRecord): ResilienceSettings {
DEFAULT_RESILIENCE_SETTINGS.requestQueue.concurrentRequests,
{ min: 1, max: 10_000 }
),
globalConcurrentRequests:
DEFAULT_RESILIENCE_SETTINGS.requestQueue.globalConcurrentRequests,
maxWaitMs: DEFAULT_RESILIENCE_SETTINGS.requestQueue.maxWaitMs,
maxQueueDepth: DEFAULT_RESILIENCE_SETTINGS.requestQueue.maxQueueDepth,
},

View File

@@ -124,6 +124,11 @@ export function normalizeRequestQueueSettings(
min: 1,
max: 10_000,
});
const globalConcurrentRequests = toInteger(
record.globalConcurrentRequests,
fallback.globalConcurrentRequests,
{ min: 0, max: 100_000 }
);
const maxWaitMs = toInteger(record.maxWaitMs, fallback.maxWaitMs, {
min: 1,
max: 24 * 60 * 60 * 1000,
@@ -141,6 +146,7 @@ export function normalizeRequestQueueSettings(
requestsPerMinute,
minTimeBetweenRequestsMs,
concurrentRequests,
globalConcurrentRequests,
maxWaitMs,
maxQueueDepth,
};
@@ -431,8 +437,16 @@ function normalizeProviderQuotaOverrideEntry(raw: unknown): ProviderQuotaOverrid
const out: ProviderQuotaOverrideSettings = {};
const rpm = typeof record.rpm === "number" ? record.rpm : Number(record.rpm);
if (Number.isFinite(rpm) && rpm > 0) out.rpm = Math.trunc(rpm);
const concurrency = typeof record.concurrency === "number" ? record.concurrency : Number(record.concurrency);
const concurrency =
typeof record.concurrency === "number" ? record.concurrency : Number(record.concurrency);
if (Number.isFinite(concurrency) && concurrency > 0) out.concurrency = Math.trunc(concurrency);
const providerConcurrency =
typeof record.providerConcurrency === "number"
? record.providerConcurrency
: Number(record.providerConcurrency);
if (Number.isFinite(providerConcurrency) && providerConcurrency >= 0) {
out.providerConcurrency = Math.trunc(providerConcurrency);
}
return Object.keys(out).length > 0 ? out : null;
}

View File

@@ -16,6 +16,8 @@ export interface RequestQueueSettings {
requestsPerMinute: number;
minTimeBetweenRequestsMs: number;
concurrentRequests: number;
/** Whole-process upstream concurrency cap. Zero disables the global gate. */
globalConcurrentRequests: number;
/**
* Legacy persisted key used as Bottleneck's post-dispatch execution
* expiration. It does not bound time spent in Bottleneck's QUEUED state.
@@ -169,6 +171,8 @@ export interface ProviderQuotaOverrideSettings {
rpm?: number;
/** Overrides the static per-connection concurrency cap. */
concurrency?: number;
/** Shared concurrency cap across every connection for this provider. */
providerConcurrency?: number;
}
export interface StreamRecoverySettings {