Files
easyai-ai-gateway/apps/api/internal/store/rate_limit_policy.go
T
wangbo 9e4fc7362d feat(queue): 增加非文本模型分布式准入队列
使用 PostgreSQL 统一同步与异步非文本任务的并发准入、持久化等待和 Worker 容量分配,并将生产 API 与独立 Worker 角色拆分。

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

已在原基线完成 Go、前端、迁移、Shell、Kustomize 与长任务容量验收;合入最新主干后将重新执行发布门禁。
2026-07-29 16:15:43 +08:00

283 lines
8.3 KiB
Go

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 {
BasePolicy map[string]any
PlatformPolicy map[string]any
RuntimePolicy map[string]any
RuntimePolicyExplicit bool
RuntimePolicyOverride map[string]any
ModelPolicy map[string]any
ModelPolicyMode string
}
// EffectiveRateLimitPolicy resolves one authoritative policy. Platform
// policies are defaults for every bound model; explicit model/runtime settings
// replace the complete policy instead of merging individual metrics.
func EffectiveRateLimitPolicy(input EffectiveRateLimitPolicyInput) map[string]any {
policy := input.BasePolicy
if policySpecified(input.PlatformPolicy) {
policy = input.PlatformPolicy
}
if input.RuntimePolicyExplicit {
policy = input.RuntimePolicy
}
if raw, ok := input.RuntimePolicyOverride["rateLimitPolicy"]; ok {
policy, _ = raw.(map[string]any)
}
mode := NormalizeRateLimitPolicyMode(input.ModelPolicyMode, input.ModelPolicy != nil)
if mode == RateLimitPolicyModeOverride {
policy = input.ModelPolicy
}
return NormalizeRateLimitPolicy(policy)
}
func NormalizeRateLimitPolicyMode(mode string, policyProvided bool) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case RateLimitPolicyModeOverride:
return RateLimitPolicyModeOverride
case RateLimitPolicyModeInherit:
return RateLimitPolicyModeInherit
default:
if policyProvided {
return RateLimitPolicyModeOverride
}
return RateLimitPolicyModeInherit
}
}
func RateLimitPolicyMetric(policy map[string]any, metric string) (float64, bool) {
rules, _ := NormalizeRateLimitPolicy(policy)["rules"].([]any)
for _, rawRule := range rules {
rule, _ := rawRule.(map[string]any)
if strings.TrimSpace(stringValue(rule["metric"])) != metric {
continue
}
value := floatValue(rule["limit"])
return value, true
}
return 0, false
}
// NormalizeRateLimitPolicy keeps the canonical rules contract while accepting
// policies imported from server-main before that contract existed. Runtime
// enforcement and worker sizing must agree on these legacy limits; otherwise a
// configured max_concurrent_requests silently becomes unlimited.
func NormalizeRateLimitPolicy(policy map[string]any) map[string]any {
if policy == nil {
return nil
}
out := clonePolicy(policy)
rules, _ := out["rules"].([]any)
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,
})
}
}
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 {
return 0, false
}
if limit < 1 {
return 1, true
}
if limit >= float64(math.MaxInt) {
return math.MaxInt, true
}
return int(math.Floor(limit)), true
}
func policySpecified(policy map[string]any) bool {
rules, _ := NormalizeRateLimitPolicy(policy)["rules"].([]any)
return len(rules) > 0
}
func lowestPositiveLegacyLimit(scopes []map[string]any, keys ...string) (float64, bool) {
limit := 0.0
found := false
for _, scope := range scopes {
for _, key := range keys {
value := floatValue(scope[key])
if value <= 0 {
continue
}
if !found || value < limit {
limit = value
found = true
}
}
}
return limit, found
}
func clonePolicy(policy map[string]any) map[string]any {
if policy == nil {
return nil
}
out := make(map[string]any, len(policy))
for key, value := range policy {
out[key] = value
}
return out
}