Documentation
¶
Overview ¶
Package grudge implements a constant-memory sketch that maps an unbounded key space to a decaying scalar score updated by feedback. It is to behavioral scores what a count-min sketch is to counts.
A Sketch holds L levels of M cells. Each key maps to one cell per level via L hashes derived from a single 64-bit hash (Kirsch–Mitzenmacher). Scores decay toward zero on access (lazily, so an idle sketch does no work), and an Aggregator (Min by default) collapses a key's L per-level scores into one. Min-aggregation shields a key: its estimate is exact whenever at least one of its cells is free of colliding traffic.
The three properties that make the structure useful:
- Collision shielding via L independent levels and min-aggregation.
- Lazy decay: scores fade toward zero, computed on access, no background work.
- Rotation (see Rotator): periodic reseeding bounds how long any false positive can persist.
grudge is policy-free: it stores and decays scores but makes no throttle, allow, or sampling decision. Callers interpret scores. See spec/SPEC.md for the normative specification.
Index ¶
- func SuggestLevels(hotKeys, cellsPerLevel uint32, targetP float64) uint32
- type Aggregator
- type Clock
- type Config
- type Decay
- type Hasher
- type HasherFactory
- type Result
- type Rotator
- func (r *Rotator) Close()
- func (r *Rotator) Query(key []byte) float64
- func (r *Rotator) QueryDetailed(key []byte) Result
- func (r *Rotator) Rotations() int64
- func (r *Rotator) TryUpdate(key []byte, delta, limit float64) bool
- func (r *Rotator) Update(key []byte, delta float64)
- func (r *Rotator) UpdateAndQuery(key []byte, delta float64) float64
- type RotatorConfig
- type Sketch
- func (s *Sketch) Config() Config
- func (s *Sketch) Query(key []byte) float64
- func (s *Sketch) QueryDetailed(key []byte) Result
- func (s *Sketch) Seed() uint64
- func (s *Sketch) TryUpdate(key []byte, delta, limit float64) bool
- func (s *Sketch) Update(key []byte, delta float64)
- func (s *Sketch) UpdateAndQuery(key []byte, delta float64) float64
- type Ticker
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SuggestLevels ¶
SuggestLevels returns the number of levels L needed so that an innocent key is falsely dragged up (all of its cells contaminated) with probability at most targetP, given hotKeys concurrently-hot keys and cellsPerLevel cells per level. It solves, for L:
targetP = (1 - (1 - 1/M)^H)^L
where M = cellsPerLevel and H = hotKeys, and returns ceil(L) with a floor of 1. This is FAIR's CalculateL, relocated. The caller supplies M (memory and per-level collision are its choice) and gets the L that meets the target.
It panics on degenerate inputs — hotKeys == 0, cellsPerLevel < 2 (a single cell collides with certainty), or targetP outside (0, 1) — because these have no meaningful answer and reaching them signals a configuration error.
Types ¶
type Aggregator ¶
Aggregator collapses a key's per-level scores into a single value. The input slice has length L (one score per level) and is never empty. Implementations must not retain or mutate the slice; it is reused across calls.
var Max Aggregator = func(levelScores []float64) float64 {
m := levelScores[0]
for _, v := range levelScores[1:] {
if v > m {
m = v
}
}
return m
}
Max returns the largest level score.
var Mean Aggregator = func(levelScores []float64) float64 { var sum float64 for _, v := range levelScores { sum += v } return sum / float64(len(levelScores)) }
Mean returns the arithmetic mean of the level scores. Useful when Min is too strict.
var Min Aggregator = func(levelScores []float64) float64 {
m := levelScores[0]
for _, v := range levelScores[1:] {
if v < m {
m = v
}
}
return m
}
Min returns the smallest level score. It is the recommended default: it yields the collision-shielding property, since a key's estimate is dragged up only when every one of its cells is contaminated.
type Clock ¶
Clock abstracts the current time so sketches can be driven by a controlled clock in tests and simulations.
type Config ¶
type Config struct {
// Levels (L) is the number of hash levels; must be >= 1.
Levels uint32
// CellsPerLevel (M) is the number of cells per level; must be >= 1.
CellsPerLevel uint32
// Decay is the decay function. Required; use None to disable decay.
Decay Decay
// Lo and Hi bound every score; require Lo < Hi and Lo <= 0 <= Hi. Hi may
// be +Inf only when Decay is not None.
Lo, Hi float64
// Aggregator collapses per-level scores. Required; Min is recommended.
Aggregator Aggregator
// Hasher builds the per-generation hasher. nil uses Murmur3.
Hasher HasherFactory
// Clock supplies the current time. nil uses the real system clock.
Clock Clock
}
Config parameterizes a single Sketch (one generation).
type Decay ¶
type Decay interface {
// Apply returns the score after dtMillis of idle time. Callers guarantee
// dtMillis >= 0.
Apply(score float64, dtMillis int64) float64
}
Decay evolves a score over elapsed idle time. Implementations MUST be monotone toward zero (never increase |score|, never cross zero) and MUST compose across time subdivision:
Apply(Apply(s, t1), t2) == Apply(s, t1+t2) (within float64 epsilon)
Composition is what allows lazy evaluation (decaying only on access) and, later, the additive merge law to hold.
var None Decay = noneDecay{}
None is the identity decay: scores persist until explicitly changed or rotated out. Sketches using None may not set Hi = +Inf (scores could grow without bound).
func Exponential ¶
Exponential returns a decay of the form score·e^(−λ·Δt) where λ is per second. λ = 0 is a valid no-op decay (distinct from None only in type). This is FAIR's decay.
type Hasher ¶
Hasher produces a 64-bit hash of a key. The Sketch derives its L level indexes from this single hash via the Kirsch–Mitzenmacher construction.
type HasherFactory ¶
HasherFactory builds a Hasher bound to a seed. Rotation calls it once per generation with a fresh seed.
func Murmur3 ¶
func Murmur3() HasherFactory
Murmur3 is the default hasher factory: fast, but NOT resistant to an adversary who chooses keys (murmur3 has seed-independent collision families). For attacker-controlled key spaces use SipHash.
The murmur3 64-bit variant takes a uint32 seed; the low 32 bits of the uint64 seed are used. This truncation is harmless because the seed is random.
Caveat on hash strength: the sketch derives all L level indexes from this one 64-bit hash via the Kirsch–Mitzenmacher construction, which amplifies any hash weakness — a single 64-bit collision collides at every level and bypasses min-aggregation shielding entirely. With attacker-controlled keys, prefer SipHash.
func SipHash ¶
func SipHash() HasherFactory
SipHash is a keyed-PRF hasher factory for adversarial key spaces: an attacker who does not know the key cannot manufacture the collisions that would defeat the Kirsch–Mitzenmacher-amplified level shield (see the caveat on Murmur3). It costs roughly 2x a murmur3 hash. The uint64 seed is expanded into SipHash's 128-bit key via two splitmix64 iterations.
type Result ¶
type Result struct {
Aggregate float64
LevelScores []float64 // length L
CellIndexes []uint32 // length L
}
Result is the detailed outcome of a query.
type Rotator ¶
type Rotator struct {
// contains filtered or unexported fields
}
Rotator manages N generations of a Sketch under changing seeds. Reads serve from the oldest ("primary") generation; writes apply to all generations so younger ones are warm when promoted. Every Period the primary is dropped, the next generation is promoted, and a fresh generation is appended. This bounds the lifetime of any false positive to Generations x Period.
func NewRotator ¶
func NewRotator(cfg RotatorConfig) (*Rotator, error)
NewRotator creates a Rotator and starts its background rotation goroutine. Call Close to stop it.
func (*Rotator) Close ¶
func (r *Rotator) Close()
Close stops the rotation goroutine and releases the ticker. It is idempotent.
func (*Rotator) QueryDetailed ¶
QueryDetailed returns the primary generation's detailed query result.
func (*Rotator) Rotations ¶
Rotations returns the number of rotations performed (observability/tests).
func (*Rotator) TryUpdate ¶
TryUpdate decides on the primary generation's aggregate; if admitted, the delta is applied to all generations (warming the younger ones). The decision is atomic within the primary, but the fan-out to younger generations is not cross-generation atomic, matching the warm-write model.
type RotatorConfig ¶
type RotatorConfig struct {
// Sketch is the per-generation configuration.
Sketch Config
// Generations is the number of concurrent generations; must be >= 2.
Generations uint32
// Period is the rotation interval; must be > 0.
Period time.Duration
// Ticker drives rotation. nil uses a real ticker with Period.
Ticker Ticker
}
RotatorConfig parameterizes a Rotator.
type Sketch ¶
type Sketch struct {
// contains filtered or unexported fields
}
Sketch is one generation: L levels of M cells under a single hash seed. All exported methods are safe for concurrent use.
func NewWithSeed ¶
NewWithSeed creates a Sketch with a fixed seed. Two sketches built with the same seed, config, clock, and op sequence produce identical observable output.
func (*Sketch) QueryDetailed ¶
QueryDetailed decays the key's L cells and returns the aggregate along with per-level scores and cell indexes. Unlike the other operations it allocates its returned slices per call.
func (*Sketch) TryUpdate ¶
TryUpdate applies delta only if the post-decay aggregate plus delta stays within limit, returning whether it was applied. It is the atomic conditional-consume for strict quota semantics: unlike Query followed by Update, no concurrent operation on this key can interleave between the check and the apply.
It holds all L of the key's cell locks simultaneously — still deadlock-free because every operation acquires cells in ascending level order — so it costs more lock contention than the optimistic path. Callers that tolerate small over-admission should prefer Query + Update. Whether or not delta is applied, the cells are decayed to now.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package grudgetest provides controllable Clock and Ticker fakes for driving grudge (and its consumers, like FAIR and recipe packages) deterministically in tests.
|
Package grudgetest provides controllable Clock and Ticker fakes for driving grudge (and its consumers, like FAIR and recipe packages) deterministically in tests. |
|
internal
|
|
|
hash
Package hash provides the concrete 64-bit hashers used by grudge.
|
Package hash provides the concrete 64-bit hashers used by grudge. |