mirror of
https://github.com/hanzoai/tasks.git
synced 2026-11-03 03:56:04 +00:00
## What changed? Fairness counters can use a count-min sketch that can automatically grow on contention. But when it grows it drops all values. This adds a mechanism to preserve counts of the top 100 keys. ## Why? More accurate fairness ## How did you test it? - [x] added new unit test(s)
83 lines
1.9 KiB
Go
83 lines
1.9 KiB
Go
package counter
|
|
|
|
import (
|
|
"math/rand/v2"
|
|
)
|
|
|
|
type (
|
|
CounterParams struct {
|
|
MapLimit int
|
|
CMS CMSketchParams
|
|
}
|
|
|
|
// hybridCounter is a Counter that uses a mapCounter until it has params.MapLimit entries,
|
|
// then switches to a cmSketch. After switching, it continues to track the top MapLimit
|
|
// entries in the mapCounter, so they can be preserved when cmSketch resizes and persisted.
|
|
// hybridCounter is not safe for concurrent use.
|
|
hybridCounter struct {
|
|
mapCounter mapCounter
|
|
cmSketch *cmSketch
|
|
params CounterParams
|
|
src rand.Source
|
|
}
|
|
)
|
|
|
|
var _ Counter = (*hybridCounter)(nil)
|
|
|
|
var DefaultCounterParams = CounterParams{
|
|
MapLimit: 100,
|
|
CMS: CMSketchParams{
|
|
W: 100,
|
|
D: 5,
|
|
Grow: CMSGrowParams{
|
|
SkipRateDecay: 10_000,
|
|
Threshold: 0.35,
|
|
Ratio: 1.5,
|
|
MaxW: 10_000,
|
|
},
|
|
},
|
|
}
|
|
|
|
func NewHybridCounter(params CounterParams, src rand.Source) *hybridCounter {
|
|
return &hybridCounter{
|
|
mapCounter: *NewMapCounter(params.MapLimit),
|
|
params: params,
|
|
src: src,
|
|
}
|
|
}
|
|
|
|
func (h *hybridCounter) GetPass(key string, base int64, inc int64) int64 {
|
|
if h.cmSketch != nil {
|
|
p := h.cmSketch.GetPass(key, base, inc)
|
|
// after migration, continue updating top-K tracker
|
|
_ = h.mapCounter.updateHeap(key, p)
|
|
return p
|
|
}
|
|
|
|
p, overflow := h.mapCounter.getPassWithOverflow(key, base, inc)
|
|
if overflow {
|
|
h.migrateToCMS()
|
|
}
|
|
return p
|
|
}
|
|
|
|
func (h *hybridCounter) migrateToCMS() {
|
|
h.cmSketch = NewCMSketchCounter(h.params.CMS, h.src, h.mapCounter.TopK)
|
|
// move existing counts into CMS
|
|
for _, entry := range h.mapCounter.heap {
|
|
_ = h.cmSketch.GetPass(entry.Key, entry.Count, 0)
|
|
}
|
|
}
|
|
|
|
func (h *hybridCounter) EstimateDistinctKeys() int {
|
|
if h.cmSketch != nil {
|
|
return h.cmSketch.EstimateDistinctKeys()
|
|
}
|
|
return h.mapCounter.EstimateDistinctKeys()
|
|
}
|
|
|
|
// TopK returns the current top-K entries being tracked.
|
|
func (h *hybridCounter) TopK() []TopKEntry {
|
|
return h.mapCounter.TopK()
|
|
}
|