新增基于任务状态、执行租约和 Worker 心跳的孤儿 River Job 精确识别,每 15 秒将确认失活的 Job 恢复为可重试,避免等待 River 一小时通用救援窗口。运行时恢复同步清除中断及终态任务的 admission、执行令牌和并发租约,防止状态残留污染队列指标。正常运行中的长任务仍由 running 状态和活跃心跳保护。验证通过完整 Go 测试、go vet、竞态检查、迁移安全检查和 PostgreSQL 18 集成测试。
84 lines
2.7 KiB
Go
84 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/httpapi"
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
// @title EasyAI AI Gateway API
|
|
// @version 0.1.0
|
|
// @description EasyAI AI Gateway 的本地鉴权、平台模型管理、定价、运行策略、钱包和 AI 任务接口。
|
|
// @description 受保护接口使用 Authorization: Bearer <JWT 或 API Key>,管理接口只接受 JWT 用户凭证。
|
|
// @BasePath /
|
|
// @schemes http https
|
|
// @securityDefinitions.apikey BearerAuth
|
|
// @in header
|
|
// @name Authorization
|
|
// @description Bearer JWT 或 API Key。
|
|
func main() {
|
|
cfg := config.Load()
|
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: cfg.LogLevel,
|
|
}))
|
|
if err := cfg.Validate(); err != nil {
|
|
logger.Error("invalid gateway configuration", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
db, err := store.ConnectWithMaxConns(ctx, cfg.DatabaseURL, cfg.DatabaseMaxConns)
|
|
if err != nil {
|
|
logger.Error("connect postgres failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
defer db.Close()
|
|
if recovery, err := db.RecoverInterruptedRuntimeState(ctx); err != nil {
|
|
logger.Error("recover interrupted runtime state failed", "error", err)
|
|
os.Exit(1)
|
|
} else if recovery.ReleasedConcurrencyLeases > 0 || recovery.ReleasedRateReservations > 0 || recovery.FailedAttempts > 0 || recovery.FailedTasks > 0 || recovery.RequeuedAsyncTasks > 0 || recovery.CleanedTaskAdmissions > 0 {
|
|
logger.Warn("interrupted runtime state recovered",
|
|
"releasedConcurrencyLeases", recovery.ReleasedConcurrencyLeases,
|
|
"releasedRateReservations", recovery.ReleasedRateReservations,
|
|
"failedAttempts", recovery.FailedAttempts,
|
|
"failedTasks", recovery.FailedTasks,
|
|
"requeuedAsyncTasks", recovery.RequeuedAsyncTasks,
|
|
"cleanedTaskAdmissions", recovery.CleanedTaskAdmissions,
|
|
)
|
|
}
|
|
|
|
server := &http.Server{
|
|
Addr: cfg.HTTPAddr,
|
|
Handler: httpapi.NewServerWithContext(ctx, cfg, db, logger),
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
logger.Info("easyai ai gateway api started", "addr", cfg.HTTPAddr)
|
|
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
logger.Error("http server failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
|
logger.Error("http shutdown failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
logger.Info("easyai ai gateway api stopped")
|
|
}
|