feat(queue): 增加非文本模型分布式准入队列
使用 PostgreSQL 统一同步与异步非文本任务的并发准入、持久化等待和 Worker 容量分配,并将生产 API 与独立 Worker 角色拆分。 补充策略管理、共享契约、OpenAPI、Kubernetes 双节点 Worker 清单及跨节点验收脚本;未默认启用任何生产 queue_size 策略。 已在原基线完成 Go、前端、迁移、Shell、Kustomize 与长任务容量验收;合入最新主干后将重新执行发布门禁。
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user