package ratelimit import "sync" // Rejection aggregates the limiter rejections of one key within one report // window. Class is the limiter class (user, public, email or admin); Key is the // class-specific subject — an account id for the user class, a client IP for the // others. The JSON shape is the gateway→backend rate-limit report wire contract. type Rejection struct { Class string `json:"class"` Key string `json:"key"` Rejected int `json:"rejected"` } // Tracker accumulates limiter rejections between drains. The gateway's periodic // reporter drains it to emit the per-key log summary and the backend report; the // per-rejection cost is one map increment under a mutex, safe on the hot path. type Tracker struct { mu sync.Mutex m map[trackerKey]int } type trackerKey struct{ class, key string } // NewTracker constructs an empty Tracker. func NewTracker() *Tracker { return &Tracker{m: make(map[trackerKey]int)} } // Add counts one rejection of key under class. func (t *Tracker) Add(class, key string) { t.mu.Lock() defer t.mu.Unlock() t.m[trackerKey{class: class, key: key}]++ } // Drain returns the rejections accumulated since the previous drain, in // unspecified order, and resets the tracker. It returns nil when nothing was // rejected. func (t *Tracker) Drain() []Rejection { t.mu.Lock() defer t.mu.Unlock() if len(t.m) == 0 { return nil } out := make([]Rejection, 0, len(t.m)) for k, n := range t.m { out = append(out, Rejection{Class: k.class, Key: k.key, Rejected: n}) } clear(t.m) return out }