Skip to content

feat: 添加供应商查询缓存, 改善性能#554

Merged
ding113 merged 3 commits intoding113:devfrom
hank9999:feat/provider-cache
Jan 7, 2026
Merged

feat: 添加供应商查询缓存, 改善性能#554
ding113 merged 3 commits intoding113:devfrom
hank9999:feat/provider-cache

Conversation

@hank9999
Copy link
Collaborator

@hank9999 hank9999 commented Jan 7, 2026

Summary

添加供应商(Provider)进程级内存缓存机制,通过减少高频数据库查询来改善代理请求性能。

Problem

在高并发场景下,每个代理请求都会调用 findAllProviders() 查询数据库获取供应商列表,导致:

  • 数据库连接池压力增大
  • 请求延迟增加
  • 资源利用率低(供应商数据变更频率远低于请求频率)

Solution

实现带自动刷新的进程级缓存,结合 Redis Pub/Sub 实现跨实例即时失效:

缓存策略

  • 30s TTL 自动过期:保证数据最终一致性
  • Redis Pub/Sub 失效通知:CRUD 操作后跨实例即时清除缓存
  • 降级策略:Redis 不可用时依赖 TTL 自动过期
  • 版本号机制:防止并发刷新竞态条件
  • 请求级快照:保证故障迁移期间数据一致性

设计决策

  • 使用失效通知模式而非数据同步:
    • 类型安全:Date 等类型从 DB 正确构造
    • 数据安全:不通过 Redis 传输敏感数据(如 provider.key)
  • Session 级快照冻结:同一请求多次调用返回相同数据

Changes

Core Changes

  • src/lib/cache/provider-cache.ts (+170 行):

    • getCachedProviders() - 带自动刷新的缓存获取
    • publishProviderCacheInvalidation() - 跨实例失效通知
    • invalidateCache() - 本地缓存失效
    • warmupProviderCache() - 启动预热
    • getProviderCacheStats() - 监控/调试统计
  • src/repository/provider.ts (+21/-4):

    • 重命名 findAllProvidersfindAllProvidersFresh(直接 DB 查询)
    • 新增 findAllProviders() 包装缓存层
  • src/app/v1/_lib/proxy/session.ts (+26):

    • 新增 getProvidersSnapshot() - 请求级 Provider 快照
    • 保证故障迁移期间数据一致性

Supporting Changes

  • src/actions/providers.ts (+33):

    • addProvidereditProviderremoveProvider 后广播缓存失效
    • 失败时仅记录警告,不影响主流程
  • src/app/v1/_lib/proxy/provider-selector.ts (+5/-1):

    • resolveProvider 优先使用 Session 快照

Testing

Automated Tests

  • bun run lint 通过
  • bun run typecheck 通过
  • 现有测试通过

Manual Testing

  • 验证缓存命中(30s 内多次请求应复用缓存)
  • 验证 CRUD 后缓存即时失效
  • 验证 Redis 不可用时 TTL 降级
  • 验证故障迁移期间数据一致性

Checklist

  • Base branch set to dev
  • Code follows project conventions
  • Self-review completed
  • Tests pass locally

Description enhanced by Claude AI

@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

引入进程级提供者缓存与 Redis 跨实例失效广播;在会话中新增请求级提供者快照;将多处读取切换为 fresh 路径或缓存接口;在增删改及部分读取后触发跨实例缓存失效(均为内部实现,API 签名未变)。

Changes

Cohort / File(s) 变更摘要
缓存基础设施
src/lib/cache/provider-cache.ts
新增提供者缓存模块:30s TTL、版本号防竞态、单次刷新合并、延迟订阅 Redis Pub/Sub 监听跨实例失效通道 CHANNEL_PROVIDERS_UPDATED;导出 invalidateCachepublishProviderCacheInvalidationgetCachedProviders(fetcher)warmupProviderCachegetProviderCacheStats
存储层集成
src/repository/provider.ts
新增 findAllProvidersFresh()(直接从 DB 读取);将 findAllProviders() 改为 getCachedProviders(findAllProvidersFresh) 的缓存包装器并调整日志标签。
会话级快照
src/app/v1/_lib/proxy/session.ts
ProxySession 中新增私有属性 providersSnapshot 及方法 getProvidersSnapshot(),懒加载并在请求生命周期内缓存提供者列表;新增对 findAllProviders 的导入。
选择器适配
src/app/v1/_lib/proxy/provider-selector.ts
枚举提供者时改为会话感知:若存在 session 则使用 session.getProvidersSnapshot(),否则回退到 findAllProviders()requestedModel 仍来自 session.getCurrentModel()
操作层广播与读路径调整
src/actions/providers.ts
新增内部函数 broadcastProviderCacheInvalidation({operation, providerId})(调用 publishProviderCacheInvalidation 并记录结果);在 addProvider / editProvider / removeProvider 成功后调用;在若干读取/统计流程将 findAllProviders 替换为 findAllProvidersFresh 或使用缓存接口以保证一致性并在必要时触发跨实例失效。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding provider query caching to improve performance, which matches the core objective of introducing process-level caching for providers.
Description check ✅ Passed The description comprehensively relates to the changeset, explaining the problem being solved (high-frequency DB queries), the solution (process-level cache with Redis Pub/Sub invalidation), design decisions, and affected files.
Docstring Coverage ✅ Passed Docstring coverage is 88.24% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @hank9999, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求引入了一个健壮的供应商数据缓存层,通过减少数据库负载显著提升了系统性能。它包含一个具有 30 秒 TTL 的内存缓存、通过 Redis Pub/Sub 实现的 CRUD 操作实时跨实例缓存失效机制,以及一个请求级快照机制,以确保在单个请求生命周期内的数据一致性。此增强旨在优化高频访问的供应商数据检索,同时保持数据新鲜度并有效处理并发情况。

Highlights

  • 新增缓存机制: 为供应商信息引入内存缓存机制,以减少频繁的数据库查询操作,显著提升性能。
  • 缓存失效广播: 实现基于 Redis Pub/Sub 的跨实例缓存失效通知机制,确保在供应商信息(增、删、改)发生变化时,所有实例的缓存都能实时清理。
  • 请求级快照: 在 ProxySession 中引入供应商列表的请求级快照功能,以保证在故障转移期间数据的一致性,避免同一请求内多次调用返回不同结果。
  • 缓存降级策略: 当 Redis 不可用时,缓存将依赖于其设定的 TTL(生存时间)自动过期,确保系统仍能正常运行。
  • 并发刷新处理: 利用版本号和 refreshPromise 机制,有效防止并发请求同时刷新缓存时可能出现的竞态条件。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added enhancement New feature or request area:provider labels Jan 7, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

本次 PR 设计并实现了一套完善的供应商查询缓存机制,值得称赞。通过引入进程级缓存(结合 Redis Pub/Sub 失效)和请求级快照,有效地降低了数据库负载,并保证了故障转移期间的数据一致性,显著提升了系统的性能和健壮性。

代码结构清晰,缓存逻辑的实现考虑了并发刷新、竞争条件等边界情况,非常可靠。

审查中发现一个潜在问题:在 src/actions/providers.ts 中,部分用于管理后台的函数错误地使用了缓存数据,可能导致管理员在增删改操作后看到长达 30 秒的旧数据。我已在具体的 review comment 中提出了修改建议。

总体而言,这是一次高质量的功能增强。

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (1)

  1. src/lib/cache/provider-cache.ts, line 69 (link)

    logic: setting refreshPromise to null here creates a small window where concurrent calls could both pass the check on line 106 and create duplicate refresh tasks

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/lib/cache/provider-cache.ts:
- Around line 46-60: ensureSubscription currently sets subscriptionInitialized =
true before awaiting subscribeCacheInvalidation, causing a race where concurrent
calls may skip subscribing; change the logic so the initialized flag is set only
after subscribeCacheInvalidation completes successfully (or use a single shared
Promise like subscriptionInitPromise returned by ensureSubscription to serialize
concurrent callers), i.e., ensure
subscribeCacheInvalidation(CHANNEL_PROVIDERS_UPDATED, ...) completes and
invalidateCache/logger.debug are registered before marking
subscriptionInitialized (or resolve subscriptionInitPromise) so multiple
concurrent callers wait instead of duplicating subscriptions.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 272c3e9 and c71acda.

📒 Files selected for processing (5)
  • src/actions/providers.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/cache/provider-cache.ts
  • src/repository/provider.ts
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use 2-space indentation in all code files

Files:

  • src/actions/providers.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/cache/provider-cache.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/repository/provider.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use double quotes for strings instead of single quotes
Use trailing commas in multi-line structures
Enforce maximum line length of 100 characters
Use path alias @/* to reference files from ./src/* directory

**/*.{ts,tsx,js,jsx}: Use Biome for linting and formatting with 2-space indent, double quotes, trailing commas, and 100 character max line length
Use path alias @/* to reference files in ./src/* directory

Files:

  • src/actions/providers.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/cache/provider-cache.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/repository/provider.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript strict mode for type safety
Use readonly or const assertions for immutable data structures

Files:

  • src/actions/providers.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/cache/provider-cache.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/repository/provider.ts
src/actions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/actions/**/*.ts: Validate all user inputs with Zod schemas before processing
Use Server Actions in next-safe-action with OpenAPI generation for admin API endpoints
Use Next.js API Routes and Server Actions for admin operations and REST endpoints

Files:

  • src/actions/providers.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Hash API keys using SHA-256 before storing in database, never store plaintext keys
Mask API keys and sensitive data in application logs
Validate required environment variables at startup with clear error messages

Files:

  • src/actions/providers.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/cache/provider-cache.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/repository/provider.ts
src/**/*provider*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Set provider circuit breaker failure threshold, open duration, and half-open success threshold in configuration

Files:

  • src/actions/providers.ts
  • src/lib/cache/provider-cache.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/repository/provider.ts
src/{repository,actions}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Avoid N+1 queries by using eager loading and batch queries for statistics

Files:

  • src/actions/providers.ts
  • src/repository/provider.ts
src/app/v1/_lib/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Guard pipeline must execute in order: ProxyAuthenticator, SensitiveWordGuard, VersionGuard, ProxySessionGuard, ProxyRateLimitGuard, ProxyProviderResolver

Files:

  • src/app/v1/_lib/proxy/session.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
src/app/v1/_lib/proxy/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/app/v1/_lib/proxy/**/*.ts: Implement guard pipeline pattern for cross-cutting concerns in request processing (auth, rate limiting, session)
Use undici library for HTTP requests instead of node-fetch for better performance

Files:

  • src/app/v1/_lib/proxy/session.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
src/app/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Implement Content-Security-Policy headers for XSS prevention

Files:

  • src/app/v1/_lib/proxy/session.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
src/app/v1/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use Hono router for ultrafast, lightweight routing in proxy endpoints

Files:

  • src/app/v1/_lib/proxy/session.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
src/lib/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use connection pooling for database and Redis connections

Files:

  • src/lib/cache/provider-cache.ts
src/repository/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use Repository pattern in src/repository/ to wrap Drizzle queries

Files:

  • src/repository/provider.ts
🧠 Learnings (10)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.

Applied to files:

  • src/actions/providers.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/cache/provider-cache.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/repository/provider.ts
📚 Learning: 2026-01-03T09:08:49.019Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-03T09:08:49.019Z
Learning: Applies to src/app/v1/_lib/proxy/**/*.ts : Implement guard pipeline pattern for cross-cutting concerns in request processing (auth, rate limiting, session)

Applied to files:

  • src/app/v1/_lib/proxy/session.ts
📚 Learning: 2026-01-03T09:08:20.573Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-03T09:08:20.573Z
Learning: Applies to src/app/v1/_lib/proxy-handler.ts : Structure request flow in proxy handler through: ProxySession.fromContext() -> detectFormat() -> GuardPipelineBuilder.run() -> ProxyForwarder.send() -> ProxyResponseHandler.dispatch()

Applied to files:

  • src/app/v1/_lib/proxy/session.ts
📚 Learning: 2026-01-03T09:08:20.573Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-03T09:08:20.573Z
Learning: Applies to src/app/v1/_lib/**/*.ts : Guard pipeline must execute in order: ProxyAuthenticator, SensitiveWordGuard, VersionGuard, ProxySessionGuard, ProxyRateLimitGuard, ProxyProviderResolver

Applied to files:

  • src/app/v1/_lib/proxy/session.ts
📚 Learning: 2026-01-03T09:08:49.019Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-03T09:08:49.019Z
Learning: Applies to src/app/v1/_lib/proxy/**/*.ts : Use undici library for HTTP requests instead of node-fetch for better performance

Applied to files:

  • src/app/v1/_lib/proxy/session.ts
📚 Learning: 2026-01-03T09:08:49.019Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-03T09:08:49.019Z
Learning: Applies to src/**/*provider*.ts : Set provider circuit breaker failure threshold, open duration, and half-open success threshold in configuration

Applied to files:

  • src/app/v1/_lib/proxy/provider-selector.ts
📚 Learning: 2026-01-03T09:08:20.573Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-03T09:08:20.573Z
Learning: Applies to src/drizzle/**/*.ts : Use Drizzle ORM with PostgreSQL for database operations

Applied to files:

  • src/repository/provider.ts
📚 Learning: 2026-01-03T09:08:49.019Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-03T09:08:49.019Z
Learning: Applies to src/drizzle/**/*.ts : Use Drizzle ORM with parameterized queries to prevent SQL injection

Applied to files:

  • src/repository/provider.ts
📚 Learning: 2026-01-03T09:08:49.019Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-03T09:08:49.019Z
Learning: Applies to src/drizzle/**/*.ts : Use JSON columns in PostgreSQL for flexible data structures (modelRedirects, providerChain, etc.)

Applied to files:

  • src/repository/provider.ts
📚 Learning: 2026-01-03T09:08:20.573Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-03T09:08:20.573Z
Learning: Applies to src/repository/**/*.ts : Use Repository pattern in `src/repository/` to wrap Drizzle queries

Applied to files:

  • src/repository/provider.ts
🧬 Code graph analysis (4)
src/actions/providers.ts (2)
src/lib/cache/provider-cache.ts (1)
  • publishProviderCacheInvalidation (80-84)
src/lib/logger.ts (1)
  • logger (168-187)
src/app/v1/_lib/proxy/session.ts (2)
src/types/provider.ts (1)
  • Provider (39-142)
src/repository/provider.ts (1)
  • findAllProviders (276-278)
src/lib/cache/provider-cache.ts (4)
src/types/provider.ts (1)
  • Provider (39-142)
src/app/v1/_lib/headers.ts (1)
  • process (95-117)
src/lib/redis/pubsub.ts (2)
  • subscribeCacheInvalidation (60-97)
  • publishCacheInvalidation (44-53)
src/lib/logger.ts (1)
  • logger (168-187)
src/app/v1/_lib/proxy/provider-selector.ts (1)
src/repository/provider.ts (1)
  • findAllProviders (276-278)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: pr-review
  • GitHub Check: Greptile Review
  • GitHub Check: pr-label
  • GitHub Check: pr-description
🔇 Additional comments (11)
src/lib/cache/provider-cache.ts (2)

92-136: 缓存刷新逻辑设计良好

getCachedProviders 实现了以下关键特性:

  • TTL 缓存命中检查
  • 单次飞行刷新(refreshPromise 合并并发请求)
  • 版本号防止刷新过程中被失效事件覆盖旧数据

实现正确,符合并发安全要求。


155-169: LGTM!

getProviderCacheStats 提供了完整的缓存状态监控信息,对于调试和运维非常有用。

src/actions/providers.ts (4)

106-127: 缓存失效广播实现正确

broadcastProviderCacheInvalidation 设计合理:

  • 捕获异常避免影响主流程
  • 失败时记录警告,其他实例依赖 TTL 过期降级
  • 日志包含操作类型和 providerId 便于追踪

522-524: LGTM!

addProvider 成功后正确调用了缓存失效广播,时机在 revalidatePath 之前,确保跨实例一致性。


663-665: LGTM!

editProvider 成功后正确触发缓存失效。


700-702: LGTM!

removeProvider 成功后正确触发缓存失效。

src/app/v1/_lib/proxy/provider-selector.ts (1)

648-652: 会话级快照保证故障迁移一致性

正确实现了会话感知的供应商获取:

  • 有 session 时使用 getProvidersSnapshot() 保证请求生命周期内数据一致
  • 无 session 时回退到 findAllProviders()(内部已使用缓存)

这确保了同一请求在多次供应商选择(如重试)时使用相同的数据源。

src/repository/provider.ts (2)

194-265: LGTM!

findAllProvidersFresh 保持了原有的数据库查询逻辑,日志标签正确更新为 findAllProvidersFresh:query_result,便于区分缓存命中和数据库查询。


267-278: 缓存封装清晰简洁

findAllProviders 通过依赖注入模式调用 getCachedProviders(findAllProvidersFresh)

  • 保持了原有 API 签名不变
  • 缓存逻辑完全委托给 provider-cache 模块
  • 注释清晰说明了缓存特性(TTL + Pub/Sub)
src/app/v1/_lib/proxy/session.ts (2)

112-118: 请求级快照设计合理

providersSnapshot 属性设计:

  • 初始值为 null,惰性加载
  • JSDoc 注释清晰说明了用途(故障迁移期间数据一致性)
  • 生命周期与 ProxySession 实例绑定(请求级别)

325-340: LGTM!

getProvidersSnapshot 实现正确:

  • 首次调用从 findAllProviders()(已缓存)获取数据并保存
  • 后续调用直接返回缓存的快照
  • 保证了同一请求生命周期内供应商列表的一致性

这与 provider-selector.ts 中的调用配合,实现了故障迁移期间的数据一致性保证。

@github-actions github-actions bot added the size/S Small PR (< 200 lines) label Jan 7, 2026
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR implements an in-memory provider caching mechanism with a 30-second TTL and Redis Pub/Sub for cross-instance cache invalidation. The implementation is well-structured with proper graceful degradation when Redis is unavailable.

PR Size: S

  • Lines changed: 260 (255 additions, 5 deletions)
  • Files changed: 5

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 1 0
Simplification 0 0 0 0

Medium Priority Issues (Consider Fixing)

  1. [TEST-MISSING-CRITICAL] No unit tests for provider-cache.ts
    • The new cache module has no dedicated tests
    • Key scenarios needing coverage: cache hit/miss, TTL expiration, concurrent refresh prevention, version-based race condition handling
    • The existing provider tests are skipped and don't cover caching behavior
    • Note: This is flagged as Medium rather than High because the graceful degradation design (TTL fallback when Redis unavailable) provides a safety net

Design Notes (Not Issues)

The Greptile summary mentioned a race condition in invalidateCache() when clearing refreshPromise. After review, this is a non-issue in practice:

  • The version check at line 123 (if (cache.version === currentVersion)) already prevents stale data from being written to cache
  • The worst case scenario is a duplicate DB query, not data corruption or inconsistency
  • This is an acceptable trade-off for code simplicity

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean (no secrets in Redis messages, only invalidation signals)
  • Error handling - Clean (graceful degradation with TTL fallback)
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Missing for new cache module (existing tests skipped)
  • Code clarity - Good

Automated review by Claude AI

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
src/actions/providers.ts (1)

3172-3172: 建议使用 findAllProvidersFresh() 以保持数据一致性

此函数是文件中唯一使用缓存版本 findAllProviders() 的地方,而其他管理函数(getProvidersgetProviderGroupsWithCountgetProvidersHealthStatus 等)均使用 findAllProvidersFresh()。这会导致供应商修改后,模型建议列表在 30 秒内可能显示过时数据。

建议改为 findAllProvidersFresh() 以与其他管理函数保持一致,除非缓存有特定的性能考虑。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between c71acda and 231d12a.

📒 Files selected for processing (1)
  • src/actions/providers.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use 2-space indentation in all code files

Files:

  • src/actions/providers.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use double quotes for strings instead of single quotes
Use trailing commas in multi-line structures
Enforce maximum line length of 100 characters
Use path alias @/* to reference files from ./src/* directory

**/*.{ts,tsx,js,jsx}: Use Biome for linting and formatting with 2-space indent, double quotes, trailing commas, and 100 character max line length
Use path alias @/* to reference files in ./src/* directory

Files:

  • src/actions/providers.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript strict mode for type safety
Use readonly or const assertions for immutable data structures

Files:

  • src/actions/providers.ts
src/actions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/actions/**/*.ts: Validate all user inputs with Zod schemas before processing
Use Server Actions in next-safe-action with OpenAPI generation for admin API endpoints
Use Next.js API Routes and Server Actions for admin operations and REST endpoints

Files:

  • src/actions/providers.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Hash API keys using SHA-256 before storing in database, never store plaintext keys
Mask API keys and sensitive data in application logs
Validate required environment variables at startup with clear error messages

Files:

  • src/actions/providers.ts
src/**/*provider*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Set provider circuit breaker failure threshold, open duration, and half-open success threshold in configuration

Files:

  • src/actions/providers.ts
src/{repository,actions}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Avoid N+1 queries by using eager loading and batch queries for statistics

Files:

  • src/actions/providers.ts
🧠 Learnings (1)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.

Applied to files:

  • src/actions/providers.ts
🧬 Code graph analysis (1)
src/actions/providers.ts (3)
src/lib/cache/provider-cache.ts (1)
  • publishProviderCacheInvalidation (80-84)
src/repository/provider.ts (1)
  • findAllProvidersFresh (201-265)
src/drizzle/schema.ts (1)
  • providers (129-271)
🔇 Additional comments (4)
src/actions/providers.ts (4)

7-7: LGTM! 导入声明正确。

引入的缓存失效发布函数和新鲜数据获取函数符合 PR 目标,命名清晰且遵循项目约定。

Also applies to: 40-40


107-128: LGTM! 辅助函数设计合理。

缓存失效广播函数的错误处理策略正确:失败时记录警告但不中断主流程,其他实例将通过 TTL 过期自动刷新缓存。日志记录包含足够的上下文信息,便于故障排查。

基于学习记录:此设计符合弹性系统最佳实践。


149-149: LGTM! 后台管理函数正确使用新鲜数据。

三处管理函数(getProvidersgetProviderGroupsWithCountgetProvidersHealthStatus)现在调用 findAllProvidersFresh() 直接从数据库获取最新数据,避免了缓存导致的操作反馈延迟。此更改解决了先前评审中标记的问题,与提交信息和 PR 目标一致。

基于历史评审反馈:此修改符合管理后台对数据新鲜度的要求。

Also applies to: 337-337, 724-724


523-524: LGTM! 缓存失效广播集成正确。

三个 CRUD 操作(添加、编辑、删除供应商)均在数据库修改完成后正确触发跨实例缓存失效广播,且在 Next.js 路径重新验证前完成。模式一致,操作类型清晰,确保多实例环境下的缓存一致性。

Also applies to: 664-665, 701-702

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/lib/cache/provider-cache.ts:
- Around line 47-62: The function ensureSubscription sets
subscriptionInitialized before awaiting subscribeCacheInvalidation, causing a
race where concurrent callers may see the flag true before subscription
completes; fix by moving assignment of subscriptionInitialized to after the
await (i.e., only set subscriptionInitialized = true when
subscribeCacheInvalidation(CHANNEL_PROVIDERS_UPDATED, ...) resolves
successfully), or implement a shared Promise (e.g., store a module-level
subscriptionPromise returned by subscribeCacheInvalidation and await it on
subsequent calls) so ensureSubscription, subscribeCacheInvalidation,
CHANNEL_PROVIDERS_UPDATED, invalidateCache, and logger.debug are coordinated and
the flag reflects actual subscription completion.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 231d12a and 244f09d.

📒 Files selected for processing (1)
  • src/lib/cache/provider-cache.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use 2-space indentation in all code files

Files:

  • src/lib/cache/provider-cache.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use double quotes for strings instead of single quotes
Use trailing commas in multi-line structures
Enforce maximum line length of 100 characters
Use path alias @/* to reference files from ./src/* directory

**/*.{ts,tsx,js,jsx}: Use Biome for linting and formatting with 2-space indent, double quotes, trailing commas, and 100 character max line length
Use path alias @/* to reference files in ./src/* directory

Files:

  • src/lib/cache/provider-cache.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript strict mode for type safety
Use readonly or const assertions for immutable data structures

Files:

  • src/lib/cache/provider-cache.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Hash API keys using SHA-256 before storing in database, never store plaintext keys
Mask API keys and sensitive data in application logs
Validate required environment variables at startup with clear error messages

Files:

  • src/lib/cache/provider-cache.ts
src/**/*provider*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Set provider circuit breaker failure threshold, open duration, and half-open success threshold in configuration

Files:

  • src/lib/cache/provider-cache.ts
src/lib/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use connection pooling for database and Redis connections

Files:

  • src/lib/cache/provider-cache.ts
🧠 Learnings (2)
📚 Learning: 2026-01-03T09:08:20.573Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-03T09:08:20.573Z
Learning: Applies to src/lib/session-manager.ts : Session Manager must use 5-minute Redis context cache with sliding window and record decision chains for audit trail

Applied to files:

  • src/lib/cache/provider-cache.ts
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.

Applied to files:

  • src/lib/cache/provider-cache.ts
🧬 Code graph analysis (1)
src/lib/cache/provider-cache.ts (2)
src/types/provider.ts (1)
  • Provider (39-142)
src/lib/redis/pubsub.ts (2)
  • subscribeCacheInvalidation (60-97)
  • publishCacheInvalidation (44-53)
🔇 Additional comments (5)
src/lib/cache/provider-cache.ts (5)

1-30: LGTM!

模块文档清晰说明了缓存的关键特性(TTL、Redis Pub/Sub、降级策略、版本控制、请求级快照支持)。类型定义完善,ProviderCacheState 中的 versionrefreshPromise 字段为防止并发竞态和请求合并提供了良好的基础。


67-72: LGTM!

invalidateCache 正确执行了所有必要的清理步骤:清除缓存数据、重置过期时间、递增版本号(用于防止进行中的刷新覆盖失效状态)、清除刷新 Promise。逻辑完整且正确。


82-86: LGTM!

跨实例缓存失效逻辑正确:先调用 invalidateCache() 清除本地缓存,再通过 publishCacheInvalidation 向 Redis Pub/Sub 发布通知。采用失效通知而非全量数据同步的设计合理,既保证了类型安全(Date 等类型从 DB 正确构造),也避免了敏感数据(如 provider.key)通过 Redis 传输。


94-138: LGTM!

缓存核心逻辑设计精良:

  1. 缓存命中路径(第 103-105 行):直接返回有效缓存数据。
  2. 并发请求合并(第 108-110 行):通过复用 refreshPromise 防止重复刷新。
  3. 版本控制(第 113、119、131 行):通过版本号检查防止已失效的数据覆盖更新后的缓存状态,有效避免竞态条件。
  4. 清理逻辑(第 131-133 行):finally 块确保刷新完成后清除 refreshPromise,允许后续刷新。

第 98 行的 void ensureSubscription() 采用异步非阻塞方式初始化订阅,符合降级策略设计(Redis 不可用时 TTL 仍然有效)。


143-172: LGTM!

两个辅助函数实现良好:

  • warmupProviderCache(第 143-152 行):在启动时预热缓存,包含完善的错误处理和日志记录,即使预热失败也不会影响后续正常运行(首次请求时会触发刷新)。
  • getProviderCacheStats(第 157-172 行):提供缓存状态统计信息用于监控和调试,expiresIn 使用 Math.max(0, ...) 防止负值,所有字段计算正确。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider enhancement New feature or request size/S Small PR (< 200 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants

Comments