feat(queue): 增加非文本模型分布式准入队列
使用 PostgreSQL 统一同步与异步非文本任务的并发准入、持久化等待和 Worker 容量分配,并将生产 API 与独立 Worker 角色拆分。 补充策略管理、共享契约、OpenAPI、Kubernetes 双节点 Worker 清单及跨节点验收脚本;未默认启用任何生产 queue_size 策略。 已在原基线完成 Go、前端、迁移、Shell、Kustomize 与长任务容量验收;合入最新主干后将重新执行发布门禁。
This commit is contained in:
@@ -1,13 +1,24 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrInvalidRateLimitPolicy = errors.New("invalid rate limit policy")
|
||||
|
||||
func IsInvalidRateLimitPolicy(err error) bool {
|
||||
return errors.Is(err, ErrInvalidRateLimitPolicy)
|
||||
}
|
||||
|
||||
const (
|
||||
RateLimitPolicyModeInherit = "inherit"
|
||||
RateLimitPolicyModeOverride = "override"
|
||||
DefaultQueueMaxWaitSeconds = 600
|
||||
MaxQueueSize = 10000
|
||||
MaxQueueWaitSeconds = 3600
|
||||
)
|
||||
|
||||
type EffectiveRateLimitPolicyInput struct {
|
||||
@@ -78,46 +89,150 @@ func NormalizeRateLimitPolicy(policy map[string]any) map[string]any {
|
||||
}
|
||||
out := clonePolicy(policy)
|
||||
rules, _ := out["rules"].([]any)
|
||||
if len(rules) > 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
legacyScopes := []map[string]any{policy}
|
||||
for _, key := range []string{"platformLimits", "modelLimits", "platform_limits", "model_limits"} {
|
||||
if nested, ok := policy[key].(map[string]any); ok {
|
||||
legacyScopes = append(legacyScopes, nested)
|
||||
if len(rules) == 0 {
|
||||
legacyScopes := []map[string]any{policy}
|
||||
for _, key := range []string{"platformLimits", "modelLimits", "platform_limits", "model_limits"} {
|
||||
if nested, ok := policy[key].(map[string]any); ok {
|
||||
legacyScopes = append(legacyScopes, nested)
|
||||
}
|
||||
}
|
||||
if limit, ok := lowestPositiveLegacyLimit(legacyScopes,
|
||||
"max_concurrent_requests", "maxConcurrentRequests", "concurrent"); ok {
|
||||
rules = append(rules, map[string]any{
|
||||
"metric": "concurrent",
|
||||
"limit": limit,
|
||||
"leaseTtlSeconds": 120,
|
||||
})
|
||||
}
|
||||
if limit, ok := lowestPositiveLegacyLimit(legacyScopes,
|
||||
"max_request_per_minute", "maxRequestsPerMinute", "rpm"); ok {
|
||||
rules = append(rules, map[string]any{
|
||||
"metric": "rpm",
|
||||
"limit": limit,
|
||||
"windowSeconds": 60,
|
||||
})
|
||||
}
|
||||
if limit, ok := lowestPositiveLegacyLimit(legacyScopes,
|
||||
"max_tokens_per_minute", "maxTokensPerMinute", "tpm_total"); ok {
|
||||
rules = append(rules, map[string]any{
|
||||
"metric": "tpm_total",
|
||||
"limit": limit,
|
||||
"windowSeconds": 60,
|
||||
})
|
||||
}
|
||||
}
|
||||
if limit, ok := lowestPositiveLegacyLimit(legacyScopes,
|
||||
"max_concurrent_requests", "maxConcurrentRequests", "concurrent"); ok {
|
||||
rules = append(rules, map[string]any{
|
||||
"metric": "concurrent",
|
||||
"limit": limit,
|
||||
"leaseTtlSeconds": 120,
|
||||
})
|
||||
}
|
||||
if limit, ok := lowestPositiveLegacyLimit(legacyScopes,
|
||||
"max_request_per_minute", "maxRequestsPerMinute", "rpm"); ok {
|
||||
rules = append(rules, map[string]any{
|
||||
"metric": "rpm",
|
||||
"limit": limit,
|
||||
"windowSeconds": 60,
|
||||
})
|
||||
}
|
||||
if limit, ok := lowestPositiveLegacyLimit(legacyScopes,
|
||||
"max_tokens_per_minute", "maxTokensPerMinute", "tpm_total"); ok {
|
||||
rules = append(rules, map[string]any{
|
||||
"metric": "tpm_total",
|
||||
"limit": limit,
|
||||
"windowSeconds": 60,
|
||||
})
|
||||
}
|
||||
if len(rules) > 0 {
|
||||
out["rules"] = rules
|
||||
normalizedRules := make([]any, 0, len(rules))
|
||||
for _, rawRule := range rules {
|
||||
rule, ok := rawRule.(map[string]any)
|
||||
if !ok {
|
||||
normalizedRules = append(normalizedRules, rawRule)
|
||||
continue
|
||||
}
|
||||
normalizedRule := clonePolicy(rule)
|
||||
if strings.TrimSpace(stringValue(normalizedRule["metric"])) == "queue_size" {
|
||||
if floatValue(normalizedRule["limit"]) <= 0 {
|
||||
continue
|
||||
}
|
||||
if floatValue(normalizedRule["maxWaitSeconds"]) <= 0 {
|
||||
normalizedRule["maxWaitSeconds"] = DefaultQueueMaxWaitSeconds
|
||||
}
|
||||
} else {
|
||||
delete(normalizedRule, "maxWaitSeconds")
|
||||
}
|
||||
normalizedRules = append(normalizedRules, normalizedRule)
|
||||
}
|
||||
out["rules"] = normalizedRules
|
||||
return out
|
||||
}
|
||||
|
||||
type QueuePolicyRule struct {
|
||||
Limit int
|
||||
MaxWaitSeconds int
|
||||
Policy map[string]any
|
||||
}
|
||||
|
||||
func QueueRuleFromPolicy(policy map[string]any) (QueuePolicyRule, bool) {
|
||||
normalized := NormalizeRateLimitPolicy(policy)
|
||||
rules, _ := normalized["rules"].([]any)
|
||||
for _, rawRule := range rules {
|
||||
rule, _ := rawRule.(map[string]any)
|
||||
if strings.TrimSpace(stringValue(rule["metric"])) != "queue_size" {
|
||||
continue
|
||||
}
|
||||
limit := int(math.Floor(floatValue(rule["limit"])))
|
||||
if limit <= 0 {
|
||||
return QueuePolicyRule{}, false
|
||||
}
|
||||
maxWaitSeconds := int(math.Floor(floatValue(rule["maxWaitSeconds"])))
|
||||
if maxWaitSeconds <= 0 {
|
||||
maxWaitSeconds = DefaultQueueMaxWaitSeconds
|
||||
}
|
||||
return QueuePolicyRule{
|
||||
Limit: limit,
|
||||
MaxWaitSeconds: maxWaitSeconds,
|
||||
Policy: normalized,
|
||||
}, true
|
||||
}
|
||||
return QueuePolicyRule{}, false
|
||||
}
|
||||
|
||||
// NormalizeAndValidateRateLimitPolicy validates only the canonical queue
|
||||
// extension. Existing rate-limit metrics intentionally keep their historical
|
||||
// permissive parsing contract.
|
||||
func NormalizeAndValidateRateLimitPolicy(policy map[string]any) (map[string]any, error) {
|
||||
if policy == nil {
|
||||
return nil, nil
|
||||
}
|
||||
rulesValue, rulesPresent := policy["rules"]
|
||||
if rulesPresent {
|
||||
if _, ok := rulesValue.([]any); !ok {
|
||||
return nil, fmt.Errorf("%w: rateLimitPolicy.rules must be an array", ErrInvalidRateLimitPolicy)
|
||||
}
|
||||
}
|
||||
rules, _ := rulesValue.([]any)
|
||||
for index, rawRule := range rules {
|
||||
rule, ok := rawRule.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: rateLimitPolicy.rules[%d] must be an object", ErrInvalidRateLimitPolicy, index)
|
||||
}
|
||||
metric := strings.TrimSpace(stringValue(rule["metric"]))
|
||||
maxWait, hasMaxWait := numericRuleValue(rule, "maxWaitSeconds")
|
||||
if metric != "queue_size" {
|
||||
if hasMaxWait {
|
||||
return nil, fmt.Errorf("%w: rateLimitPolicy.rules[%d].maxWaitSeconds is only valid for queue_size", ErrInvalidRateLimitPolicy, index)
|
||||
}
|
||||
continue
|
||||
}
|
||||
limit, hasLimit := numericRuleValue(rule, "limit")
|
||||
if !hasLimit {
|
||||
return nil, fmt.Errorf("%w: rateLimitPolicy.rules[%d].limit must be an integer", ErrInvalidRateLimitPolicy, index)
|
||||
}
|
||||
if limit != math.Trunc(limit) || limit < 0 || limit > MaxQueueSize {
|
||||
return nil, fmt.Errorf("%w: queue_size limit must be 0 or an integer between 1 and %d", ErrInvalidRateLimitPolicy, MaxQueueSize)
|
||||
}
|
||||
if limit == 0 {
|
||||
continue
|
||||
}
|
||||
if hasMaxWait && (maxWait != math.Trunc(maxWait) || maxWait < 1 || maxWait > MaxQueueWaitSeconds) {
|
||||
return nil, fmt.Errorf("%w: queue_size maxWaitSeconds must be an integer between 1 and %d", ErrInvalidRateLimitPolicy, MaxQueueWaitSeconds)
|
||||
}
|
||||
}
|
||||
return NormalizeRateLimitPolicy(policy), nil
|
||||
}
|
||||
|
||||
func numericRuleValue(rule map[string]any, key string) (float64, bool) {
|
||||
raw, ok := rule[key]
|
||||
if !ok || raw == nil {
|
||||
return 0, false
|
||||
}
|
||||
switch raw.(type) {
|
||||
case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
return floatValue(raw), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func ConcurrentPolicyCapacity(policy map[string]any) (int, bool) {
|
||||
limit, ok := RateLimitPolicyMetric(policy, "concurrent")
|
||||
if !ok || limit <= 0 {
|
||||
|
||||
Reference in New Issue
Block a user