(new): rich message support
(fix): runtime reliability (tests): regression coverage (doc): v1.1 release notes
This commit is contained in:
+20
-9
@@ -157,9 +157,8 @@ func (rl *RateLimiter) GlobalAllow() bool {
|
||||
return limiter.Allow()
|
||||
}
|
||||
|
||||
// Allow checks if a request for the given chat can be made without blocking.
|
||||
// Returns false if: global cooldown, chat cooldown, global limiter, or chat limiter denies.
|
||||
// Note: Global limiter is checked before chat limiter — upstream limits take priority.
|
||||
// Allow checks whether a request for the given chat can be made without blocking.
|
||||
// A rejected chat reservation does not consume global capacity.
|
||||
func (rl *RateLimiter) Allow(chatID int64) bool {
|
||||
// Check global cooldown
|
||||
rl.globalMu.RLock()
|
||||
@@ -177,15 +176,27 @@ func (rl *RateLimiter) Allow(chatID int64) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check global token bucket
|
||||
limiter := rl.getGlobalLimiter()
|
||||
if limiter != nil && !limiter.Allow() {
|
||||
return false
|
||||
now := time.Now()
|
||||
globalLimiter := rl.getGlobalLimiter()
|
||||
var globalReservation *rate.Reservation
|
||||
if globalLimiter != nil {
|
||||
globalReservation = globalLimiter.ReserveN(now, 1)
|
||||
if !globalReservation.OK() || globalReservation.DelayFrom(now) > 0 {
|
||||
globalReservation.CancelAt(now)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check chat token bucket
|
||||
chatLimiter := rl.getChatLimiter(chatID)
|
||||
return chatLimiter.Allow()
|
||||
chatReservation := chatLimiter.ReserveN(now, 1)
|
||||
if !chatReservation.OK() || chatReservation.DelayFrom(now) > 0 {
|
||||
chatReservation.CancelAt(now)
|
||||
if globalReservation != nil {
|
||||
globalReservation.CancelAt(now)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Check applies rate limiting based on configuration.
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func TestRateLimiterCheckDropOverflowHonorsGlobalLock(t *testing.T) {
|
||||
@@ -28,6 +30,25 @@ func TestRateLimiterChatLocksAreScopedPerChat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterRejectedChatDoesNotConsumeGlobalCapacity(t *testing.T) {
|
||||
rl := NewRateLimiter()
|
||||
rl.SetGlobalRate(1)
|
||||
|
||||
if !rl.Allow(42) {
|
||||
t.Fatal("expected initial request for chat 42 to succeed")
|
||||
}
|
||||
rl.globalMu.Lock()
|
||||
rl.globalLimiter = rate.NewLimiter(1, 1)
|
||||
rl.globalMu.Unlock()
|
||||
|
||||
if rl.Allow(42) {
|
||||
t.Fatal("expected exhausted chat limiter to reject the request")
|
||||
}
|
||||
if !rl.Allow(7) {
|
||||
t.Fatal("expected rejected chat request not to consume global capacity")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterGlobalWaitRespectsContextCancellation(t *testing.T) {
|
||||
rl := NewRateLimiter()
|
||||
rl.SetGlobalLock(1)
|
||||
|
||||
Reference in New Issue
Block a user