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

@@ -13,6 +13,7 @@ type RequestQueueSettings = {
requestsPerMinute: number;
minTimeBetweenRequestsMs: number;
concurrentRequests: number;
globalConcurrentRequests: number;
maxWaitMs: number;
};
@@ -240,13 +241,21 @@ function RequestQueueCard({
}
/>
<NumberField
label={t("resilienceConcurrentRequests")}
label={t("resilienceConnectionScopeConcurrentRequests")}
value={draft.concurrentRequests}
min={1}
onChange={(concurrentRequests) =>
setDraft((prev) => ({ ...prev, concurrentRequests }))
}
/>
<NumberField
label={t("resilienceGlobalConcurrentRequests")}
value={draft.globalConcurrentRequests}
min={0}
onChange={(globalConcurrentRequests) =>
setDraft((prev) => ({ ...prev, globalConcurrentRequests }))
}
/>
<NumberField
label={t("resilienceMaxQueueWait")}
value={draft.maxWaitMs}
@@ -278,11 +287,21 @@ function RequestQueueCard({
</div>
</div>
<div className="rounded-xl border border-border bg-bg-subtle p-4">
<div className="text-xs text-text-muted">{t("resilienceConcurrentRequests")}</div>
<div className="text-xs text-text-muted">
{t("resilienceConnectionScopeConcurrentRequests")}
</div>
<div className="mt-1 text-sm font-semibold text-text-main">
{value.concurrentRequests}
</div>
</div>
<div className="rounded-xl border border-border bg-bg-subtle p-4">
<div className="text-xs text-text-muted">
{t("resilienceGlobalConcurrentRequests")}
</div>
<div className="mt-1 text-sm font-semibold text-text-main">
{value.globalConcurrentRequests || t("statusDisabled")}
</div>
</div>
<div className="rounded-xl border border-border bg-bg-subtle p-4">
<div className="text-xs text-text-muted">{t("resilienceMaxQueueWait")}</div>
<div className="mt-1 text-sm font-semibold text-text-main">

View File

@@ -7538,6 +7538,8 @@
"resilienceRequestsPerMinute": "Requests per minute",
"resilienceMinTimeBetweenRequests": "Minimum time between requests",
"resilienceConcurrentRequests": "Concurrent requests",
"resilienceConnectionScopeConcurrentRequests": "Gleichzeitige Anfragen pro Verbindung/Quota-Bereich",
"resilienceGlobalConcurrentRequests": "Globale gleichzeitige Upstream-Anfragen (0 = deaktiviert)",
"resilienceMaxQueueWaitTime": "Maximale Wartezeit in der Warteschlange",
"resilienceBaseCooldown": "Base cooldown",
"resilienceUseUpstreamRetryHints": "Use upstream retry hints",

View File

@@ -7550,6 +7550,8 @@
"resilienceRequestsPerMinute": "Requests per minute",
"resilienceMinTimeBetweenRequests": "Minimum time between requests",
"resilienceConcurrentRequests": "Concurrent requests",
"resilienceConnectionScopeConcurrentRequests": "Concurrent requests per connection/quota scope",
"resilienceGlobalConcurrentRequests": "Global concurrent upstream requests (0 = disabled)",
"resilienceMaxQueueWaitTime": "Maximum queue wait time",
"resilienceBaseCooldown": "Base cooldown",
"resilienceUseUpstreamRetryHints": "Use upstream retry hints",

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 {

View File

@@ -32,6 +32,7 @@ export const legacyResilienceDefaultsSchema = z
requestsPerMinute: z.number().int().min(1).optional(),
minTimeBetweenRequests: z.number().int().min(0).optional(),
concurrentRequests: z.number().int().min(1).optional(),
globalConcurrentRequests: z.number().int().min(0).max(100_000).optional(),
})
.strict();
@@ -170,6 +171,7 @@ export const updateResilienceSchema = z
.object({
rpm: z.number().int().min(1).optional(),
concurrency: z.number().int().min(1).optional(),
providerConcurrency: z.number().int().min(0).max(100_000).optional(),
})
.strict()
)