Files
loki/pkg/dataobj/sections/pointers/iter.go
Robert Fratto f6091a67d1 chore(engine): move to toggleable section prefetching (#19142)
dataset.readerDownloader was originally introduced in #16429, an attempt to
balance peak memory usage of reading a section with read times by downloading a
configurable size of pages in advance.

In practice, each roundtrip to object storage adds too much of a latency hit,
and we've started to set the cache limit high enough to ensure that each reader
only needs a single prefetch. Given what we've found, it no longer makes sense
to control peak memory usage via the prefetch size. Other options, such as
downloading directly to disk, may be explored in the future.

In the meantime, this PR removes the ability to specify a cache size. All
non-pruned pages will be bulk requested using the range reader (#19067) on the
first read call. Pages which have left the potential read window will continue
to be eagerly removed for garbage collection.

However, we don't want to prefetch when the dataset is entirely in memory,
which is the case when the logs section builder is performing k-way merge over
in-memory sections. To lower the memory usage of builders, prefetching is
configurable. For this initial PR, prefetching is only disabled for the logs
section builder; all other reads force prefetching.

Signed-off-by: Robert Fratto <robertfratto@gmail.com>
2025-09-09 11:54:34 -04:00

197 lines
6.1 KiB
Go

package pointers
import (
"context"
"errors"
"fmt"
"io"
"time"
"unsafe"
"github.com/grafana/loki/v3/pkg/dataobj"
"github.com/grafana/loki/v3/pkg/dataobj/internal/dataset"
"github.com/grafana/loki/v3/pkg/dataobj/internal/metadata/datasetmd"
"github.com/grafana/loki/v3/pkg/dataobj/internal/result"
"github.com/grafana/loki/v3/pkg/dataobj/internal/util/slicegrow"
"github.com/grafana/loki/v3/pkg/dataobj/internal/util/symbolizer"
"github.com/grafana/loki/v3/pkg/dataobj/sections/internal/columnar"
)
// Iter iterates over pointers in the provided decoder. All pointers sections are
// iterated over in order.
func Iter(ctx context.Context, obj *dataobj.Object) result.Seq[SectionPointer] {
return result.Iter(func(yield func(SectionPointer) bool) error {
for i, section := range obj.Sections().Filter(CheckSection) {
pointersSection, err := Open(ctx, section)
if err != nil {
return fmt.Errorf("opening section %d: %w", i, err)
}
for result := range IterSection(ctx, pointersSection) {
if result.Err() != nil || !yield(result.MustValue()) {
return result.Err()
}
}
}
return nil
})
}
func IterSection(ctx context.Context, section *Section) result.Seq[SectionPointer] {
return result.Iter(func(yield func(SectionPointer) bool) error {
columnarSection := section.inner
dset, err := columnar.MakeDataset(columnarSection, columnarSection.Columns())
if err != nil {
return fmt.Errorf("creating columns dataset: %w", err)
}
columns, err := result.Collect(dset.ListColumns(ctx))
if err != nil {
return err
}
r := dataset.NewReader(dataset.ReaderOptions{
Dataset: dset,
Columns: columns,
Prefetch: true,
})
defer r.Close()
sym := symbolizer.New(128, 1024)
var rows [1]dataset.Row
for {
n, err := r.Read(ctx, rows[:])
if err != nil && !errors.Is(err, io.EOF) {
return err
} else if n == 0 && errors.Is(err, io.EOF) {
return nil
}
var pointer SectionPointer
for _, row := range rows[:n] {
if err := decodeRow(section.Columns(), row, &pointer, sym); err != nil {
return err
}
if !yield(pointer) {
return nil
}
}
}
})
}
// decodeRow decodes a stream from a [dataset.Row], using the provided columns to
// determine the column type. The list of columns must match the columns used
// to create the row.
//
// The sym argument is used for reusing label values between calls to
// decodeRow. If sym is nil, label value strings are always allocated.
func decodeRow(columns []*Column, row dataset.Row, pointer *SectionPointer, sym *symbolizer.Symbolizer) error {
pointer.Reset()
for columnIndex, columnValue := range row.Values {
if columnValue.IsNil() || columnValue.IsZero() {
continue
}
column := columns[columnIndex]
switch column.Type {
case ColumnTypePath:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_BINARY {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.Path = sym.Get(unsafeString(columnValue.Binary()))
case ColumnTypeSection:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_INT64 {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.Section = columnValue.Int64()
case ColumnTypePointerKind:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_INT64 {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
switch columnValue.Int64() {
case int64(PointerKindStreamIndex):
pointer.PointerKind = PointerKindStreamIndex
case int64(PointerKindColumnIndex):
pointer.PointerKind = PointerKindColumnIndex
default:
return fmt.Errorf("invalid pointer kind %d", columnValue.Int64())
}
case ColumnTypeStreamID:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_INT64 {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.StreamID = columnValue.Int64()
case ColumnTypeStreamIDRef:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_INT64 {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.StreamIDRef = columnValue.Int64()
case ColumnTypeMinTimestamp:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_INT64 {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.StartTs = time.Unix(0, columnValue.Int64())
case ColumnTypeMaxTimestamp:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_INT64 {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.EndTs = time.Unix(0, columnValue.Int64())
case ColumnTypeRowCount:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_INT64 {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.LineCount = columnValue.Int64()
case ColumnTypeUncompressedSize:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_INT64 {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.UncompressedSize = columnValue.Int64()
case ColumnTypeColumnName:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_BINARY {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.ColumnName = sym.Get(unsafeString(columnValue.Binary()))
case ColumnTypeColumnIndex:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_INT64 {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
pointer.ColumnIndex = columnValue.Int64()
case ColumnTypeValuesBloomFilter:
if ty := columnValue.Type(); ty != datasetmd.PHYSICAL_TYPE_BINARY {
return fmt.Errorf("invalid type %s for %s", ty, column.Type)
}
filterBytes := columnValue.Binary()
pointer.ValuesBloomFilter = slicegrow.GrowToCap(pointer.ValuesBloomFilter, len(filterBytes))
pointer.ValuesBloomFilter = pointer.ValuesBloomFilter[:len(filterBytes)]
copy(pointer.ValuesBloomFilter, filterBytes)
default:
// TODO(rfratto): We probably don't want to return an error on unexpected
// columns because it breaks forward compatibility. Should we log
// something here?
}
}
return nil
}
func unsafeString(data []byte) string {
return unsafe.String(unsafe.SliceData(data), len(data))
}