Block refcounting (atomic refs + condemned flag) lets live queries survive concurrent compaction and retention. Levelled compactor merges 2h->8h->32h blocks via raw chunk passthrough. Retention drops blocks older than the configured window. Background goroutine drives flush/compact/retain cycles; exported RunCompaction/ApplyRetention allow deterministic test control via injectable clock. Soak test: 10k series x 48h simulated at 15s intervals (115M samples). Validates flat memory (158 MiB peak), bounded disk (13 blocks), and zero query errors during compaction. All existing tests refactored to table-driven with uniform assertions.
187 lines
4.4 KiB
Go
187 lines
4.4 KiB
Go
package block
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sync/atomic"
|
|
|
|
"git.dvdt.dev/david/ingot/internal/chunkenc"
|
|
"git.dvdt.dev/david/ingot/internal/index"
|
|
"git.dvdt.dev/david/ingot/labels"
|
|
)
|
|
|
|
// Reader provides read access to an immutable on-disk block.
|
|
type Reader struct {
|
|
dir string
|
|
Meta BlockMeta
|
|
idx *index.Reader
|
|
chunks *chunkReader
|
|
refs atomic.Int32
|
|
condemned atomic.Bool
|
|
}
|
|
|
|
// Open opens a block directory for reading. Chunk files are mmap'd.
|
|
func Open(dir string) (*Reader, error) {
|
|
meta, err := readMeta(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read the index file into memory.
|
|
indexData, err := os.ReadFile(filepath.Join(dir, "index"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
idx, err := index.NewReader(indexData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cr, err := newChunkReader(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := &Reader{
|
|
dir: dir,
|
|
Meta: meta,
|
|
idx: idx,
|
|
chunks: cr,
|
|
}
|
|
r.refs.Store(1) // DB's ownership ref
|
|
return r, nil
|
|
}
|
|
|
|
// Series returns all series entries from the index.
|
|
func (r *Reader) Series() []index.SeriesEntry {
|
|
return r.idx.Series()
|
|
}
|
|
|
|
// SeriesByRef looks up a series by ref.
|
|
func (r *Reader) SeriesByRef(ref uint64) (index.SeriesEntry, bool) {
|
|
return r.idx.SeriesByRef(ref)
|
|
}
|
|
|
|
// Postings returns sorted series refs matching label name=value.
|
|
func (r *Reader) Postings(name, value string) []uint64 {
|
|
return r.idx.Postings(name, value)
|
|
}
|
|
|
|
// ChunkIterator returns an iterator for a chunk at the given ref.
|
|
func (r *Reader) ChunkIterator(ref index.ChunkRef) (chunkenc.ChunkIterator, error) {
|
|
return r.chunks.chunkIterator(ref)
|
|
}
|
|
|
|
// SeriesChunkIterator returns an iterator over all chunks for a series in a
|
|
// time range. Chunks are iterated in order.
|
|
func (r *Reader) SeriesChunkIterator(ref uint64, mint, maxt int64) (chunkenc.ChunkIterator, error) {
|
|
entry, ok := r.idx.SeriesByRef(ref)
|
|
if !ok {
|
|
return &emptyIterator{}, nil
|
|
}
|
|
|
|
var iters []chunkenc.ChunkIterator
|
|
for _, cm := range entry.Chunks {
|
|
if cm.MaxT < mint || cm.MinT > maxt {
|
|
continue
|
|
}
|
|
it, err := r.chunks.chunkIterator(cm.Ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
iters = append(iters, it)
|
|
}
|
|
|
|
if len(iters) == 0 {
|
|
return &emptyIterator{}, nil
|
|
}
|
|
if len(iters) == 1 {
|
|
return iters[0], nil
|
|
}
|
|
return &multiIterator{iters: iters}, nil
|
|
}
|
|
|
|
// Labels returns the labels for a series by ref.
|
|
func (r *Reader) Labels(ref uint64) ([]labels.Label, bool) {
|
|
entry, ok := r.idx.SeriesByRef(ref)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return entry.Labels, true
|
|
}
|
|
|
|
// LabelValues returns sorted unique values for the given label name.
|
|
func (r *Reader) LabelValues(name string) []string {
|
|
return r.idx.LabelValues(name)
|
|
}
|
|
|
|
// AllPostings returns sorted refs for all series in the block.
|
|
func (r *Reader) AllPostings() []uint64 {
|
|
return r.idx.AllPostings()
|
|
}
|
|
|
|
// Dir returns the block directory path.
|
|
func (r *Reader) Dir() string { return r.dir }
|
|
|
|
// Ref increments the refcount. Called by Querier on snapshot.
|
|
func (r *Reader) Ref() { r.refs.Add(1) }
|
|
|
|
// Release decrements the refcount. Returns true if the refcount hit zero
|
|
// and the block is condemned (caller should delete the directory).
|
|
func (r *Reader) Release() bool {
|
|
if r.refs.Add(-1) == 0 {
|
|
r.Close()
|
|
return r.condemned.Load()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Condemn marks the block for directory deletion when refcount reaches zero.
|
|
func (r *Reader) Condemn() { r.condemned.Store(true) }
|
|
|
|
// RawChunkData returns the raw chunk bytes at the given ref (for compaction).
|
|
func (r *Reader) RawChunkData(ref index.ChunkRef) ([]byte, error) {
|
|
return r.chunks.chunkData(ref)
|
|
}
|
|
|
|
// Close releases all resources (munmaps chunk files).
|
|
func (r *Reader) Close() error {
|
|
return r.chunks.close()
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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 }
|