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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}