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 channel retry attempts, allowing 1–10 retries per channel.
    • Channels that repeatedly fail are temporarily excluded, allowing selection of the next available channel.
    • Retry state is preserved during request processing and resets when switching channels.
    • Channel configuration updates to retry attempts are treated as non-sensitive changes.
  • Bug Fixes

    • Improved fallback channel selection when prioritized channels are unavailable.
  • Tests

    • Added coverage for retry limits, default behavior, channel exclusion, and priority-based fallback selection.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 30dc678c-9220-47ec-a05a-f0765043ab08

📥 Commits

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

📒 Files selected for processing (8)
  • 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
🚧 Files skipped from review as they are similar to previous changes (8)
  • constant/context_key.go
  • model/channel.go
  • controller/channel_authz.go
  • controller/channel.go
  • model/channel_cache.go
  • model/ability.go
  • model/ability_retry_test.go
  • controller/relay.go

Walkthrough

The change adds configurable channel retry attempts, request-scoped retry state, exclusion-aware channel selection, and relay integration. It validates retry settings and adds tests for retry exhaustion and excluded-channel priority selection.

Changes

Channel retry routing

Layer / File(s) Summary
Retry configuration and validation
model/channel.go, controller/channel.go, controller/channel_authz.go
Adds nullable channel retry limits with a default of one attempt. Validation restricts configured values to 1–10. The field is classified as non-sensitive.
Exclusion-aware channel selection
model/ability.go, model/channel_cache.go, service/channel_select.go, model/ability_retry_test.go
Adds exclusion-aware database, cache, and service selection paths. Selection omits exhausted channels and tests retain the highest-priority remaining channel.
Request-scoped retry state
constant/context_key.go, service/channel_retry.go, service/channel_retry_test.go
Stores the current channel, attempt counts, maximum attempts, and excluded IDs in Gin context. Helpers lock channels, record failures, clear state, and exclude exhausted channels.
Relay retry integration
controller/relay.go
Records channel failures during relay processing. Restores locked channels from cache, initializes retry state for selected channels, and rejects retries for specific-channel requests.

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

Sequence Diagram(s)

sequenceDiagram
  participant Relay
  participant ChannelRetry
  participant ChannelSelector
  participant ChannelCache
  Relay->>ChannelRetry: record channel failure
  ChannelRetry-->>Relay: return locked channel and exclusions
  Relay->>ChannelSelector: request eligible channel
  ChannelSelector->>ChannelCache: select channel excluding exhausted IDs
  ChannelCache-->>ChannelSelector: return channel or no candidate
  ChannelSelector-->>Relay: return selected channel
Loading

Possibly related PRs

Poem

A rabbit hops through retry lanes,
Counting attempts and dodging pains.
A tired channel leaves the queue,
The next best path comes hopping through.
Context keeps the trail just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.35% 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 clearly and concisely describes the main change: adding 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 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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

@thirking
thirking force-pushed the codex/channel-retry-attempts branch from 004a70d to 736e451 Compare August 4, 2026 03:43
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