Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

ratelimit.gno

5.08 Kb · 144 lines
  1// Package ratelimit is a deterministic token-bucket rate limiter — a port of
  2// golang.org/x/time/rate with the wall clock replaced by a caller-supplied
  3// monotonic tick (on-chain, that tick is the block height).
  4//
  5// It is a pure library: it imports no chain APIs and reads no ambient state.
  6// A [Limiter] owns a set of per-key token buckets sharing one rate/burst
  7// config; the caller decides what a key is (typically an address string) and
  8// supplies the current tick on every call. Between two observations at ticks
  9// `last` and `now`, a bucket gains (now-last)*rate tokens, capped at `burst`.
 10// Everything is integer/float arithmetic over persistent avl state, hence
 11// deterministic and replayable.
 12//
 13// A realm wires it up by holding a *Limiter in a package-level var and feeding
 14// it runtime.ChainHeight() as the tick. For a complete, live example see the
 15// demo realm [r/moul/x/daily/ratelimitdemo](/r/moul/x/daily/ratelimitdemo/v0).
 16package ratelimit
 17
 18import "gno.land/p/nt/avl/v0"
 19
 20// bucket is the per-key state persisted in the avl tree.
 21type bucket struct {
 22	tokens float64 // tokens available as of `last`
 23	last   int64   // tick at which `tokens` was computed
 24}
 25
 26// Limiter is a set of per-key token buckets sharing one rate/burst config.
 27// The zero value is not usable; construct one with [New].
 28type Limiter struct {
 29	rate    float64   // tokens replenished per tick
 30	burst   float64   // bucket capacity (max tokens, largest single burst)
 31	buckets *avl.Tree // key string -> *bucket, ordered by key (deterministic)
 32}
 33
 34// New returns a Limiter replenishing `rate` tokens per tick, each bucket capped
 35// at `burst`. rate is clamped to >= 0, burst to >= 1.
 36func New(rate, burst float64) *Limiter {
 37	l := &Limiter{buckets: avl.NewTree()}
 38	l.SetConfig(rate, burst)
 39	return l
 40}
 41
 42// SetConfig updates the shared rate and burst. rate is clamped to >= 0, burst
 43// to >= 1. Existing buckets keep their stored tokens; the new config applies
 44// from the next refill.
 45func (l *Limiter) SetConfig(rate, burst float64) {
 46	if rate < 0 {
 47		rate = 0
 48	}
 49	if burst < 1 {
 50		burst = 1
 51	}
 52	l.rate = rate
 53	l.burst = burst
 54}
 55
 56// Config returns the current rate (tokens/tick) and burst (capacity).
 57func (l *Limiter) Config() (rate, burst float64) { return l.rate, l.burst }
 58
 59// Allow consumes one token for `key` at tick `now` and reports whether the
 60// request is permitted. When the bucket is empty it returns false and consumes
 61// nothing. Equivalent to AllowN(key, now, 1).
 62func (l *Limiter) Allow(key string, now int64) bool {
 63	return l.AllowN(key, now, 1)
 64}
 65
 66// AllowN consumes `n` tokens for `key` at tick `now` and reports whether the
 67// request is permitted. When fewer than `n` tokens are available it returns
 68// false and consumes nothing. A key is seen for the first time with a full
 69// bucket of `burst` tokens.
 70func (l *Limiter) AllowN(key string, now int64, n float64) bool {
 71	b := l.load(key, now)
 72	ok := b.tokens >= n
 73	if ok {
 74		b.tokens -= n
 75	}
 76	l.buckets.Set(key, b)
 77	return ok
 78}
 79
 80// Tokens is a read-only view of how many whole tokens `key` has available at
 81// tick `now`, without mutating any state.
 82func (l *Limiter) Tokens(key string, now int64) int {
 83	if !l.buckets.Has(key) {
 84		return tokensToInt(l.burst)
 85	}
 86	b := l.buckets.Get(key).(*bucket)
 87	return tokensToInt(refill(b.tokens, b.last, now, l.rate, l.burst))
 88}
 89
 90// Len returns the number of keys the limiter has seen.
 91func (l *Limiter) Len() int { return l.buckets.Size() }
 92
 93// Iterate calls fn for every known key in ascending order, passing the whole
 94// tokens available at tick `now` and the last tick the key was observed.
 95// Returning true from fn stops the iteration early; Iterate reports whether it
 96// was stopped that way.
 97func (l *Limiter) Iterate(now int64, fn func(key string, tokens int, last int64) bool) bool {
 98	return l.buckets.Iterate("", "", func(key string, value any) bool {
 99		b := value.(*bucket)
100		return fn(key, tokensToInt(refill(b.tokens, b.last, now, l.rate, l.burst)), b.last)
101	})
102}
103
104// load returns the live bucket for key, refilled to `now`, creating a full
105// bucket the first time a key is seen. The returned bucket is not yet stored;
106// callers that mutate it must Set it back.
107func (l *Limiter) load(key string, now int64) *bucket {
108	if l.buckets.Has(key) {
109		b := l.buckets.Get(key).(*bucket)
110		b.tokens = refill(b.tokens, b.last, now, l.rate, l.burst)
111		b.last = now
112		return b
113	}
114	return &bucket{tokens: l.burst, last: now}
115}
116
117// --- pure helpers (unit-tested) -------------------------------------------
118
119// fmin returns the smaller of two float64 values.
120func fmin(a, b float64) float64 {
121	if a < b {
122		return a
123	}
124	return b
125}
126
127// refill returns the token count at tick `now` given `tokens` observed at tick
128// `last`, replenishing at `r` tokens/tick up to capacity `cap`. It never
129// decreases below the stored value and never exceeds `cap`.
130func refill(tokens float64, last, now int64, r, cap float64) float64 {
131	if now <= last {
132		return fmin(tokens, cap)
133	}
134	elapsed := float64(now - last)
135	return fmin(cap, tokens+elapsed*r)
136}
137
138// tokensToInt floors a token count to a whole, non-negative token.
139func tokensToInt(t float64) int {
140	if t < 0 {
141		return 0
142	}
143	return int(t)
144}