feat(queue): 增加非文本模型分布式准入队列
使用 PostgreSQL 统一同步与异步非文本任务的并发准入、持久化等待和 Worker 容量分配,并将生产 API 与独立 Worker 角色拆分。 补充策略管理、共享契约、OpenAPI、Kubernetes 双节点 Worker 清单及跨节点验收脚本;未默认启用任何生产 queue_size 策略。 已在原基线完成 Go、前端、迁移、Shell、Kustomize 与长任务容量验收;合入最新主干后将重新执行发布门禁。
This commit is contained in:
@@ -345,6 +345,10 @@ func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
|
||||
input.Config = config
|
||||
platform, err := s.store.CreatePlatform(r.Context(), input)
|
||||
if err != nil {
|
||||
if store.IsInvalidRateLimitPolicy(err) {
|
||||
writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
|
||||
return
|
||||
}
|
||||
s.logger.Error("create platform failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "create platform failed")
|
||||
return
|
||||
@@ -398,6 +402,10 @@ func (s *Server) updatePlatform(w http.ResponseWriter, r *http.Request) {
|
||||
input.Config = config
|
||||
platform, err := s.store.UpdatePlatform(r.Context(), r.PathValue("platformID"), input)
|
||||
if err != nil {
|
||||
if store.IsInvalidRateLimitPolicy(err) {
|
||||
writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
|
||||
return
|
||||
}
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "platform not found")
|
||||
return
|
||||
@@ -471,7 +479,7 @@ func (s *Server) createPlatformModel(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
model, err := s.store.CreatePlatformModel(r.Context(), input)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrInvalidPlatformModelConfiguration) {
|
||||
if errors.Is(err, store.ErrInvalidPlatformModelConfiguration) || store.IsInvalidRateLimitPolicy(err) {
|
||||
writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
|
||||
return
|
||||
}
|
||||
@@ -519,7 +527,7 @@ func (s *Server) replacePlatformModels(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
models, err := s.store.ReplacePlatformModels(r.Context(), platformID, input.Models)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrInvalidPlatformModelConfiguration) {
|
||||
if errors.Is(err, store.ErrInvalidPlatformModelConfiguration) || store.IsInvalidRateLimitPolicy(err) {
|
||||
writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
|
||||
return
|
||||
}
|
||||
@@ -1206,8 +1214,9 @@ func (s *Server) createTask(kind string, compatible bool) http.Handler {
|
||||
return
|
||||
}
|
||||
if responsePlan.asyncMode {
|
||||
if err := s.runner.EnqueueAsyncTask(r.Context(), task); err != nil {
|
||||
writeTaskError(http.StatusInternalServerError, err.Error(), nil, "enqueue_failed")
|
||||
if err := s.runner.SubmitAsyncTask(r.Context(), task); err != nil {
|
||||
applyRunErrorHeaders(w, err)
|
||||
writeTaskError(statusFromRunError(err), runErrorMessage(err), runErrorDetails(err), runErrorCode(err))
|
||||
return
|
||||
}
|
||||
writeTaskAccepted(w, task)
|
||||
@@ -1307,7 +1316,7 @@ func openAIEmbeddingsDoc() {}
|
||||
|
||||
// openAIImagesDoc godoc
|
||||
// @Summary 创建或编辑 OpenAI Images
|
||||
// @Description 默认同步返回 OpenAI-compatible Images 响应;设置 X-Async=true 时异步创建任务并返回 202。
|
||||
// @Description 默认同步返回 OpenAI-compatible Images 响应;非文本模型并发饱和且配置队列时等待准入,超过最长等待返回 504 queue_timeout;设置 X-Async=true 时异步创建任务并返回 202。
|
||||
// @Tags images
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -1319,6 +1328,7 @@ func openAIEmbeddingsDoc() {}
|
||||
// @Failure 400 {object} OpenAIErrorEnvelope
|
||||
// @Failure 401 {object} OpenAIErrorEnvelope
|
||||
// @Failure 429 {object} OpenAIErrorEnvelope
|
||||
// @Failure 504 {object} OpenAIErrorEnvelope
|
||||
// @Failure 502 {object} OpenAIErrorEnvelope
|
||||
// @Router /api/v1/images/generations [post]
|
||||
// @Router /api/v1/images/edits [post]
|
||||
@@ -1326,7 +1336,7 @@ func openAIImagesDoc() {}
|
||||
|
||||
// easyAIMediaTasksDoc godoc
|
||||
// @Summary 创建 EasyAI 媒体任务
|
||||
// @Description 默认同步返回 EasyAI GeneratedResponse;设置 X-Async=true 时返回同时兼容 Gateway 与 server-main EasyAIClient 的异步提交结构。
|
||||
// @Description 默认同步返回 EasyAI GeneratedResponse;非文本模型并发饱和且配置队列时等待准入,超过最长等待返回 504 queue_timeout;设置 X-Async=true 时返回同时兼容 Gateway 与 server-main EasyAIClient 的异步提交结构。
|
||||
// @Tags media
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -1342,6 +1352,7 @@ func openAIImagesDoc() {}
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Failure 429 {object} ErrorEnvelope
|
||||
// @Failure 504 {object} ErrorEnvelope
|
||||
// @Failure 502 {object} ErrorEnvelope
|
||||
// @Router /api/v1/videos/generations [post]
|
||||
// @Router /api/v1/song/generations [post]
|
||||
@@ -1351,19 +1362,10 @@ func openAIImagesDoc() {}
|
||||
func easyAIMediaTasksDoc() {}
|
||||
|
||||
func (s *Server) requestExecutionContext(r *http.Request) (context.Context, context.CancelFunc) {
|
||||
base := context.WithoutCancel(r.Context())
|
||||
if s.ctx == nil {
|
||||
return base, func() {}
|
||||
return runner.NewRequestExecutionContext(r.Context(), context.Background())
|
||||
}
|
||||
ctx, cancel := context.WithCancel(base)
|
||||
go func() {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
return ctx, cancel
|
||||
return runner.NewRequestExecutionContext(r.Context(), s.ctx)
|
||||
}
|
||||
|
||||
func requestStillConnected(r *http.Request) bool {
|
||||
@@ -1663,6 +1665,8 @@ func statusFromRunError(err error) int {
|
||||
return http.StatusNotFound
|
||||
case store.ModelCandidateErrorCode(err) == "platform_cooling_down" || store.ModelCandidateErrorCode(err) == "model_cooling_down":
|
||||
return http.StatusTooManyRequests
|
||||
case errors.Is(err, store.ErrQueueTimeout) || clients.ErrorCode(err) == "queue_timeout":
|
||||
return http.StatusGatewayTimeout
|
||||
case errors.Is(err, store.ErrNoModelCandidate):
|
||||
return http.StatusNotFound
|
||||
case errors.Is(err, store.ErrRateLimited):
|
||||
@@ -1710,7 +1714,11 @@ func runErrorDetails(err error) map[string]any {
|
||||
}
|
||||
|
||||
func applyRunErrorHeaders(w http.ResponseWriter, err error) {
|
||||
if retryAfter := store.ModelCandidateRetryAfter(err); retryAfter > 0 {
|
||||
retryAfter := store.ModelCandidateRetryAfter(err)
|
||||
if limitRetryAfter := store.RateLimitRetryAfter(err); limitRetryAfter > 0 {
|
||||
retryAfter = limitRetryAfter
|
||||
}
|
||||
if retryAfter > 0 {
|
||||
seconds := int((retryAfter + time.Second - 1) / time.Second)
|
||||
if seconds < 1 {
|
||||
seconds = 1
|
||||
@@ -1783,6 +1791,13 @@ func rateLimitErrorDetail(err error) map[string]any {
|
||||
"limit": limitErr.Limit,
|
||||
},
|
||||
}
|
||||
if limitErr.Reason != "" {
|
||||
detail["reason"] = limitErr.Reason
|
||||
}
|
||||
if limitErr.QueueLimit > 0 {
|
||||
detail["queueDepth"] = limitErr.QueueDepth
|
||||
detail["queueLimit"] = limitErr.QueueLimit
|
||||
}
|
||||
if limitErr.RetryAfter > 0 {
|
||||
detail["retryAfterMs"] = limitErr.RetryAfter.Milliseconds()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user