Skip to content

feat: add per-channel retry attempts#6240

Open
thirking wants to merge 2 commits into
QuantumNous:mainfrom
thirking:codex/channel-retry-attempts
Open

feat: add per-channel retry attempts#6240
thirking wants to merge 2 commits into
QuantumNous:mainfrom
thirking:codex/channel-retry-attempts

Conversation

@thirking

@thirking thirking commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 本 PR 由 AI 辅助完成,并由提交者人工审阅、验证和整理。

📝 变更描述 / Description

当前的 RetryTimes 只控制请求级重试,重试过程中可能过早切换渠道,导致不稳定但低成本的渠道没有获得足够的重试机会,也让“成本优先还是稳定优先”的策略难以表达。

本 PR 增加渠道级 retry_attempts 配置:

  • 每个渠道可以配置自己的总尝试次数,包含首次请求。
  • 当前渠道未达到次数上限时,请求固定重试当前渠道。
  • 渠道耗尽后,从当前请求的候选集中排除该渠道,再按最高优先级和权重选择其他渠道。
  • 保留 RetryTimes 作为单请求的全局安全上限。

例如:

低价渠道 A(6 次) → A → A → A → A → A
耗尽后选择下一优先级渠道 B(3 次) → B → B

这样可以在保持成本优势的同时,避免路由器在主渠道只失败一次后就随机切换。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 已搜索现有 Issues 与 PRs,未发现相同设计的重复提交。
  • Bug fix 说明: 本 PR 未标记为 Bug fix。
  • 变更理解: 已理解状态管理、优先级选择、缓存路径和全局重试预算的影响。
  • 范围聚焦: 未包含 Docker、Compose 或无关依赖改动。
  • 本地验证: 已通过 go test ./...、default 类型检查和生产构建、classic 生产构建,以及改动文件定向 lint。
  • 安全合规: 未加入敏感凭据。

📸 运行证明 / Proof of Work

  • PostgreSQL 迁移后确认 channels.retry_attempts 字段存在。
  • 部署容器健康检查通过,/api/status 返回 200。
  • 手动验证失败序列按渠道尝试次数执行,并在渠道耗尽后进入下一候选渠道。

Summary by CodeRabbit

  • New Features

    • Added configurable per-channel attempt limits from 1–10, including form controls in channel settings.
    • Channels now retry failed requests before routing to other eligible channels by priority.
    • Exhausted channels are temporarily excluded from subsequent selection for the request.
    • Added localized labels and guidance for the new setting across supported languages.
  • Bug Fixes

    • Improved channel failure tracking and retry selection, including locked-channel handling.
    • Added validation to prevent invalid attempt limits.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds per-channel request attempt limits, request-scoped retry tracking, exclusion-aware channel selection, relay integration, validation, and configuration UI support across classic and default channel management interfaces.

Changes

Channel retry routing

Layer / File(s) Summary
Retry contracts and exclusion-aware selection
model/ability.go, model/channel.go, model/channel_cache.go, model/ability_retry_test.go
Adds retry configuration to channels and supports excluding exhausted channel IDs during priority-based selection, with database selection coverage.
Request-scoped retry state
constant/context_key.go, service/channel_retry.go, service/channel_retry_test.go
Tracks the current channel, attempts, maximum attempts, locked state, and excluded channels in request context.
Selection and relay integration
service/channel_select.go, controller/relay.go
Propagates exclusions through channel selection, records failures, restores locked retry channels, and initializes retry state for selected channels.
Channel configuration and validation
controller/channel.go, controller/channel_authz.go, web/classic/..., web/default/src/features/channels/..., web/default/src/i18n/locales/*
Validates and persists attempt limits, exposes them in channel forms, classifies the field as advanced and non-sensitive, and adds translations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Relay
  participant ChannelRetry
  participant ChannelSelector
  participant ChannelStore
  Relay->>ChannelRetry: record failed channel
  ChannelRetry-->>Relay: retry state and excluded IDs
  Relay->>ChannelSelector: request next eligible channel
  ChannelSelector->>ChannelStore: query excluding exhausted channels
  ChannelStore-->>ChannelSelector: return remaining channel
  ChannelSelector-->>Relay: provide selected channel
Loading

Possibly related PRs

Suggested reviewers: fux-dev

Poem

I’m a bunny with channels to hop,
Counting each try till the failures stop.
When one path is worn, I mark it aside,
Then hop by priority, ears open wide.
Forms bloom with numbers from one up to ten—
Retry trails guide the request again.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: configurable retry attempts per channel.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 1

Caution

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

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

142-147: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix cross-group retry condition when channels are excluded.

By forcing priorityRetry = 0 when channels are excluded (to guarantee robust priority selection), the crossGroupRetry group switch condition breaks. Because priorityRetry sits at 0, the check >= common.RetryTimes will always evaluate to false, trapping the retry loop in the current group instead of failing over to the next group.

Replace priorityRetry with param.GetRetry() in the check and log message to evaluate the actual number of attempts made within the current group correctly.

🐛 Proposed fix
-			if crossGroupRetry && priorityRetry >= common.RetryTimes {
+			if crossGroupRetry && param.GetRetry() >= common.RetryTimes {
 				// Current group has exhausted all retries, prepare to switch to next group
 				// This request still uses current group, but next retry will use next group
 				// 当前分组已用完所有重试次数,准备切换到下一个分组
 				// 本次请求仍使用当前分组,但下次重试将使用下一个分组
-				logger.LogDebug(param.Ctx, "Current group %s retries exhausted (priorityRetry=%d >= RetryTimes=%d), preparing switch to next group for next retry", autoGroup, priorityRetry, common.RetryTimes)
+				logger.LogDebug(param.Ctx, "Current group %s retries exhausted (attempts=%d >= RetryTimes=%d), preparing switch to next group for next retry", autoGroup, param.GetRetry(), common.RetryTimes)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/channel_select.go` around lines 142 - 147, In the cross-group retry
branch guarded by crossGroupRetry, replace priorityRetry with param.GetRetry()
in both the RetryTimes comparison and the associated logger.LogDebug arguments,
so group switching uses the actual current-group attempt count when channels are
excluded.
model/ability.go (1)

115-122: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Lost exclusion filter when retry != 0.

When retry != 0, channelQuery is reassigned but the channel_id NOT IN ? filter is omitted. Additionally, getPriority does not factor in excluded channels when determining the priority level.

If a caller provides both retry > 0 and excluded (e.g., if upstream selection stops forcing retry = 0), the query will select channels ignoring the exclusion list. The in-memory filter in GetChannelWithExclusions would then strip them, potentially returning nil even if valid channels exist at other priorities.

To fix this, update getPriority to accept and apply the excluded map, and re-apply the channel_id NOT IN ? clause when rebuilding channelQuery for retry != 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/ability.go` around lines 115 - 122, The retry path in the
channel-selection logic drops exclusions. Update getPriority to accept and apply
the excluded-channel map, pass excluded from its caller, and preserve the
channel_id NOT IN ? condition when rebuilding channelQuery for retry != 0 in the
surrounding ability query flow.
🧹 Nitpick comments (1)
model/ability.go (1)

147-155: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant in-memory exclusion.

Since getChannelQueryWithExclusions already filters out the excluded channels in the SQL query (when retry == 0), this in-memory filtering loop is largely redundant. Once the SQL query accurately handles exclusions for all retry values, this entire block can be safely removed to simplify the logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/ability.go` around lines 147 - 155, Update
getChannelQueryWithExclusions to apply excluded-channel filtering for every
retry value, then remove the redundant in-memory exclusion block that rebuilds
abilities. Preserve the existing abilities result flow after the query handles
exclusions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/default/src/i18n/locales/zh-TW.json`:
- Line 2008: Restore the protected key in the locale entry by changing
`footer.newapi.projectAttributionSuffix` back to
`footer.new\u0061pi.projectAttributionSuffix`; leave its translated value
unchanged.

---

Outside diff comments:
In `@model/ability.go`:
- Around line 115-122: The retry path in the channel-selection logic drops
exclusions. Update getPriority to accept and apply the excluded-channel map,
pass excluded from its caller, and preserve the channel_id NOT IN ? condition
when rebuilding channelQuery for retry != 0 in the surrounding ability query
flow.

In `@service/channel_select.go`:
- Around line 142-147: In the cross-group retry branch guarded by
crossGroupRetry, replace priorityRetry with param.GetRetry() in both the
RetryTimes comparison and the associated logger.LogDebug arguments, so group
switching uses the actual current-group attempt count when channels are
excluded.

---

Nitpick comments:
In `@model/ability.go`:
- Around line 147-155: Update getChannelQueryWithExclusions to apply
excluded-channel filtering for every retry value, then remove the redundant
in-memory exclusion block that rebuilds abilities. Preserve the existing
abilities result flow after the query handles exclusions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2cffaca0-8a33-4b89-8665-884ebda992f3

📥 Commits

Reviewing files that changed from the base of the PR and between a63364d and b22a9c5.

📒 Files selected for processing (24)
  • constant/context_key.go
  • controller/channel.go
  • controller/channel_authz.go
  • controller/relay.go
  • model/ability.go
  • model/ability_retry_test.go
  • model/channel.go
  • model/channel_cache.go
  • service/channel_retry.go
  • service/channel_retry_test.go
  • service/channel_select.go
  • web/classic/src/components/table/channels/modals/EditChannelModal.jsx
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/constants.ts
  • web/default/src/features/channels/lib/channel-form-errors.ts
  • web/default/src/features/channels/lib/channel-form.ts
  • web/default/src/features/channels/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh-TW.json
  • web/default/src/i18n/locales/zh.json

Comment thread web/default/src/i18n/locales/zh-TW.json Outdated

@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.

🧹 Nitpick comments (1)
model/ability.go (1)

63-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pass excluded IDs into getPriority
getPriority is only called from getChannelQueryWithExclusions, which already builds excludedIDs. Accepting []int here avoids converting the same set twice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/ability.go` around lines 63 - 72, Update getPriority to accept the
already-built excluded channel IDs as a []int parameter, and use that slice
directly for the channel_id NOT IN filter instead of converting excluded
internally. Update its sole caller, getChannelQueryWithExclusions, to pass
excludedIDs and remove the redundant conversion path while preserving the
existing empty-slice behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@model/ability.go`:
- Around line 63-72: Update getPriority to accept the already-built excluded
channel IDs as a []int parameter, and use that slice directly for the channel_id
NOT IN filter instead of converting excluded internally. Update its sole caller,
getChannelQueryWithExclusions, to pass excludedIDs and remove the redundant
conversion path while preserving the existing empty-slice behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 474936de-a29c-4bf1-afc3-67a273afde2b

📥 Commits

Reviewing files that changed from the base of the PR and between b22a9c5 and 004a70d.

📒 Files selected for processing (5)
  • model/ability.go
  • model/ability_retry_test.go
  • service/channel_retry.go
  • service/channel_select.go
  • web/default/src/i18n/locales/zh-TW.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • service/channel_select.go
  • model/ability_retry_test.go
  • service/channel_retry.go

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.

1 participant