fix(ratelimit): hold streaming concurrency until stream end (#450 #30) - #481
Conversation
The streaming chat path released its concurrency reservation at handler return — before the SSE stream finished — so a key capped at N concurrent requests could run far more than N simultaneous streams. The borrow-based MultiReservation can't be carried into the stream, so it was simply dropped (TPM was already reconciled post-stream via add_tokens_post_stream, but concurrency was not). Add an owned StreamConcurrencyGuard and MultiReservation::into_stream_hold that keeps the per-layer concurrency permits held and releases them on drop. The guard is moved into the on_complete closure, which the CompleteOnDrop guard fires on both normal completion and mid-stream cancellation, so the permit is held for the stream's full lifetime and never leaked. /v1/messages and /v1/completions share the same handler-scope reservation pattern for their streaming paths; correct reservation handling for those lands when they route through the shared policy pipeline (#22). Part of #450 (finding #30)
|
Warning Review limit reached
More reviews will be available in 39 minutes and 30 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR fixes a concurrency limit overflow bug in streaming chat responses by extending the lifetime of rate-limit concurrency permits from request-handler scope to the entire SSE stream lifetime. A new ChangesStreaming Concurrency Hold for Rate Limiting
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@crates/aisix-ratelimit/src/limiter.rs`:
- Around line 370-381: into_stream_hold currently reassigns ownership of permits
to the caller-supplied Arc<Limiter<C>> which can leak permits or decrement the
wrong limiter if reservations come from a different Limiter; change it to verify
and capture the original limiter ownership instead of accepting an external one:
either (1) require that all Reservation entries reference the same
Arc<Limiter<C>> and assert/return Err if any reservation.limiter pointer != the
provided limiter, or (preferable) change into_stream_hold to take the limiter
from the reservations themselves (or capture the reserved states/Arc there) and
build StreamConcurrencyGuard using that captured Arc and the reservations'
internal reserved state, ensuring you still set each reservation.committed =
true and that StreamConcurrencyGuard owns the exact Limiter that created the
permits (apply the same fix to the similar code at the 389-405 path).
- Around line 389-393: The StreamConcurrencyGuard type can be dropped
immediately by callers which recreates the early-release bug; mark the guard as
must-use by adding the #[must_use] attribute to the StreamConcurrencyGuard
struct declaration so the compiler warns when the returned guard is ignored
(update the declaration around StreamConcurrencyGuard<C: Clock = SystemClock> to
include #[must_use]). Ensure the attribute applies to the public struct that
holds limiter, keys, and released so callers receive the warning when they fail
to keep the guard alive.
🪄 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: 45d60d48-a1d5-4187-917b-f70a70b4a61b
📒 Files selected for processing (3)
crates/aisix-proxy/src/chat.rscrates/aisix-ratelimit/src/lib.rscrates/aisix-ratelimit/src/limiter.rs
…invariant Address review: ignoring the returned guard immediately releases the permit (recreating the bug); document that the supplied limiter must be the one the reservation was acquired against.
Part of #450 (finding #30).
Problem
The streaming chat path (
/v1/chat/completions) released its concurrency reservation at handler return — before the SSE stream finished (chat.rspreviously diddrop(reservation)right afterchat_stream). A key capped at N concurrent requests could therefore run far more than N simultaneous streams. The borrow-basedMultiReservationcan't be carried into a stream that outlives the handler, so it was simply dropped. (TPM was already reconciled at stream end viaadd_tokens_post_streamfrom an earlier fix; concurrency was not.)Fix
Add an owned
StreamConcurrencyGuardandMultiReservation::into_stream_hold()(inaisix-ratelimit) that keeps the per-layer concurrency permits held and releases them on drop. The guard is moved into theon_completeclosure, which theCompleteOnDropguard fires on both normal completion and mid-stream cancellation — so the permit is held for the stream's full lifetime and never leaked.Scope / deferred siblings
/v1/messagesand/v1/completionsbind their reservation for handler scope only and share the same early-release pattern on their streaming paths. Correct reservation handling for those lands when they're routed through the shared policy pipeline in #22 (called out per the repo's whole-bug-class guideline).Tests
stream_hold_keeps_concurrency_until_guard_dropinlimiter.rs— a 2nd concurrent request is rejected while the hold is alive and admitted after it drops.Summary by CodeRabbit