Skip to content

fix(rate-limit): unify lease mechanism for all periodic cost limits#674

Merged
ding113 merged 1 commit intodevfrom
fix/lease-module-consistency
Jan 28, 2026
Merged

fix(rate-limit): unify lease mechanism for all periodic cost limits#674
ding113 merged 1 commit intodevfrom
fix/lease-module-consistency

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 28, 2026

Summary

  • Migrate User daily quota from checkUserDailyCost to checkCostLimitsWithLease
  • Align key-quota.ts and keys.ts to use DB direct (sumKeyCostInTimeRange) for consistency with my-usage.ts
  • All periodic limits (5h/daily/weekly/monthly) now use lease mechanism for Key/User/Provider
  • Total limits remain on 5-min Redis cache (no time window applicable)

Problem

The rate limiting system had inconsistent data sources and mechanisms:

  1. User daily quota used checkUserDailyCost while other periodic limits used checkCostLimitsWithLease
  2. Admin interfaces (key-quota.ts, keys.ts) used Redis-first queries while my-usage.ts used DB direct queries
  3. getTotalUsageForKey did not exclude warmup requests, causing potential billing inconsistencies

Related Issues:

Solution

  1. Unified Lease Mechanism: Migrated User daily quota to checkCostLimitsWithLease for consistency with other periodic limits
  2. DB Direct Queries: Changed admin interfaces to use sumKeyCostInTimeRange instead of RateLimitService.getCurrentCost
  3. Warmup Exclusion: Added EXCLUDE_WARMUP_CONDITION to getTotalUsageForKey
  4. Parser Enhancement: Updated parseLimitInfo to handle both checkCostLimits and checkCostLimitsWithLease error formats

Lease Usage Matrix (After Migration)

Check Type Key User Provider Uses Lease?
5h limit Yes Yes Yes Yes
Daily limit Yes Yes Yes Yes
Weekly Yes Yes Yes Yes
Monthly Yes Yes Yes Yes
Total Yes Yes Yes No (5-min cache)

Changes

Core Changes

  • rate-limit-guard.ts: Migrate User daily check from checkUserDailyCost to checkCostLimitsWithLease (+89/-73)
  • provider-selector.ts: Update Provider checks to use checkCostLimitsWithLease (+3/-3)
  • usage-logs.ts: Add EXCLUDE_WARMUP_CONDITION to getTotalUsageForKey (+7/-1)

Data Source Alignment

  • key-quota.ts: Switch from RateLimitService.getCurrentCost to sumKeyCostInTimeRange (+24/-11)
  • keys.ts: Switch from RateLimitService.getCurrentCost to sumKeyCostInTimeRange (+25/-15)
  • my-usage.ts: Align Key quota queries with User quota (DB direct) (+28/-19)

Tests

  • my-usage-consistency.test.ts: New test file documenting data source consistency (+221 lines)
  • rate-limit-guard.test.ts: Update mocks and add User daily rolling mode tests (+124/-29)
  • provider-selector-total-limit.test.ts: Update mock method names (+13/-9)

Test Plan

  • bun run typecheck passes
  • bun run lint passes (modified files)
  • 31 related unit tests pass
  • User daily rolling mode tested
  • Key daily check order verified (Key before User)

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • No breaking changes to external APIs

Description enhanced by Claude AI

Greptile Overview

Greptile Summary

Unified all periodic cost limits (5h/daily/weekly/monthly) to use the lease mechanism (checkCostLimitsWithLease), migrating User daily quota from the standalone checkUserDailyCost function.

Key Changes:

  • User daily quota now uses checkCostLimitsWithLease for consistency with Key and Provider periodic limits
  • All quota display UIs (my-usage.ts, key-quota.ts, keys.ts) now use direct DB queries (sumKeyCostInTimeRange) instead of Redis-first approach for data consistency
  • parseLimitInfo in rate-limit-guard.ts updated to parse both old (X/Y) and new (usage: X/Y) error formats
  • getTotalUsageForKey now excludes warmup requests via EXCLUDE_WARMUP_CONDITION for consistency
  • Comprehensive test coverage including rolling mode, check order verification, and format parsing

Benefits:

  • Reduced database query pressure through lease-based caching
  • Atomic budget deduction via Lua scripts
  • Unified fail-open strategy across all periodic limits
  • Data source consistency eliminates Redis/DB value mismatches

Confidence Score: 5/5

  • Safe to merge - well-tested refactoring with comprehensive test coverage and clear architectural benefits
  • Score reflects thorough implementation with 31 passing unit tests, clear migration path, data consistency improvements, and no breaking changes to external behavior
  • No files require special attention - all changes are well-structured and properly tested

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/rate-limit-guard.ts migrated User daily quota check from checkUserDailyCost to checkCostLimitsWithLease, updated parseLimitInfo to support both error message formats
src/repository/usage-logs.ts added EXCLUDE_WARMUP_CONDITION to getTotalUsageForKey for consistency with other statistics functions
src/actions/my-usage.ts replaced Key quota queries with direct DB access (sumKeyCostInTimeRange, sumKeyTotalCostById) for data source consistency
tests/unit/proxy/rate-limit-guard.test.ts updated all test cases to use checkCostLimitsWithLease format, added new tests for User daily rolling mode and check order verification

Sequence Diagram

sequenceDiagram
    participant Client
    participant RateLimitGuard
    participant RateLimitService
    participant LeaseCache
    participant Redis
    participant Database

    Client->>RateLimitGuard: API Request
    
    Note over RateLimitGuard: Check Order:<br/>1. Total limits<br/>2. Session/RPM<br/>3-6. Periodic limits (5h, daily)<br/>7-10. Weekly, Monthly

    RateLimitGuard->>RateLimitService: checkCostLimitsWithLease(key, "key", {5h, daily, weekly, monthly})
    
    RateLimitService->>LeaseCache: Get cached lease slice
    
    alt Lease exists and valid
        LeaseCache-->>RateLimitService: Return cached slice
        Note over RateLimitService: Use cached budget<br/>(reduce DB pressure)
    else Lease expired or missing
        LeaseCache-->>RateLimitService: Cache miss
        RateLimitService->>Database: sumKeyCostInTimeRange(keyId, startTime, endTime)
        Database-->>RateLimitService: Current usage
        RateLimitService->>Redis: Store new lease slice (Lua script)
        Redis-->>RateLimitService: Lease created
    end
    
    RateLimitService->>RateLimitService: Check usage < limit
    RateLimitService-->>RateLimitGuard: {allowed: true/false, reason}
    
    alt User daily check needed
        RateLimitGuard->>RateLimitService: checkCostLimitsWithLease(user, "user", {daily})
        Note over RateLimitService: Same lease mechanism<br/>for User quota
        RateLimitService-->>RateLimitGuard: {allowed: true/false, reason}
    end
    
    alt All checks pass
        RateLimitGuard-->>Client: Request allowed
    else Any check fails
        RateLimitGuard->>RateLimitGuard: parseLimitInfo(reason)
        Note over RateLimitGuard: Extract usage from:<br/>(usage: X/Y) format
        RateLimitGuard-->>Client: RateLimitError with details
    end
Loading

- Migrate User daily quota from checkUserDailyCost to checkCostLimitsWithLease
- Align key-quota.ts and keys.ts to use DB direct (sumKeyCostInTimeRange)
- All periodic limits (5h/daily/weekly/monthly) now use lease mechanism
- Total limits remain on 5-min Redis cache (no time window applicable)

Closes #673

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

将成本计算从 RateLimitService.getCurrentCost 直接查询迁移至基于时间范围的 sumKeyCostInTimeRange 聚合方式,替换 checkCostLimits 为 checkCostLimitsWithLease,并修复 getTotalUsageForKey 中的 Warmup 数据排除不一致问题。

Changes

内聚组 / 文件 变更摘要
成本计算迁移
src/actions/key-quota.ts, src/actions/keys.ts, src/actions/my-usage.ts
将成本查询从 RateLimitService.getCurrentCost 重构为基于时间范围的 sumKeyCostInTimeRange 和 sumKeyTotalCost 调用;引入 getTimeRangeForPeriod 等时间工具支持动态计算 5h、日、周、月时间范围;采用 key 的 dailyResetTime 和 dailyResetMode 计算日期时间范围
租约感知成本检查
src/app/v1/_lib/proxy/provider-selector.ts, src/app/v1/_lib/proxy/rate-limit-guard.ts
将 checkCostLimits 替换为 checkCostLimitsWithLease;扩展 parseLimitInfo 以解析两种错误格式;为日期检查增加滚动/固定重置模式处理,包括 resetTime 计算和本地化错误消息
数据一致性修复
src/repository/usage-logs.ts
getTotalUsageForKey 的 WHERE 条件增加 EXCLUDE_WARMUP_CONDITION,与 statistics.ts 中 sumKeyTotalCost 的 Warmup 排除逻辑一致
测试更新
tests/unit/actions/my-usage-consistency.test.ts, tests/unit/proxy/provider-selector-total-limit.test.ts, tests/unit/proxy/rate-limit-guard.test.ts
添加一致性和解析逻辑测试;将所有 mock 和断言从 checkCostLimits 迁移至 checkCostLimitsWithLease;验证租约检查调用顺序和新的错误原因格式;新增滚动/固定重置模式场景测试

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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
Title check ✅ Passed PR标题清晰准确地反映了主要变更:统一所有周期性成本限制采用lease机制。
Description check ✅ Passed PR描述详细说明了迁移内容、引入的lease矩阵表格,以及测试计划,与变更集相关。
Linked Issues check ✅ Passed PR解决了#673中的inconsistency问题。通过在getTotalUsageForKey中添加EXCLUDE_WARMUP_CONDITION,使Warmup排除逻辑与statistics.sumKeyTotalCost一致。
Out of Scope Changes check ✅ Passed 所有变更都在scope内:将user daily quota迁移到checkCostLimitsWithLease、统一DB直接查询方式、确保周期性限制使用lease机制、修复Warmup排除inconsistency。

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/lease-module-consistency

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ding113, 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!

This pull request significantly refactors the rate-limiting system by standardizing all periodic cost limits to use a lease-based mechanism. This change improves consistency in how limits are applied and reported across different entities (Key, User, Provider) and various timeframes. It also ensures that all quota-related UI components fetch data from a unified, direct database source, resolving potential discrepancies caused by mixed data retrieval strategies.

Highlights

  • Unified Lease Mechanism: All periodic cost limits (5h, daily, weekly, monthly) for Key, User, and Provider entities now consistently utilize the checkCostLimitsWithLease mechanism, replacing older checkCostLimits and checkUserDailyCost methods.
  • Data Source Consistency: The key-quota.ts and keys.ts files have been updated to directly query the database using sumKeyCostInTimeRange for periodic key usage, aligning their data source with my-usage.ts to ensure consistent quota reporting across different UI components.
  • User Daily Quota Migration: The User daily quota check has been fully migrated from checkUserDailyCost to the unified checkCostLimitsWithLease function, streamlining rate limit logic.
  • Improved Limit Info Parsing: The parseLimitInfo utility function in rate-limit-guard.ts has been enhanced to correctly interpret both the legacy checkCostLimits format and the new checkCostLimitsWithLease output format for limit reasons.
  • Enhanced Test Coverage: New unit tests (my-usage-consistency.test.ts) have been added to explicitly verify the data source consistency for Key and User quotas, the correct parsing of limit information, and the adoption of the lease mechanism.
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.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

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

This pull request successfully unifies the periodic rate limit checks to use a consistent lease-based mechanism, which is a great improvement for maintainability and performance. The changes also align data sources for quota display, enhancing consistency across the UI. My review includes a couple of suggestions for further refactoring to improve code clarity and robustness.


if (!dailyCheck.allowed) {
logger.warn(`[RateLimit] User daily limit exceeded: user=${user.id}, ${dailyCheck.reason}`);
const { currentUsage, limitValue } = parseLimitInfo(userDailyCheck.reason!);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Parsing values from a formatted string can be brittle. A future change to the error message format in checkCostLimitsWithLease could break this parsing logic.

For better robustness and maintainability, consider refactoring checkCostLimitsWithLease to return a structured object with currentUsage and limitValue fields, similar to how checkTotalCostLimit returns a current value. This would eliminate the need for string parsing here.

@github-actions github-actions bot added size/M Medium PR (< 500 lines) bug Something isn't working area:Rate Limit labels Jan 28, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Caution

The CodeRabbit agent's plans did not produce any file changes.

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

No significant issues identified in this PR. The changes correctly migrate User daily quota from checkUserDailyCost to checkCostLimitsWithLease and align admin interfaces (key-quota.ts, keys.ts) to use DB direct queries for consistency with my-usage.ts.

PR Size: M

  • Lines changed: 694 (534 additions, 160 deletions)
  • Files changed: 9

Review Coverage

  • Logic and correctness - Clean
    • Migration from checkCostLimits to checkCostLimitsWithLease is consistent
    • parseLimitInfo correctly handles both old and new error message formats
    • Time range calculations use appropriate functions (getTimeRangeForPeriod, getTimeRangeForPeriodWithMode)
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
    • Error message parsing has appropriate fallback behavior
    • Logging is consistent with existing patterns
  • Type safety - Clean
    • DailyResetMode type casts are consistent with existing codebase patterns
  • Documentation accuracy - Clean
    • Comments accurately describe the migration and new behavior
    • Test documentation clearly explains the consistency fix
  • Test coverage - Adequate
    • 221 lines of new tests covering parseLimitInfo format parsing
    • Tests verify both checkCostLimits and checkCostLimitsWithLease formats
    • Documentation tests explain the lease usage matrix
  • Code clarity - Good
    • Consistent naming and structure across modified files
    • Clear separation of Key vs User time range calculations

Notes

  • The EXCLUDE_WARMUP_CONDITION addition to getTotalUsageForKey ensures consistency with other statistics functions
  • All periodic cost limits (5h/daily/weekly/monthly) now use the lease mechanism uniformly
  • Total limits remain on 5-min Redis cache (no time window applicable) as documented

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)
tests/unit/proxy/rate-limit-guard.test.ts (1)

3-41: 测试 mock 的 ERROR_CODES 缺少 RATE_LIMIT_5H_ROLLING_EXCEEDED

当前实现已使用 RATE_LIMIT_5H_ROLLING_EXCEEDED,但 mock 中未定义,会把 undefined 传给 getErrorMessageServer,降低用例对回归的敏感度。建议补齐常量。

建议修改
   ERROR_CODES: {
     RATE_LIMIT_TOTAL_EXCEEDED: "RATE_LIMIT_TOTAL_EXCEEDED",
     RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED: "RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED",
     RATE_LIMIT_RPM_EXCEEDED: "RATE_LIMIT_RPM_EXCEEDED",
     RATE_LIMIT_DAILY_QUOTA_EXCEEDED: "RATE_LIMIT_DAILY_QUOTA_EXCEEDED",
     RATE_LIMIT_5H_EXCEEDED: "RATE_LIMIT_5H_EXCEEDED",
+    RATE_LIMIT_5H_ROLLING_EXCEEDED: "RATE_LIMIT_5H_ROLLING_EXCEEDED",
     RATE_LIMIT_WEEKLY_EXCEEDED: "RATE_LIMIT_WEEKLY_EXCEEDED",
     RATE_LIMIT_MONTHLY_EXCEEDED: "RATE_LIMIT_MONTHLY_EXCEEDED",
   },
🧹 Nitpick comments (3)
tests/unit/proxy/provider-selector-total-limit.test.ts (1)

148-159: 测试断言与新的 Lease 机制保持一致。

测试正确验证了 checkCostLimitsWithLease 的调用参数,包含所有必要的限额字段。

建议:第一个测试(filterByLimits 测试,第 40-101 行)未显式验证 checkCostLimitsWithLease 的调用。根据 provider-selector.ts 中的实现,filterByLimits 也会调用此方法。可以考虑在该测试中添加类似的断言以确保覆盖完整。

tests/unit/actions/my-usage-consistency.test.ts (1)

89-119: 建议:将文档测试转换为真实的集成测试。

当前测试使用 vi.doMock 但未实际调用被 mock 的模块,导致 mock 未被使用。如果目的是文档记录,现有方式可接受。但如果希望真正验证数据源一致性,建议:

  1. 实际导入并调用 getMyQuota 或相关函数
  2. 验证 mock 函数被正确调用
  3. 或者将此类文档移至 README/ADR 文档中
示例:将文档测试转为真实测试
 it("should use sumKeyCostInTimeRange for Key quota (not RateLimitService.getCurrentCost)", async () => {
-  // This test documents the expected behavior:
-  // Key quota should use direct DB query (sumKeyCostInTimeRange) instead of Redis-first (getCurrentCost)
-
-  // Mock the statistics module
   const sumKeyCostInTimeRangeMock = vi.fn(async () => 10.5);
-  const sumKeyTotalCostByIdMock = vi.fn(async () => 100.25);
-  const sumUserCostInTimeRangeMock = vi.fn(async () => 10.5);
-  const sumUserTotalCostMock = vi.fn(async () => 100.25);
-
-  vi.doMock("@/repository/statistics", () => ({
-    sumKeyCostInTimeRange: sumKeyCostInTimeRangeMock,
-    sumKeyTotalCostById: sumKeyTotalCostByIdMock,
-    sumUserCostInTimeRange: sumUserCostInTimeRangeMock,
-    sumUserTotalCost: sumUserTotalCostMock,
-  }));
-
-  // Verify the function signatures match
-  expect(typeof sumKeyCostInTimeRangeMock).toBe("function");
-  expect(typeof sumKeyTotalCostByIdMock).toBe("function");
-
-  // The test validates that:
-  // 1. Key 5h/daily/weekly/monthly uses sumKeyCostInTimeRange (DB direct)
-  // ...
+  vi.mock("@/repository/statistics", () => ({
+    sumKeyCostInTimeRange: sumKeyCostInTimeRangeMock,
+    // ... other mocks
+  }));
+  
+  // Import after mocking
+  const { getMyQuota } = await import("@/actions/my-usage");
+  
+  // Call and verify
+  await getMyQuota(/* test params */);
+  expect(sumKeyCostInTimeRangeMock).toHaveBeenCalled();
 });
src/actions/my-usage.ts (1)

237-292: 建议复用已计算的时间范围并避免调用已弃用的 sumUserCost

目前 5h/weekly/monthly/total 仍走 sumUserCost,会二次计算时间范围并引入额外异步开销,也可能与 Key 侧窗口存在微小偏差。可以直接用已算好的 range5h/rangeWeekly/rangeMonthly + sumUserCostInTimeRange,并直接调用 sumUserTotalCost,同时去掉对已弃用函数的依赖。

建议修改
-    const { sumUserCostInTimeRange, sumKeyCostInTimeRange, sumKeyTotalCostById } = await import(
-      "@/repository/statistics"
-    );
+    const {
+      sumUserCostInTimeRange,
+      sumUserTotalCost,
+      sumKeyCostInTimeRange,
+      sumKeyTotalCostById,
+    } = await import("@/repository/statistics");
-      sumUserCost(user.id, "5h"),
+      sumUserCostInTimeRange(user.id, range5h.startTime, range5h.endTime),
       sumUserCostInTimeRange(user.id, userDailyTimeRange.startTime, userDailyTimeRange.endTime),
-      sumUserCost(user.id, "weekly"),
-      sumUserCost(user.id, "monthly"),
-      sumUserCost(user.id, "total"),
+      sumUserCostInTimeRange(user.id, rangeWeekly.startTime, rangeWeekly.endTime),
+      sumUserCostInTimeRange(user.id, rangeMonthly.startTime, rangeMonthly.endTime),
+      sumUserTotalCost(user.id),

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

Labels

area:Rate Limit bug Something isn't working size/M Medium PR (< 500 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant