Skip to content

增加渠道重试优先级模式(Round-robin 和 Sequential) - #2724

Closed
zhang2023san-oss wants to merge 2 commits into
QuantumNous:mainfrom
zhang2023san-oss:feature/retry-priority-modes
Closed

增加渠道重试优先级模式(Round-robin 和 Sequential)#2724
zhang2023san-oss wants to merge 2 commits into
QuantumNous:mainfrom
zhang2023san-oss:feature/retry-priority-modes

Conversation

@zhang2023san-oss

@zhang2023san-oss zhang2023san-oss commented Jan 23, 2026

Copy link
Copy Markdown

功能描述

本 PR 包含两个相关的渠道重试增强功能:

  1. 重试时避开已尝试渠道(Commit: bfe3478

在渠道重试过程中,系统会自动排除已经尝试过的渠道,避免重复请求同一个失败的渠道。

功能特性:

  • 新增配置选项 RetryAvoidUsedChannelEnabled,可在运营设置中开启/关闭
  • 重试时自动记录已使用的渠道 ID
  • 在选择下一个渠道时排除已使用的渠道
  • 前端新增开关按钮,支持动态配置
  1. 渠道重试优先级模式(Commit: 5dd455d

实现了两种渠道重试的优先级模式,用户可根据业务需求选择:

Sequential(顺序模式):

  • 同一优先级内尝试所有渠道后才降级到下一优先级
  • 例如:A1 → A2 → A3 → B1 → B2 → B3
  • 适合希望充分利用高优先级渠道的场景

Round-robin(轮询模式):

  • 每个优先级轮流尝试,实现负载均衡
  • 例如:A1 → B1 → C1 → A2 → B2 → C2
  • 适合希望分散请求压力的场景

实现细节

功能 1: 重试避开已尝试渠道

修改的文件:

  1. common/constants.go
    - 新增 RetryAvoidUsedChannelEnabled 配置常量
  2. model/option.go
    - 添加配置项的加载和更新逻辑
  3. controller/relay.go
    - 在重试循环中记录已使用的渠道 ID
    - 将排除列表传递给渠道选择函数
  4. service/channel_select.go
    - 新增 UsedChannelIds 字段到 RetryParam 结构体
    - 将排除列表传递给底层查询函数
  5. model/ability.go & model/channel_cache.go
    - 在渠道查询时应用排除逻辑
    - 使用 NOT IN 子句过滤已使用的渠道
  6. 前端文件
    - web/src/pages/Setting/Operation/SettingsMonitoring.jsx - 新增开关按钮
    - web/src/i18n/locales/zh.json & en.json - 国际化文本

功能 2: 渠道重试优先级模式

修改的文件:

  1. model/option.go (line 414-417)
    - 修复了 RetryPriorityMode 配置加载问题
    - 原本错误地放在了只处理 "Enabled" 结尾配置项的条件块内
  2. model/ability.go (line 82-92)
    - 在 getPriority 函数中添加了 round-robin 模式的模运算逻辑
    - 确保在轮询模式下正确循环优先级
  3. model/channel_cache.go (line 149-155)
    - 在 GetRandomSatisfiedChannel 函数中添加了 round-robin 模式的模运算逻辑
    - 支持内存缓存场景下的轮询模式
  4. controller/relay.go (line 229-237)
    - 移除了 round-robin 模式下的 IncreasePriorityIndex() 调用
    - 避免双重优先级切换导致的跳过问题
  5. service/channel_select.go (line 184-201)
    - 区分两种模式的参数传递
    - Round-robin 模式传递 retry 参数
    - Sequential 模式传递 priorityIndex 参数
  6. 前端文件
    - web/src/pages/Setting/Operation/SettingsMonitoring.jsx - 优先级模式选择器
    - web/src/i18n/locales/zh.json & en.json - 国际化文本

核心逻辑

Round-robin 模式:
if common.RetryPriorityMode == "round-robin" && len(priorities) > 0 {
priorityToUse = priorities[retry%len(priorities)]
}

Sequential 模式:
if retry >= len(priorities) {
priorityToUse = priorities[len(priorities)-1]
} else {
priorityToUse = priorities[retry]
}

渠道排除逻辑:
if len(excludeIds) > 0 {
var excludeIdList []int
for id := range excludeIds {
excludeIdList = append(excludeIdList, id)
}
channelQuery = channelQuery.Where("channel_id NOT IN ?", excludeIdList)
}

测试结果

功能 1: 重试避开已尝试渠道

  • ✅ 开启功能后,重试时不会选择已失败的渠道
  • ✅ 日志正确显示排除的渠道数量
  • ✅ 前端开关可正常切换配置

功能 2: 渠道重试优先级模式

测试环境:

  • 4 个渠道:2, 3 (优先级 10),5, 6 (优先级 0)
  • 2 个唯一优先级:[10, 0]

Round-robin 模式测试结果:
retry=0 → 渠道 #2 (优先级 10) ✓
retry=1 → 渠道 #5 (优先级 0) ✓
retry=2 → 渠道 #3 (优先级 10) ✓
retry=3 → 渠道 #6 (优先级 0) ✓

符合预期的轮询模式:在优先级之间交替选择。

相关 Issue

(如果有相关 issue,请在此处引用)

检查清单

  • 代码已通过编译
  • 已在本地测试 round-robin 模式
  • 已在本地测试渠道排除功能
  • 已移除调试日志
  • 需要测试 sequential 模式(建议维护者测试)
  • 代码符合项目规范
  • 已更新相关文档(前端国际化文件)

备注

  1. 功能 1 提供了基础的渠道排除能力,避免重复请求失败的渠道
  2. 功能 2 修复了 round-robin 模式的核心逻辑问题,确保在重试时能够正确地在不同优先级之间轮换
  3. 两个功能可以独立使用,也可以组合使用以获得最佳的重试效果

Summary by CodeRabbit

Release Notes

  • New Features
    • Added "Avoid Used Channels on Retry" option to prevent re-selecting previously failed channels during retries, improving retry efficiency.
    • Introduced "Retry Priority Mode" configuration with two options: Sequential (default) for exhaustive retry at each priority level, and Round-robin for cycling through priority levels.
    • Both options are configurable in the monitoring settings panel with multilingual support.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

The PR introduces configuration-driven retry behavior for channel selection, adding support for avoiding previously-failed channels and choosing between sequential or round-robin priority modes. New global variables, database query filters, state tracking, and UI controls are added across backend and frontend layers.

Changes

Cohort / File(s) Summary
Configuration & Constants
common/constants.go, model/option.go
Introduces RetryAvoidUsedChannelEnabled (bool, default false) and RetryPriorityMode (string, default "sequential") as global configuration variables with option map bindings and validation for "sequential"/"round-robin" values.
Channel Selection Core Logic
controller/relay.go, model/ability.go, model/channel_cache.go
Implements exclusion set filtering in channel queries, supports round-robin priority cycling via modulo arithmetic, and advances to next priority when all channels in current priority are excluded. GetChannel and GetRandomSatisfiedChannel now accept excludeIds parameter and return nil instead of error when no channels remain after filtering.
Service Layer
service/channel_select.go
Extends RetryParam with UsedChannelIds map, CurrentPriorityIndex tracker, and helper methods (AddUsedChannel, IsChannelUsed, IncreasePriorityIndex, GetPriorityIndex). Threads excludeIds parameter through GetRandomSatisfiedChannel calls and conditionally uses round-robin vs sequential retry mode.
Frontend Settings UI
web/src/components/settings/OperationSetting.jsx, web/src/pages/Setting/Operation/SettingsMonitoring.jsx
Adds RetryAvoidUsedChannelEnabled (Switch toggle) and RetryPriorityMode (Select dropdown with "sequential"/"round-robin" options) to monitoring settings component with explanatory labels.
Localization
web/src/i18n/locales/en.json, web/src/i18n/locales/zh.json
Provides English and Chinese translations for retry feature labels, descriptions, and mode options ("Avoid used channels on retry", "Retry Priority Mode", "Sequential"/"Round-robin" descriptions).
Verification Script
verify-changes.sh
Adds comprehensive shell-based validation covering backend symbol presence, frontend component updates, documentation checks, Go syntax validation, and frontend build verification with colorized output.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Relay Handler
    participant Service as Channel Select
    participant Model as Channel Selection
    participant DB as Database
    
    Client->>Service: Start retry loop with RetryParam
    loop Until channel found or retries exhausted
        Service->>Service: Determine priority (round-robin or sequential)
        Service->>Model: GetRandomSatisfiedChannel(group, model, retry, usedIds)
        
        alt RetryAvoidUsedChannelEnabled && usedIds provided
            Model->>DB: Query channels WHERE NOT IN (usedIds) <br/> ORDER BY priority, weight DESC
            DB-->>Model: Return candidates
            Model->>Model: Filter non-excluded channels
        else Normal path
            Model->>DB: Query channels ORDER BY priority, weight DESC
            DB-->>Model: Return candidates
        end
        
        alt Channel found
            Model-->>Service: Return channel
            Service->>Service: AddUsedChannel(channelId)
            Service-->>Client: Return selected channel
        else No channels in priority (all excluded)
            Model-->>Service: Return nil, nil
            Service->>Service: IncreasePriorityIndex()
            Service->>Service: Retry next priority
        else All priorities exhausted
            Service-->>Client: Error - no usable channels
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • xyfacai

Poem

🐰 Hops through retry channels with glee,
No second chances for failed paths, you see!
Round-robin or sequential, the choice is divine,
Each priority gets its moment to shine!
Less wasted attempts, more channels to try,
A smoother path forward—reaching for the sky!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding retry priority modes (Round-robin and Sequential) for channel retries.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
service/channel_select.go (1)

147-158: Auto-group nil handling now skips remaining priorities.

With exclusions enabled, GetRandomSatisfiedChannel can return nil because the current priority is exhausted, not the entire group. The current logic immediately advances to the next group and resets retry, which skips remaining priorities in the same group.

🛠️ Suggested fix (stay in same group and let retry advance priority)
-        channel, _ = model.GetRandomSatisfiedChannel(autoGroup, param.ModelName, priorityRetry, param.UsedChannelIds)
-        if channel == nil {
-            // Current group has no available channel for this model, try next group
-            logger.LogDebug(param.Ctx, "No available channel in group %s for model %s at priorityRetry %d, trying next group", autoGroup, param.ModelName, priorityRetry)
-            common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1)
-            common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupRetryIndex, 0)
-            param.SetRetry(0)
-            continue
-        }
+        channel, _ = model.GetRandomSatisfiedChannel(autoGroup, param.ModelName, priorityRetry, param.UsedChannelIds)
+        if channel == nil {
+            // Priority exhausted (possibly due to exclusions); stay in same group and let outer retry advance priority
+            logger.LogDebug(param.Ctx, "Priority exhausted in group %s for model %s at priorityRetry %d, retrying same group", autoGroup, param.ModelName, priorityRetry)
+            common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i)
+            return nil, selectGroup, nil
+        }
🤖 Fix all issues with AI agents
In `@controller/relay.go`:
- Around line 331-334: RelayTask currently dereferences channel.Id after calling
getChannel, but getChannel can return (nil, nil) when a priority is exhausted;
add a nil check for channel immediately after the getChannel call in RelayTask
and handle that case (e.g., skip to the next priority loop iteration or return
gracefully) instead of dereferencing channel.Id, ensuring any subsequent logic
that expects a non-nil channel is only executed when channel != nil; refer to
the getChannel call and the channel.Id usage inside RelayTask to locate where to
add this guard.

In `@model/option.go`:
- Around line 414-417: The "RetryPriorityMode" branch currently ignores invalid
values and only updates common.RetryPriorityMode for known values, allowing bad
values to be persisted; change the handler in the switch case for
"RetryPriorityMode" to validate the incoming value and return an error when it
is not "sequential" or "round-robin" so the caller can abort updating
OptionMap/DB, and only set common.RetryPriorityMode when the value is valid;
ensure the surrounding function (the option-setting function that contains the
switch for "RetryPriorityMode") propagates this error back to the caller so
persistence is skipped on invalid input.

In `@verify-changes.sh`:
- Around line 122-133: The script verify-changes.sh uses absolute local file
paths in the check_file_exists calls which will break on other machines/CI;
update the four check_file_exists invocations (the ones checking the
开发文档/自测文档/部署指南/变更总结) to compute repo-relative paths (e.g., based on the script's
directory or a $REPO_ROOT env var) or guard them behind an optional env flag
(e.g., SKIP_DOC_CHECKS) so CI and other devs can run the script without those
user-specific /Users/... paths.
- Around line 139-156: Update the Go version check to match go.mod by changing
REQUIRED_VERSION from "1.24.0" to "1.25.1", enable reliable exit-code checks by
adding "set -o pipefail" before running the build, and replace the unreliable
"go build ... | grep -q 'error'" logic with an exit-status based test around the
go build invocation (use the same go build command that creates
/tmp/new-api-test); adjust the if/else accordingly to treat a non-zero exit as
failure and remove the grep filtering.

Comment thread controller/relay.go
Comment on lines 331 to +334
if channel == nil {
return nil, types.NewError(fmt.Errorf("分组 %s 下模型 %s 的可用渠道不存在(retry)", selectGroup, info.OriginModelName), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
// 该优先级的所有渠道都被排除,返回 nil 以便继续尝试下一个优先级
// All channels at this priority have been excluded, return nil to continue trying next priority
return nil, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard against nil channel in RelayTask to avoid panic.

getChannel can now return (nil, nil) when a priority is exhausted by exclusions. RelayTask dereferences channel.Id without a nil check, which can panic in this new path.

🛠️ Suggested fix (handle nil channel in RelayTask)
--- a/controller/relay.go
+++ b/controller/relay.go
@@
 	for ; shouldRetryTaskRelay(c, channelId, taskErr, retryTimes) && retryParam.GetRetry() < retryTimes; retryParam.IncreaseRetry() {
 		channel, newAPIError := getChannel(c, relayInfo, retryParam)
 		if newAPIError != nil {
 			logger.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", newAPIError.Error()))
 			taskErr = service.TaskErrorWrapperLocal(newAPIError.Err, "get_channel_failed", http.StatusInternalServerError)
 			break
 		}
+		if channel == nil {
+			retryParam.IncreasePriorityIndex()
+			logger.LogInfo(c, fmt.Sprintf("priority exhausted for task retry, switching priority index=%d", retryParam.GetPriorityIndex()))
+			continue
+		}
 		channelId = channel.Id
🤖 Prompt for AI Agents
In `@controller/relay.go` around lines 331 - 334, RelayTask currently dereferences
channel.Id after calling getChannel, but getChannel can return (nil, nil) when a
priority is exhausted; add a nil check for channel immediately after the
getChannel call in RelayTask and handle that case (e.g., skip to the next
priority loop iteration or return gracefully) instead of dereferencing
channel.Id, ensuring any subsequent logic that expects a non-nil channel is only
executed when channel != nil; refer to the getChannel call and the channel.Id
usage inside RelayTask to locate where to add this guard.

Comment thread model/option.go
Comment on lines +414 to +417
case "RetryPriorityMode":
if value == "sequential" || value == "round-robin" {
common.RetryPriorityMode = value
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject invalid RetryPriorityMode to avoid config drift.

Right now invalid values are silently accepted into OptionMap/DB while common.RetryPriorityMode stays unchanged, which can desync UI vs runtime behavior. Consider returning an error (and keeping OptionMap consistent) when the value is invalid.

🛠️ Suggested fix
-import (
-	"strconv"
-	"strings"
-	"time"
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
@@
 	case "RetryPriorityMode":
-		if value == "sequential" || value == "round-robin" {
-			common.RetryPriorityMode = value
-		}
+		if value == "sequential" || value == "round-robin" {
+			common.RetryPriorityMode = value
+		} else {
+			common.OptionMap[key] = common.RetryPriorityMode
+			return fmt.Errorf("invalid RetryPriorityMode: %s", value)
+		}
🤖 Prompt for AI Agents
In `@model/option.go` around lines 414 - 417, The "RetryPriorityMode" branch
currently ignores invalid values and only updates common.RetryPriorityMode for
known values, allowing bad values to be persisted; change the handler in the
switch case for "RetryPriorityMode" to validate the incoming value and return an
error when it is not "sequential" or "round-robin" so the caller can abort
updating OptionMap/DB, and only set common.RetryPriorityMode when the value is
valid; ensure the surrounding function (the option-setting function that
contains the switch for "RetryPriorityMode") propagates this error back to the
caller so persistence is skipped on invalid input.

Comment thread verify-changes.sh
Comment on lines +122 to +133
# 检查文档文件
check_file_exists "/Users/liyifan20/Documents/个人知识库/百度/10、项目介绍/new-api/功能开发/02-渠道重试避开已用渠道-开发文档.md" \
"开发文档存在"

check_file_exists "/Users/liyifan20/Documents/个人知识库/百度/10、项目介绍/new-api/功能开发/03-渠道重试避开已用渠道-自测文档.md" \
"自测文档存在"

check_file_exists "/Users/liyifan20/Documents/个人知识库/百度/10、项目介绍/new-api/功能开发/04-部署和测试指南.md" \
"部署指南存在"

check_file_exists "/Users/liyifan20/Documents/个人知识库/百度/10、项目介绍/new-api/功能开发/05-代码变更总结.md" \
"变更总结存在"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid absolute local paths in repo scripts.

These /Users/... paths will fail for anyone else and in CI. Consider repo‑relative paths or make these checks optional via env vars.

♻️ Suggested adjustment (repo-relative paths + optional checks)
- check_file_exists "/Users/liyifan20/Documents/个人知识库/百度/10、项目介绍/new-api/功能开发/02-渠道重试避开已用渠道-开发文档.md" \
-     "开发文档存在"
+ check_file_exists "docs/02-渠道重试避开已用渠道-开发文档.md" \
+     "开发文档存在 (repo)"
🤖 Prompt for AI Agents
In `@verify-changes.sh` around lines 122 - 133, The script verify-changes.sh uses
absolute local file paths in the check_file_exists calls which will break on
other machines/CI; update the four check_file_exists invocations (the ones
checking the 开发文档/自测文档/部署指南/变更总结) to compute repo-relative paths (e.g., based on
the script's directory or a $REPO_ROOT env var) or guard them behind an optional
env flag (e.g., SKIP_DOC_CHECKS) so CI and other devs can run the script without
those user-specific /Users/... paths.

Comment thread verify-changes.sh
Comment on lines +139 to +156
# 检查 Go 语法(如果 Go 可用)
if command -v go &> /dev/null; then
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
echo -e "${YELLOW}当前 Go 版本: $GO_VERSION${NC}"

# 检查版本是否满足要求
REQUIRED_VERSION="1.24.0"
if [ "$(printf '%s\n' "$REQUIRED_VERSION" "$GO_VERSION" | sort -V | head -n1)" = "$REQUIRED_VERSION" ]; then
echo -e "${GREEN}✓${NC} Go 版本满足要求 (>= 1.24.0)"

# 尝试编译检查
echo " 正在进行编译检查..."
if go build -o /tmp/new-api-test main.go 2>&1 | grep -q "error"; then
echo -e "${RED}✗${NC} 编译检查失败,请查看错误信息"
go build -o /tmp/new-api-test main.go 2>&1 | head -20
else
echo -e "${GREEN}✓${NC} 编译检查通过"
rm -f /tmp/new-api-test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd go.mod

Repository: QuantumNous/new-api

Length of output: 69


🏁 Script executed:

cat verify-changes.sh | sed -n '139,156p'

Repository: QuantumNous/new-api

Length of output: 782


🏁 Script executed:

cat go.mod

Repository: QuantumNous/new-api

Length of output: 5657


🏁 Script executed:

head -20 verify-changes.sh

Repository: QuantumNous/new-api

Length of output: 364


Align Go version requirement with go.mod and fix build check to use exit codes.

The hardcoded 1.24.0 requirement should be updated to match go.mod which specifies go 1.25.1. Additionally, the current go build | grep -q "error" check is unreliable; use exit codes instead by adding set -o pipefail and removing the grep filter.

🛠️ Suggested fix (exit-code based build check)
 set -e
+set -o pipefail
@@
-    REQUIRED_VERSION="1.24.0"
+    REQUIRED_VERSION="1.25.1"
     if [ "$(printf '%s\n' "$REQUIRED_VERSION" "$GO_VERSION" | sort -V | head -n1)" = "$REQUIRED_VERSION" ]; then
         echo -e "${GREEN}✓${NC} Go 版本满足要求 (>= 1.24.0)"
@@
-    if go build -o /tmp/new-api-test main.go 2>&1 | grep -q "error"; then
-        echo -e "${RED}✗${NC} 编译检查失败,请查看错误信息"
-        go build -o /tmp/new-api-test main.go 2>&1 | head -20
-    else
+    if go build -o /tmp/new-api-test main.go; then
         echo -e "${GREEN}✓${NC} 编译检查通过"
         rm -f /tmp/new-api-test
+    else
+        echo -e "${RED}✗${NC} 编译检查失败,请查看错误信息"
     fi
🤖 Prompt for AI Agents
In `@verify-changes.sh` around lines 139 - 156, Update the Go version check to
match go.mod by changing REQUIRED_VERSION from "1.24.0" to "1.25.1", enable
reliable exit-code checks by adding "set -o pipefail" before running the build,
and replace the unreliable "go build ... | grep -q 'error'" logic with an
exit-status based test around the go build invocation (use the same go build
command that creates /tmp/new-api-test); adjust the if/else accordingly to treat
a non-zero exit as failure and remove the grep filtering.

Comment thread verify-changes.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱

@seefs001 seefs001 closed this Jan 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants