(new): rich message support
Golang lint / lint (pull_request) Successful in 1m20s
Golang lint / lint (push) Successful in 4m8s

(fix): runtime reliability
(tests): regression coverage
(doc): v1.1 release notes
This commit is contained in:
2026-08-12 16:34:44 +03:00
parent 48ddf66540
commit f03a081ed6
83 changed files with 6122 additions and 1925 deletions
+20 -9
View File
@@ -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.
+21
View File
@@ -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)