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
+39
View File
@@ -57,6 +57,8 @@ type Config struct {
GlobalHTTPProxySource string
LogLevel slog.Level
BillingEngineMode string
ProcessRole string
DatabaseMaxConns int
AsyncQueueWorkerEnabled bool
AsyncWorkerHardLimit int
AsyncWorkerRefreshIntervalSeconds int
@@ -113,6 +115,8 @@ func Load() Config {
GlobalHTTPProxySource: globalProxy.Source,
LogLevel: logLevel(env("LOG_LEVEL", "info")),
BillingEngineMode: strings.ToLower(env("BILLING_ENGINE_MODE", "observe")),
ProcessRole: strings.ToLower(strings.TrimSpace(env("AI_GATEWAY_PROCESS_ROLE", "all"))),
DatabaseMaxConns: envInt("AI_GATEWAY_DATABASE_MAX_CONNS", 0),
AsyncQueueWorkerEnabled: env("AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED", "true") == "true",
AsyncWorkerHardLimit: envIntValidated("AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT", 2048),
AsyncWorkerRefreshIntervalSeconds: envIntValidated("AI_GATEWAY_ASYNC_WORKER_REFRESH_INTERVAL_SECONDS", 5),
@@ -120,6 +124,14 @@ func Load() Config {
}
func (c Config) Validate() error {
switch strings.ToLower(strings.TrimSpace(c.ProcessRole)) {
case "", "all", "api", "worker":
default:
return errors.New("AI_GATEWAY_PROCESS_ROLE must be all, api, or worker")
}
if c.DatabaseMaxConns < 0 || c.DatabaseMaxConns > 1000 {
return errors.New("AI_GATEWAY_DATABASE_MAX_CONNS must be between 1 and 1000 when configured")
}
switch strings.ToLower(strings.TrimSpace(c.BillingEngineMode)) {
case "", "observe", "enforce", "hold":
default:
@@ -184,6 +196,33 @@ func (c Config) Validate() error {
return nil
}
func (c Config) EffectiveProcessRole() string {
role := strings.ToLower(strings.TrimSpace(c.ProcessRole))
if role == "" {
return "all"
}
return role
}
func (c Config) RunsPublicHTTP() bool {
return c.EffectiveProcessRole() != "worker"
}
func (c Config) RunsAsyncExecutionWorker() bool {
switch c.EffectiveProcessRole() {
case "api":
return false
case "worker":
return true
default:
return c.AsyncQueueWorkerEnabled
}
}
func (c Config) RunsBackgroundWorkers() bool {
return c.EffectiveProcessRole() != "api"
}
type GlobalHTTPProxyStatus struct {
HTTPProxy string
Source string
+39
View File
@@ -94,6 +94,45 @@ func TestLoadAsyncQueueWorkerEnabled(t *testing.T) {
}
}
func TestProcessRolePrecedenceAndCompatibility(t *testing.T) {
t.Setenv("AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED", "false")
cfg := Load()
if got := cfg.EffectiveProcessRole(); got != "all" {
t.Fatalf("legacy configuration changed process role to %q", got)
}
if cfg.RunsAsyncExecutionWorker() {
t.Fatal("legacy async worker disable was ignored")
}
t.Setenv("AI_GATEWAY_PROCESS_ROLE", "worker")
cfg = Load()
if got := cfg.EffectiveProcessRole(); got != "worker" {
t.Fatalf("effective process role = %q, want worker", got)
}
if cfg.RunsPublicHTTP() || !cfg.RunsAsyncExecutionWorker() || !cfg.RunsBackgroundWorkers() {
t.Fatalf("worker role capabilities are inconsistent: %+v", cfg)
}
t.Setenv("AI_GATEWAY_PROCESS_ROLE", "api")
cfg = Load()
if !cfg.RunsPublicHTTP() || cfg.RunsAsyncExecutionWorker() || cfg.RunsBackgroundWorkers() {
t.Fatalf("api role capabilities are inconsistent: %+v", cfg)
}
}
func TestValidateProcessRoleAndDatabasePool(t *testing.T) {
cfg := Load()
cfg.ProcessRole = "invalid"
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "PROCESS_ROLE") {
t.Fatalf("Validate() error = %v, want invalid process role", err)
}
cfg.ProcessRole = "api"
cfg.DatabaseMaxConns = -1
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "DATABASE_MAX_CONNS") {
t.Fatalf("Validate() error = %v, want invalid database max conns", err)
}
}
func TestValidateTaskHistorySettings(t *testing.T) {
cfg := Load()
cfg.TaskRetentionDays = 30