feat(queue): 增加非文本模型分布式准入队列

使用 PostgreSQL 统一同步与异步非文本任务的并发准入、持久化等待和 Worker 容量分配,并将生产 API 与独立 Worker 角色拆分。

补充策略管理、共享契约、OpenAPI、Kubernetes 双节点 Worker 清单及跨节点验收脚本;未默认启用任何生产 queue_size 策略。

已在原基线完成 Go、前端、迁移、Shell、Kustomize 与长任务容量验收;合入最新主干后将重新执行发布门禁。
This commit is contained in:
2026-07-29 16:15:43 +08:00
parent 2e5a90731b
commit 9e4fc7362d
55 changed files with 4565 additions and 237 deletions
@@ -26,13 +26,18 @@ type RunnerActionRule = {
cooldownSeconds: string;
};
type RuntimePolicyForm = {
export type RuntimePolicyForm = {
policyKey: string;
name: string;
description: string;
rpm: string;
tpm: string;
concurrency: string;
queueSize: string;
queueMaxWaitSeconds: string;
rateLimitPolicyExtra: Record<string, unknown>;
rateLimitRuleExtras: Record<string, Record<string, unknown>>;
rateLimitPreservedRules: Array<Record<string, unknown>>;
retryEnabled: boolean;
retryMaxAttempts: string;
retryAllowKeywords: string[];
@@ -275,11 +280,13 @@ export function RuntimePoliciesPanel(props: {
<Label className="spanTwo"><Input value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} /></Label>
<section className="runtimePolicySection spanTwo">
<header><strong></strong><span>TPM / RPM / </span></header>
<header><strong></strong><span>TPM / RPM / / </span></header>
<div className="runtimePolicyRows">
<Label>RPM / <Input value={form.rpm} placeholder="不填或负数为不限" inputMode="numeric" onChange={(event) => setForm({ ...form, rpm: event.target.value })} /></Label>
<Label>TPM / Token<Input value={form.tpm} placeholder="不填或负数为不限" inputMode="numeric" onChange={(event) => setForm({ ...form, tpm: event.target.value })} /></Label>
<Label><Input value={form.concurrency} placeholder="不填或负数为不限" inputMode="numeric" onChange={(event) => setForm({ ...form, concurrency: event.target.value })} /></Label>
<Label><Input value={form.queueSize} placeholder="0 或留空关闭排队" inputMode="numeric" onChange={(event) => setForm({ ...form, queueSize: event.target.value })} /></Label>
<Label><Input value={form.queueMaxWaitSeconds} placeholder="默认 600" inputMode="numeric" disabled={!Number(form.queueSize)} onChange={(event) => setForm({ ...form, queueMaxWaitSeconds: event.target.value })} /></Label>
</div>
</section>
@@ -615,6 +622,11 @@ function createDefaultForm(policyKey = 'default-runtime-v1'): RuntimePolicyForm
rpm: '120',
tpm: '240000',
concurrency: '6',
queueSize: '',
queueMaxWaitSeconds: '600',
rateLimitPolicyExtra: {},
rateLimitRuleExtras: {},
rateLimitPreservedRules: [],
retryEnabled: true,
retryMaxAttempts: '2',
retryAllowKeywords: ['rate_limit', 'timeout', 'server_error', 'network', '429', '5xx'],
@@ -949,8 +961,9 @@ function defaultRunnerActionTarget(action: RunnerFailoverAction): RunnerFailover
return action === 'cooldown_and_next' ? 'model' : 'platform';
}
function policyToForm(policy: RuntimePolicySet): RuntimePolicyForm {
export function policyToForm(policy: RuntimePolicySet): RuntimePolicyForm {
const rateRules = Array.isArray(policy.rateLimitPolicy?.rules) ? policy.rateLimitPolicy.rules : [];
const rateLimitParts = splitRuntimeRateLimitPolicy(readObject(policy.rateLimitPolicy));
const retry = readObject(policy.retryPolicy);
const disable = readObject(policy.autoDisablePolicy);
const degrade = readObject(policy.degradePolicy);
@@ -961,6 +974,11 @@ function policyToForm(policy: RuntimePolicySet): RuntimePolicyForm {
rpm: formRateLimitText(readRateLimit(rateRules, 'rpm')),
tpm: formRateLimitText(readRateLimit(rateRules, 'tpm_total')),
concurrency: formRateLimitText(readRateLimit(rateRules, 'concurrent')),
queueSize: formRateLimitText(readRateLimit(rateRules, 'queue_size')),
queueMaxWaitSeconds: formRateLimitPropertyText(readRateLimitRule(rateRules, 'queue_size'), 'maxWaitSeconds', '600'),
rateLimitPolicyExtra: rateLimitParts.extra,
rateLimitRuleExtras: rateLimitParts.ruleExtras,
rateLimitPreservedRules: rateLimitParts.preservedRules,
retryEnabled: readBool(retry.enabled, true),
retryMaxAttempts: String(readNumber(retry.maxAttempts, 2)),
retryAllowKeywords: tagsFromValue(retry.allowKeywords),
@@ -976,12 +994,15 @@ function policyToForm(policy: RuntimePolicySet): RuntimePolicyForm {
};
}
function formToPayload(form: RuntimePolicyForm): RuntimePolicySetUpsertRequest {
export function formToPayload(form: RuntimePolicyForm): RuntimePolicySetUpsertRequest {
return {
policyKey: form.policyKey.trim(),
name: form.name.trim(),
description: form.description.trim() || undefined,
rateLimitPolicy: { rules: rateLimitRules(form) },
rateLimitPolicy: {
...form.rateLimitPolicyExtra,
rules: rateLimitRules(form),
},
retryPolicy: {
enabled: form.retryEnabled,
maxAttempts: positiveInt(form.retryMaxAttempts, 2),
@@ -1005,16 +1026,54 @@ function formToPayload(form: RuntimePolicyForm): RuntimePolicySetUpsertRequest {
function rateLimitRules(form: RuntimePolicyForm) {
return [
limitRule('rpm', form.rpm),
limitRule('tpm_total', form.tpm),
limitRule('concurrent', form.concurrency, 60, 120),
...form.rateLimitPreservedRules,
limitRule('rpm', form.rpm, 60, undefined, form.rateLimitRuleExtras.rpm),
limitRule('tpm_total', form.tpm, 60, undefined, form.rateLimitRuleExtras.tpm_total),
limitRule('concurrent', form.concurrency, 60, 120, form.rateLimitRuleExtras.concurrent),
queueLimitRule(form.queueSize, form.queueMaxWaitSeconds, form.rateLimitRuleExtras.queue_size),
].filter((rule): rule is NonNullable<ReturnType<typeof limitRule>> => Boolean(rule));
}
function limitRule(metric: string, value: string, windowSeconds = 60, leaseTtlSeconds?: number) {
function queueLimitRule(queueSize: string, maxWaitSeconds: string, extra: Record<string, unknown> = {}) {
if (!queueSize.trim() || Number(queueSize) === 0) return undefined;
const limit = boundedPositiveInt(queueSize, '排队长度', 10000);
const maxWait = maxWaitSeconds.trim() ? boundedPositiveInt(maxWaitSeconds, '最长等待', 3600) : 600;
return { ...extra, metric: 'queue_size', limit, maxWaitSeconds: maxWait };
}
function limitRule(metric: string, value: string, windowSeconds = 60, leaseTtlSeconds?: number, extra: Record<string, unknown> = {}) {
const limit = Number(value);
if (!Number.isFinite(limit) || limit <= 0) return undefined;
return { metric, limit, windowSeconds, leaseTtlSeconds };
return {
...extra,
metric,
limit,
windowSeconds: typeof extra.windowSeconds === 'number' ? extra.windowSeconds : windowSeconds,
leaseTtlSeconds: typeof extra.leaseTtlSeconds === 'number' ? extra.leaseTtlSeconds : leaseTtlSeconds,
};
}
function splitRuntimeRateLimitPolicy(policy: Record<string, unknown>) {
const editableMetrics = new Set(['rpm', 'tpm_total', 'concurrent', 'queue_size']);
const extra = { ...policy };
delete extra.rules;
const ruleExtras: Record<string, Record<string, unknown>> = {};
const preservedRules: Array<Record<string, unknown>> = [];
const rules = Array.isArray(policy.rules) ? policy.rules : [];
rules.forEach((rawRule) => {
const rule = readObject(rawRule);
const metric = typeof rule.metric === 'string' ? rule.metric.trim() : '';
if (!editableMetrics.has(metric)) {
preservedRules.push({ ...rule });
return;
}
const ruleExtra = { ...rule };
delete ruleExtra.metric;
delete ruleExtra.limit;
delete ruleExtra.maxWaitSeconds;
ruleExtras[metric] = ruleExtra;
});
return { extra, ruleExtras, preservedRules };
}
function isDefaultPolicy(policy: RuntimePolicySet) {
@@ -1026,7 +1085,9 @@ function rateLimitSummary(policy: RuntimePolicySet) {
const rpm = rateLimitText(readRateLimit(rules, 'rpm'));
const tpm = rateLimitText(readRateLimit(rules, 'tpm_total'));
const concurrent = rateLimitText(readRateLimit(rules, 'concurrent'));
return `RPM ${rpm} / TPM ${tpm} / 并发 ${concurrent}`;
const queue = readRateLimit(rules, 'queue_size');
const queueText = queue ? ` / 排队 ${rateLimitText(queue)} / 最长 ${formRateLimitPropertyText(readRateLimitRule(rules, 'queue_size'), 'maxWaitSeconds', '600')}` : '';
return `RPM ${rpm} / TPM ${tpm} / 并发 ${concurrent}${queueText}`;
}
function retrySummary(policy: RuntimePolicySet) {
@@ -1045,11 +1106,20 @@ function degradeSummary(policy: RuntimePolicySet) {
}
function readRateLimit(rules: unknown[], metric: string) {
const rule = rules.find((item) => readObject(item).metric === metric);
const rule = readRateLimitRule(rules, metric);
const limit = Number(readObject(rule).limit);
return Number.isFinite(limit) ? limit : undefined;
}
function readRateLimitRule(rules: unknown[], metric: string) {
return rules.find((item) => readObject(item).metric === metric);
}
function formRateLimitPropertyText(rule: unknown, key: string, fallback = '') {
const value = Number(readObject(rule)[key]);
return Number.isFinite(value) && value > 0 ? String(value) : fallback;
}
function formRateLimitText(value: number | undefined) {
return value === undefined ? '' : String(value);
}
@@ -1090,6 +1160,14 @@ function positiveInt(value: string, fallback: number) {
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function boundedPositiveInt(value: string, label: string, max: number) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1 || parsed > max) {
throw new Error(`${label}必须是 1${max} 的整数`);
}
return parsed;
}
function readNumber(value: unknown, fallback: number) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;