Files
ingot/internal/head/series.go
T
david 376d3faf25 In-memory head with WAL
Head connects chunkenc and WAL: label-to-ref resolution with striped
concurrent maps, active Gorilla chunk per series sealing at 120 samples,
and an Appender that buffers samples, writes WAL on commit, then applies
to head. OOO rejection checks both committed and batch state. WAL replay
on Open rebuilds the full in-memory state. Exported
ChunkAppender/ChunkIterator interfaces from chunkenc.
2026-07-04 14:36:24 -04:00

127 lines
2.8 KiB
Go

package head
import (
"sync"
"git.dvdt.dev/david/ingot/internal/chunkenc"
"git.dvdt.dev/david/ingot/labels"
)
const samplesPerChunk = 120
type chunkMeta struct {
chunk *chunkenc.XORChunk
minT int64
maxT int64
}
type memSeries struct {
mu sync.Mutex
ref uint64
labels []labels.Label
// Active chunk and its appender.
chunk *chunkenc.XORChunk
chunkApp chunkenc.ChunkAppender
chunkMinT int64
// Sealed chunks awaiting block flush.
sealed []chunkMeta
// Timestamp of last appended sample (for OOO rejection).
lastT int64
hasData bool // false until the first sample is appended
}
// append adds a sample to the series. Caller must hold s.mu.
func (s *memSeries) append(t int64, v float64) {
if s.chunk == nil || s.chunk.NumSamples() >= samplesPerChunk {
s.cutNewChunk(t)
}
if !s.hasData {
s.chunkMinT = t
s.hasData = true
}
s.chunkApp.Append(t, v)
s.lastT = t
}
// cutNewChunk seals the current chunk (if any) and starts a fresh one.
func (s *memSeries) cutNewChunk(t int64) {
if s.chunk != nil {
s.sealed = append(s.sealed, chunkMeta{
chunk: s.chunk,
minT: s.chunkMinT,
maxT: s.lastT,
})
}
s.chunk = chunkenc.NewXORChunk()
s.chunkApp, _ = s.chunk.Appender() // always succeeds on a fresh chunk
s.chunkMinT = t
}
// iterator returns an iterator over all samples in [mint, maxt].
// Caller must hold s.mu.
func (s *memSeries) iterator(mint, maxt int64) chunkenc.ChunkIterator {
var iters []chunkenc.ChunkIterator
for _, cm := range s.sealed {
if cm.maxT < mint || cm.minT > maxt {
continue
}
iters = append(iters, cm.chunk.Iterator())
}
if s.chunk != nil && s.chunk.NumSamples() > 0 && s.chunkMinT <= maxt && s.lastT >= mint {
// Snapshot the active chunk bytes for safe concurrent iteration.
snap := &chunkenc.XORChunk{}
*snap = *s.chunk
iters = append(iters, snap.Iterator())
}
if len(iters) == 0 {
return &emptyIterator{}
}
if len(iters) == 1 {
return iters[0]
}
return &multiIterator{iters: iters}
}
// multiIterator chains multiple ChunkIterators in order.
type multiIterator struct {
iters []chunkenc.ChunkIterator
cur int
}
func (m *multiIterator) Next() bool {
for m.cur < len(m.iters) {
if m.iters[m.cur].Next() {
return true
}
if m.iters[m.cur].Err() != nil {
return false
}
m.cur++
}
return false
}
func (m *multiIterator) At() (int64, float64) {
return m.iters[m.cur].At()
}
func (m *multiIterator) Err() error {
if m.cur < len(m.iters) {
return m.iters[m.cur].Err()
}
return nil
}
// emptyIterator is returned when a series has no data in the requested range.
type emptyIterator struct{}
func (e *emptyIterator) Next() bool { return false }
func (e *emptyIterator) At() (int64, float64) { return 0, 0 }
func (e *emptyIterator) Err() error { return nil }