perf(queue): 按策略动态扩缩异步 Worker
将 River 执行容量按平台模型与用户组的有效并发策略动态调整,并为长任务续租、并发租约原子抢占和限流退避补充保护。\n\n统一平台模型限流继承语义,兼容历史 platformLimits/modelLimits,并为三个迁移图像模型建立独立并发租约。补充管理端显式继承/覆盖配置、指标、单元测试及隔离 PostgreSQL 验收。\n\n验证:go test ./... -count=1;pnpm lint;pnpm test;pnpm build;隔离 PostgreSQL 并发原子性/续租测试;128 任务与三分钟长任务动态 Worker 验收。
This commit is contained in:
@@ -1513,6 +1513,7 @@ function mergeExistingPlatformModelInput(input: PlatformModelBindingInput, curre
|
||||
discountFactor: (input.discountFactor ?? existing.discountFactor) || undefined,
|
||||
pricingRuleSetId: input.pricingRuleSetId ?? existing.pricingRuleSetId,
|
||||
rateLimitPolicy: input.rateLimitPolicy ?? existing.rateLimitPolicy,
|
||||
rateLimitPolicyMode: input.rateLimitPolicyMode ?? existing.rateLimitPolicyMode,
|
||||
retryPolicy: input.retryPolicy ?? existing.retryPolicy,
|
||||
runtimePolicyOverride: input.runtimePolicyOverride ?? (existing.runtimePolicyOverride as Record<string, unknown> | undefined),
|
||||
runtimePolicySetId: input.runtimePolicySetId ?? existing.runtimePolicySetId,
|
||||
|
||||
@@ -409,13 +409,13 @@ function ModelBindingPolicy(props: { form: PlatformWizardForm; onChange: (value:
|
||||
</Label>
|
||||
</>
|
||||
)}
|
||||
<ToggleField checked={form.modelOverrideRateLimit} label="覆盖模型限流策略" onChange={(checked) => onChange({ ...form, modelOverrideRateLimit: checked })} />
|
||||
<ToggleField checked={form.modelOverrideRateLimit} label="覆盖模型限流策略" onChange={(checked) => onChange({ ...form, modelOverrideRateLimit: checked, modelRateLimitOverrideTouched: true })} />
|
||||
{form.modelOverrideRateLimit && (
|
||||
<>
|
||||
<Label>模型 RPM<Input value={form.modelRpmLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelRpmLimit: event.target.value })} /></Label>
|
||||
<Label>模型 RPS<Input value={form.modelRpsLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelRpsLimit: event.target.value })} /></Label>
|
||||
<Label>模型 TPM<Input value={form.modelTpmLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelTpmLimit: event.target.value })} /></Label>
|
||||
<Label>模型并发<Input value={form.modelConcurrencyLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelConcurrencyLimit: event.target.value })} /></Label>
|
||||
<Label>模型 RPM<Input value={form.modelRpmLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelRpmLimit: event.target.value, modelRateLimitOverrideTouched: true })} /></Label>
|
||||
<Label>模型 RPS<Input value={form.modelRpsLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelRpsLimit: event.target.value, modelRateLimitOverrideTouched: true })} /></Label>
|
||||
<Label>模型 TPM<Input value={form.modelTpmLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelTpmLimit: event.target.value, modelRateLimitOverrideTouched: true })} /></Label>
|
||||
<Label>模型并发<Input value={form.modelConcurrencyLimit} placeholder="不填则不限制" inputMode="numeric" onChange={(event) => onChange({ ...form, modelConcurrencyLimit: event.target.value, modelRateLimitOverrideTouched: true })} /></Label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1004,6 +1004,7 @@ function platformToForm(
|
||||
const rateLimitPolicy = platform.rateLimitPolicy ?? {};
|
||||
const networkProxy = readNetworkProxyConfig(config);
|
||||
const currentModels = platformModels.filter((model) => model.platformId === platform.id);
|
||||
const modelRateLimitPolicy = currentModels.find((model) => model.rateLimitPolicyMode === 'override')?.rateLimitPolicy ?? {};
|
||||
return {
|
||||
...createEmptyPlatformForm(platform.provider, defaults),
|
||||
provider: platform.provider,
|
||||
@@ -1034,6 +1035,12 @@ function platformToForm(
|
||||
selectedModelIds: platformModelBaseIds(platform, baseModels, currentModels),
|
||||
modelDiscountFactors: platformModelDiscountFactors(platform, baseModels, currentModels),
|
||||
modelNameMappings: platformModelNameMappings(platform, baseModels, currentModels),
|
||||
modelOverrideRateLimit: currentModels.length > 0 && currentModels.every((model) => model.rateLimitPolicyMode === 'override'),
|
||||
modelRateLimitOverrideTouched: false,
|
||||
modelRpmLimit: readLimit(modelRateLimitPolicy, 'rpm'),
|
||||
modelRpsLimit: readLimit(modelRateLimitPolicy, 'rps'),
|
||||
modelTpmLimit: readLimit(modelRateLimitPolicy, 'tpm_total'),
|
||||
modelConcurrencyLimit: readLimit(modelRateLimitPolicy, 'concurrent'),
|
||||
selectionMode: 'partial',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { BaseModelCatalogItem } from '@easyai-ai-gateway/contracts';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createEmptyPlatformForm, platformModelPayloads } from './platform-form';
|
||||
|
||||
const baseModel = {
|
||||
id: 'model-1',
|
||||
providerKey: 'simulation',
|
||||
canonicalModelKey: 'simulation:model-1',
|
||||
invocationName: 'model-1',
|
||||
providerModelName: 'model-1',
|
||||
modelType: ['video_generate'],
|
||||
modelAlias: 'model-1',
|
||||
displayName: 'Model 1',
|
||||
referenceCount: 0,
|
||||
pricingVersion: 1,
|
||||
status: 'active',
|
||||
createdAt: '2026-07-24T00:00:00Z',
|
||||
updatedAt: '2026-07-24T00:00:00Z',
|
||||
} satisfies BaseModelCatalogItem;
|
||||
|
||||
function selectedForm() {
|
||||
return {
|
||||
...createEmptyPlatformForm('simulation'),
|
||||
selectedModelIds: [baseModel.id],
|
||||
};
|
||||
}
|
||||
|
||||
describe('platform model rate limit payload', () => {
|
||||
it('preserves an existing model policy when the edit form was not touched', () => {
|
||||
const [payload] = platformModelPayloads([baseModel], selectedForm());
|
||||
|
||||
expect(payload.rateLimitPolicyMode).toBeUndefined();
|
||||
expect(payload.rateLimitPolicy).toBeUndefined();
|
||||
});
|
||||
|
||||
it('writes inherit only after the override is explicitly disabled', () => {
|
||||
const [payload] = platformModelPayloads([baseModel], {
|
||||
...selectedForm(),
|
||||
modelRateLimitOverrideTouched: true,
|
||||
});
|
||||
|
||||
expect(payload.rateLimitPolicyMode).toBe('inherit');
|
||||
expect(payload.rateLimitPolicy).toBeUndefined();
|
||||
});
|
||||
|
||||
it('writes an explicit unlimited override when the enabled policy has no rules', () => {
|
||||
const [payload] = platformModelPayloads([baseModel], {
|
||||
...selectedForm(),
|
||||
modelOverrideRateLimit: true,
|
||||
modelRateLimitOverrideTouched: true,
|
||||
});
|
||||
|
||||
expect(payload.rateLimitPolicyMode).toBe('override');
|
||||
expect(payload.rateLimitPolicy).toEqual({ rules: [] });
|
||||
});
|
||||
});
|
||||
@@ -41,6 +41,7 @@ export interface PlatformWizardForm {
|
||||
modelRetryEnabled: boolean;
|
||||
modelRetryMaxAttempts: string;
|
||||
modelOverrideRateLimit: boolean;
|
||||
modelRateLimitOverrideTouched: boolean;
|
||||
modelRpmLimit: string;
|
||||
modelRpsLimit: string;
|
||||
modelTpmLimit: string;
|
||||
@@ -100,6 +101,7 @@ export function createEmptyPlatformForm(provider = '', defaults?: ProviderConnec
|
||||
modelRetryEnabled: true,
|
||||
modelRetryMaxAttempts: '2',
|
||||
modelOverrideRateLimit: false,
|
||||
modelRateLimitOverrideTouched: false,
|
||||
modelRpmLimit: '',
|
||||
modelRpsLimit: '',
|
||||
modelTpmLimit: '',
|
||||
@@ -187,12 +189,15 @@ export function platformModelPayloads(models: BaseModelCatalogItem[], form: Plat
|
||||
retryPolicy: form.modelOverrideRetry
|
||||
? { enabled: form.modelRetryEnabled, maxAttempts: form.modelRetryEnabled ? positiveInt(form.modelRetryMaxAttempts, 2) : 1 }
|
||||
: undefined,
|
||||
rateLimitPolicy: form.modelOverrideRateLimit ? rateLimitPolicyPayload({
|
||||
rpmLimit: form.modelRpmLimit,
|
||||
rpsLimit: form.modelRpsLimit,
|
||||
tpmLimit: form.modelTpmLimit,
|
||||
concurrencyLimit: form.modelConcurrencyLimit,
|
||||
}) : undefined,
|
||||
rateLimitPolicy: form.modelRateLimitOverrideTouched && form.modelOverrideRateLimit ? rateLimitPolicyPayload({
|
||||
rpmLimit: form.modelRpmLimit,
|
||||
rpsLimit: form.modelRpsLimit,
|
||||
tpmLimit: form.modelTpmLimit,
|
||||
concurrencyLimit: form.modelConcurrencyLimit,
|
||||
}) : undefined,
|
||||
rateLimitPolicyMode: form.modelRateLimitOverrideTouched
|
||||
? (form.modelOverrideRateLimit ? 'override' : 'inherit')
|
||||
: undefined,
|
||||
runtimePolicyOverride: platformModelRuntimeOverride(form),
|
||||
};
|
||||
});
|
||||
@@ -304,14 +309,6 @@ function platformModelRuntimeOverride(form: PlatformWizardForm) {
|
||||
maxAttempts: form.modelRetryEnabled ? positiveInt(form.modelRetryMaxAttempts, 2) : 1,
|
||||
};
|
||||
}
|
||||
if (form.modelOverrideRateLimit) {
|
||||
override.rateLimitPolicy = rateLimitPolicyPayload({
|
||||
rpmLimit: form.modelRpmLimit,
|
||||
rpsLimit: form.modelRpsLimit,
|
||||
tpmLimit: form.modelTpmLimit,
|
||||
concurrencyLimit: form.modelConcurrencyLimit,
|
||||
});
|
||||
}
|
||||
return Object.keys(override).length ? override : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ export interface PlatformModelBindingInput {
|
||||
pricingMode?: string;
|
||||
retryPolicy?: Record<string, unknown>;
|
||||
rateLimitPolicy?: Record<string, unknown>;
|
||||
rateLimitPolicyMode?: 'inherit' | 'override';
|
||||
runtimePolicySetId?: string;
|
||||
runtimePolicyOverride?: Record<string, unknown>;
|
||||
pricingRuleSetId?: string;
|
||||
|
||||
Reference in New Issue
Block a user