Add limit.Service.Check, a bucket check that uses nothing but reports the wait

This commit is contained in:
binwiederhier committed 2026-11-01 11:47:51 +00:00
1 parent e04b01af6d
commit b916f216fc
2 files changed
+43

No files matched your search

+23
View File
@@ -187,6 +187,29 @@ func (s *Service) Tokens(key string, rule Rule) int64 {
return int64(math.Max(0, math.Floor(b.tokens)))
}
// Check decides like Allow whether the key's bucket for the rule has one unit now, without using
// it. Like Tokens, a rule without a bucket admits nothing. It is for "may I try" checks that charge
// only on failure, and still need to tell the client how long to wait.
func (s *Service) Check(key string, rule Rule) Decision {
if rule.Burst <= 0 {
return Decision{}
}
now := s.now()
bs := s.buckets[shardIndex(key)]
bs.mu.Lock()
defer bs.mu.Unlock()
e, ok := bs.entries[key]
if !ok {
return Decision{OK: true} // A fresh bucket is full
}
b := &e.buckets[rule.Kind]
b.refill(now, &rule)
if b.tokens < 1 {
return Decision{RetryAfter: b.wait(1, &rule)}
}
return Decision{OK: true}
}
// Refund hands n units of the kind back to the key, e.g. for bytes that were reserved but not
// sent. The bucket cannot exceed its burst, and the daily count cannot go below zero.
func (s *Service) Refund(key string, kind Kind, n int64) {
+20
View File
@@ -271,6 +271,26 @@ func TestService_Tokens_DoesNotConsume(t *testing.T) {
require.Equal(t, int64(0), s.Tokens("k", Rule{Kind: KindAuth})) // No bucket, no tokens
}
func TestService_Check(t *testing.T) {
// Check answers like Allow would for one unit, including the wait, but uses nothing
s, clock := newTestServiceWithClock(t)
rule := Rule{
Kind: KindAuth,
Burst: 2,
Refill: time.Minute,
}
require.Equal(t, Decision{OK: true}, s.Check("k", rule)) // A fresh bucket is full
require.True(t, s.Allow("k", rule, 2).OK)
require.Equal(t, Decision{RetryAfter: time.Minute}, s.Check("k", rule))
clock.advance(20 * time.Second)
require.Equal(t, Decision{RetryAfter: 40 * time.Second}, s.Check("k", rule))
require.Equal(t, int64(0), s.Tokens("k", rule)) // Checking did not take anything
clock.advance(40 * time.Second)
require.Equal(t, Decision{OK: true}, s.Check("k", rule))
require.Equal(t, int64(1), s.Tokens("k", rule))
require.Equal(t, Decision{}, s.Check("k", Rule{Kind: KindAuth})) // No bucket admits nothing, as Tokens says 0
}
func TestService_Prune(t *testing.T) {
s, clock := newTestServiceWithClock(t)
rule := Rule{