refactor(runner): 收敛上游失败决策与轮转策略
将同平台重试、跨平台动作、健康副作用和冷却排队统一到单一失败决策,避免旧降级策略与 failover 重复执行。\n\n增加事务级单源保护、候选实时刷新、冷却错误契约、管理接口严格校验及兼容策略只读展示。\n\n验证:真实 Gateway HTTP/PostgreSQL 接受测试 10 项通过;go test ./...、pnpm openapi、pnpm lint、pnpm test、pnpm build 均通过。
This commit is contained in:
@@ -3,6 +3,7 @@ package httpapi
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -71,6 +72,10 @@ func (s *Server) updateRunnerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if err := validateRunnerPolicyInput(input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
|
||||
return
|
||||
}
|
||||
item, err := s.store.UpsertDefaultRunnerPolicy(r.Context(), input)
|
||||
if err != nil {
|
||||
s.logger.Error("update runner policy failed", "error", err)
|
||||
@@ -80,6 +85,89 @@ func (s *Server) updateRunnerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func validateRunnerPolicyInput(input store.RunnerPolicyInput) error {
|
||||
rawRules, exists := input.FailoverPolicy["actionRules"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
rules, ok := rawRules.([]any)
|
||||
if !ok {
|
||||
return errors.New("failoverPolicy.actionRules must be an array")
|
||||
}
|
||||
validActions := map[string]bool{
|
||||
"stop": true, "next": true, "cooldown_and_next": true, "demote_and_next": true, "disable_and_next": true,
|
||||
}
|
||||
for index, rawRule := range rules {
|
||||
rule, ok := rawRule.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("failoverPolicy.actionRules[%d] must be an object", index)
|
||||
}
|
||||
action := strings.TrimSpace(stringValue(rule["action"]))
|
||||
if !validActions[action] {
|
||||
return fmt.Errorf("failoverPolicy.actionRules[%d].action is invalid", index)
|
||||
}
|
||||
if target, present := rule["target"]; present {
|
||||
targetValue := strings.TrimSpace(stringValue(target))
|
||||
if targetValue != "model" && targetValue != "platform" {
|
||||
return fmt.Errorf("failoverPolicy.actionRules[%d].target is invalid", index)
|
||||
}
|
||||
}
|
||||
if !runnerActionRuleHasMatch(rule) {
|
||||
return fmt.Errorf("failoverPolicy.actionRules[%d] must include at least one match condition", index)
|
||||
}
|
||||
if action == "cooldown_and_next" && positiveJSONInteger(rule["cooldownSeconds"]) <= 0 {
|
||||
return fmt.Errorf("failoverPolicy.actionRules[%d].cooldownSeconds must be a positive integer", index)
|
||||
}
|
||||
if action == "demote_and_next" && positiveJSONInteger(rule["demoteSteps"]) <= 0 {
|
||||
return fmt.Errorf("failoverPolicy.actionRules[%d].demoteSteps must be a positive integer", index)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runnerActionRuleHasMatch(rule map[string]any) bool {
|
||||
for _, key := range []string{"categories", "codes", "errorCodes", "statusCodes", "keywords"} {
|
||||
switch values := rule[key].(type) {
|
||||
case []any:
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(fmt.Sprint(value)) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func positiveJSONInteger(value any) int {
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
if typed > 0 {
|
||||
return typed
|
||||
}
|
||||
case int64:
|
||||
if typed > 0 {
|
||||
return int(typed)
|
||||
}
|
||||
case float64:
|
||||
if typed > 0 && typed == float64(int(typed)) {
|
||||
return int(typed)
|
||||
}
|
||||
case json.Number:
|
||||
parsed, err := typed.Int64()
|
||||
if err == nil && parsed > 0 {
|
||||
return int(parsed)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type updatePlatformDynamicPriorityRequest struct {
|
||||
DynamicPriority *int `json:"dynamicPriority" example:"10"`
|
||||
Reset bool `json:"reset" example:"false"`
|
||||
|
||||
Reference in New Issue
Block a user