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.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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 }
|
||||
Reference in New Issue
Block a user