173 lines
4.4 KiB
Go
173 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
st "git.ipng.ch/ipng/nginx-logtail/internal/store"
|
|
pb "git.ipng.ch/ipng/nginx-logtail/proto/logtailpb"
|
|
)
|
|
|
|
// Cache holds the tiered ring buffers for the aggregator and answers TopN,
|
|
// Trend, and StreamSnapshots queries from them. It is populated by a
|
|
// 1-minute ticker that snapshots the current merged view from the Merger.
|
|
//
|
|
// Tick-based (not snapshot-triggered) so the ring stays on the same 1-minute
|
|
// cadence as the collectors, regardless of how many collectors are connected.
|
|
type Cache struct {
|
|
source string
|
|
merger *Merger
|
|
|
|
mu sync.RWMutex
|
|
fineRing [st.FineRingSize]st.Snapshot
|
|
fineHead int
|
|
fineFilled int
|
|
coarseRing [st.CoarseRingSize]st.Snapshot
|
|
coarseHead int
|
|
coarseFilled int
|
|
fineTick int
|
|
|
|
subMu sync.Mutex
|
|
subs map[chan st.Snapshot]struct{}
|
|
}
|
|
|
|
func NewCache(merger *Merger, source string) *Cache {
|
|
return &Cache{
|
|
merger: merger,
|
|
source: source,
|
|
subs: make(map[chan st.Snapshot]struct{}),
|
|
}
|
|
}
|
|
|
|
// Run drives the 1-minute rotation ticker. Blocks until ctx is cancelled.
|
|
func (c *Cache) Run(ctx context.Context) {
|
|
ticker := time.NewTicker(time.Minute)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case t := <-ticker.C:
|
|
c.rotate(t)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) rotate(now time.Time) {
|
|
fine := st.Snapshot{Timestamp: now, Entries: c.merger.TopK(st.FineTopK)}
|
|
|
|
c.mu.Lock()
|
|
c.fineRing[c.fineHead] = fine
|
|
c.fineHead = (c.fineHead + 1) % st.FineRingSize
|
|
if c.fineFilled < st.FineRingSize {
|
|
c.fineFilled++
|
|
}
|
|
c.fineTick++
|
|
if c.fineTick >= st.CoarseEvery {
|
|
c.fineTick = 0
|
|
coarse := c.mergeFineBuckets(now)
|
|
c.coarseRing[c.coarseHead] = coarse
|
|
c.coarseHead = (c.coarseHead + 1) % st.CoarseRingSize
|
|
if c.coarseFilled < st.CoarseRingSize {
|
|
c.coarseFilled++
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
c.broadcast(fine)
|
|
}
|
|
|
|
func (c *Cache) mergeFineBuckets(now time.Time) st.Snapshot {
|
|
merged := make(map[string]int64)
|
|
count := min(st.CoarseEvery, c.fineFilled)
|
|
for i := 0; i < count; i++ {
|
|
idx := (c.fineHead - 1 - i + st.FineRingSize) % st.FineRingSize
|
|
for _, e := range c.fineRing[idx].Entries {
|
|
merged[e.Label] += e.Count
|
|
}
|
|
}
|
|
return st.Snapshot{Timestamp: now, Entries: st.TopKFromMap(merged, st.CoarseTopK)}
|
|
}
|
|
|
|
// QueryTopN answers a TopN request from the ring buffers.
|
|
func (c *Cache) QueryTopN(filter *pb.Filter, groupBy pb.GroupBy, n int, window pb.Window) []st.Entry {
|
|
cf := st.CompileFilter(filter)
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
buckets, count := st.BucketsForWindow(window, c.fineView(), c.coarseView(), c.fineFilled, c.coarseFilled)
|
|
grouped := make(map[string]int64)
|
|
for i := 0; i < count; i++ {
|
|
idx := (buckets.Head - 1 - i + buckets.Size) % buckets.Size
|
|
for _, e := range buckets.Ring[idx].Entries {
|
|
t := st.LabelTuple(e.Label)
|
|
if !st.MatchesFilter(t, cf) {
|
|
continue
|
|
}
|
|
grouped[st.DimensionLabel(t, groupBy)] += e.Count
|
|
}
|
|
}
|
|
return st.TopKFromMap(grouped, n)
|
|
}
|
|
|
|
// QueryTrend answers a Trend request from the ring buffers.
|
|
func (c *Cache) QueryTrend(filter *pb.Filter, window pb.Window) []st.TrendPoint {
|
|
cf := st.CompileFilter(filter)
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
buckets, count := st.BucketsForWindow(window, c.fineView(), c.coarseView(), c.fineFilled, c.coarseFilled)
|
|
points := make([]st.TrendPoint, count)
|
|
for i := 0; i < count; i++ {
|
|
idx := (buckets.Head - count + i + buckets.Size) % buckets.Size
|
|
snap := buckets.Ring[idx]
|
|
var total int64
|
|
for _, e := range snap.Entries {
|
|
if st.MatchesFilter(st.LabelTuple(e.Label), cf) {
|
|
total += e.Count
|
|
}
|
|
}
|
|
points[i] = st.TrendPoint{Timestamp: snap.Timestamp, Count: total}
|
|
}
|
|
return points
|
|
}
|
|
|
|
func (c *Cache) fineView() st.RingView {
|
|
ring := make([]st.Snapshot, st.FineRingSize)
|
|
copy(ring, c.fineRing[:])
|
|
return st.RingView{Ring: ring, Head: c.fineHead, Size: st.FineRingSize}
|
|
}
|
|
|
|
func (c *Cache) coarseView() st.RingView {
|
|
ring := make([]st.Snapshot, st.CoarseRingSize)
|
|
copy(ring, c.coarseRing[:])
|
|
return st.RingView{Ring: ring, Head: c.coarseHead, Size: st.CoarseRingSize}
|
|
}
|
|
|
|
func (c *Cache) Subscribe() chan st.Snapshot {
|
|
ch := make(chan st.Snapshot, 4)
|
|
c.subMu.Lock()
|
|
c.subs[ch] = struct{}{}
|
|
c.subMu.Unlock()
|
|
return ch
|
|
}
|
|
|
|
func (c *Cache) Unsubscribe(ch chan st.Snapshot) {
|
|
c.subMu.Lock()
|
|
delete(c.subs, ch)
|
|
c.subMu.Unlock()
|
|
close(ch)
|
|
}
|
|
|
|
func (c *Cache) broadcast(snap st.Snapshot) {
|
|
c.subMu.Lock()
|
|
defer c.subMu.Unlock()
|
|
for ch := range c.subs {
|
|
select {
|
|
case ch <- snap:
|
|
default:
|
|
}
|
|
}
|
|
}
|