feat(worker): 实现集群限流与自适应负载
保留平台模型 RPM、TPM 和并发策略语义,增加 PostgreSQL 集群级租约、饱和候选重选和多平台自动负载,避免突发任务固定等待首个平台。\n\n新增 Worker 实时负载采样、自适应 active/heavy 容量、心跳与管理端指标,并扩展本地 acceptance runner,覆盖三 Worker、同模型三平台 2/4/6 并发和 48 个带图视频突发任务。\n\n验证:go test ./...、go vet ./...、PostgreSQL 跨 Store 集成测试、gofmt、bash -n、ShellCheck 及本地集群 provider-burst 验收通过;48/48 成功,无越限、重复提交、重复计费、重复回调或终态资源泄漏。
This commit is contained in:
@@ -59,6 +59,7 @@ type Server struct {
|
||||
type Report struct {
|
||||
GeminiRequests int64 `json:"geminiRequests"`
|
||||
GeminiInvalid int64 `json:"geminiInvalid"`
|
||||
GeminiInputImages int64 `json:"geminiInputImages"`
|
||||
GeminiInputBytes int64 `json:"geminiInputBytes"`
|
||||
GeminiOutputBytes int64 `json:"geminiOutputBytes"`
|
||||
VideoSubmissions int64 `json:"videoSubmissions"`
|
||||
@@ -163,7 +164,7 @@ func (s *Server) geminiGenerateContent(w http.ResponseWriter, r *http.Request) {
|
||||
writeProtocolError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
inputBytes, err := validateGeminiImageRequest(body)
|
||||
inputBytes, inputImages, err := validateGeminiImageRequest(body)
|
||||
if err != nil {
|
||||
s.recordGeminiInvalid()
|
||||
writeProtocolError(w, http.StatusBadRequest, err.Error())
|
||||
@@ -180,6 +181,7 @@ func (s *Server) geminiGenerateContent(w http.ResponseWriter, r *http.Request) {
|
||||
s.geminiIdempotency[idempotencyKey] = struct{}{}
|
||||
}
|
||||
s.report.GeminiRequests++
|
||||
s.report.GeminiInputImages += int64(inputImages)
|
||||
s.report.GeminiInputBytes += int64(inputBytes)
|
||||
s.report.GeminiOutputBytes += int64(outputBytes)
|
||||
s.mu.Unlock()
|
||||
@@ -533,7 +535,7 @@ func decodeImageDataURL(raw string) ([]byte, error) {
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func validateGeminiImageRequest(body map[string]any) (int, error) {
|
||||
func validateGeminiImageRequest(body map[string]any) (int, int, error) {
|
||||
generationConfig, _ := body["generationConfig"].(map[string]any)
|
||||
modalities, _ := generationConfig["responseModalities"].([]any)
|
||||
hasImageModality := false
|
||||
@@ -543,9 +545,10 @@ func validateGeminiImageRequest(body map[string]any) (int, error) {
|
||||
}
|
||||
}
|
||||
if !hasImageModality {
|
||||
return 0, errors.New("generationConfig.responseModalities must contain IMAGE")
|
||||
return 0, 0, errors.New("generationConfig.responseModalities must contain IMAGE")
|
||||
}
|
||||
total := 0
|
||||
images := 0
|
||||
contents, _ := body["contents"].([]any)
|
||||
for _, rawContent := range contents {
|
||||
content, _ := rawContent.(map[string]any)
|
||||
@@ -558,22 +561,41 @@ func validateGeminiImageRequest(body map[string]any) (int, error) {
|
||||
}
|
||||
encoded := strings.TrimSpace(stringValue(inline["data"]))
|
||||
if encoded == "" {
|
||||
fileData, _ := part["fileData"].(map[string]any)
|
||||
if fileData == nil {
|
||||
fileData, _ = part["file_data"].(map[string]any)
|
||||
}
|
||||
fileURI := strings.TrimSpace(stringValue(fileData["fileUri"]))
|
||||
if fileURI == "" {
|
||||
fileURI = strings.TrimSpace(stringValue(fileData["file_uri"]))
|
||||
}
|
||||
if fileURI == "" {
|
||||
fileURI = strings.TrimSpace(stringValue(fileData["uri"]))
|
||||
}
|
||||
if fileURI == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(fileURI, "http://") && !strings.HasPrefix(fileURI, "https://") {
|
||||
return 0, 0, errors.New("Gemini fileData URI must use HTTP or HTTPS")
|
||||
}
|
||||
images++
|
||||
continue
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Gemini inlineData is not valid Base64: %w", err)
|
||||
return 0, 0, fmt.Errorf("Gemini inlineData is not valid Base64: %w", err)
|
||||
}
|
||||
if len(decoded) == 0 {
|
||||
return 0, errors.New("Gemini inlineData is empty")
|
||||
return 0, 0, errors.New("Gemini inlineData is empty")
|
||||
}
|
||||
total += len(decoded)
|
||||
images++
|
||||
}
|
||||
}
|
||||
if total == 0 {
|
||||
return 0, errors.New("Gemini request has no inlineData image")
|
||||
if images == 0 {
|
||||
return 0, 0, errors.New("Gemini request has no inlineData or fileData image")
|
||||
}
|
||||
return total, nil
|
||||
return total, images, nil
|
||||
}
|
||||
|
||||
func (s *Server) recordGeminiInvalid() {
|
||||
|
||||
Reference in New Issue
Block a user