github.com/thanos-io/thanos@v0.32.5/pkg/block/indexheader/lazy_binary_reader.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package indexheader
     5  
     6  import (
     7  	"context"
     8  	"os"
     9  	"path/filepath"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/go-kit/log"
    14  	"github.com/go-kit/log/level"
    15  	"github.com/oklog/ulid"
    16  	"github.com/pkg/errors"
    17  	"github.com/prometheus/client_golang/prometheus"
    18  	"github.com/prometheus/client_golang/prometheus/promauto"
    19  	"github.com/prometheus/prometheus/tsdb/index"
    20  	"github.com/thanos-io/objstore"
    21  	"go.uber.org/atomic"
    22  
    23  	"github.com/thanos-io/thanos/pkg/block"
    24  )
    25  
    26  var (
    27  	errNotIdle              = errors.New("the reader is not idle")
    28  	errUnloadedWhileLoading = errors.New("the index-header has been concurrently unloaded")
    29  )
    30  
    31  // LazyBinaryReaderMetrics holds metrics tracked by LazyBinaryReader.
    32  type LazyBinaryReaderMetrics struct {
    33  	loadCount         prometheus.Counter
    34  	loadFailedCount   prometheus.Counter
    35  	unloadCount       prometheus.Counter
    36  	unloadFailedCount prometheus.Counter
    37  	loadDuration      prometheus.Histogram
    38  }
    39  
    40  // NewLazyBinaryReaderMetrics makes new LazyBinaryReaderMetrics.
    41  func NewLazyBinaryReaderMetrics(reg prometheus.Registerer) *LazyBinaryReaderMetrics {
    42  	return &LazyBinaryReaderMetrics{
    43  		loadCount: promauto.With(reg).NewCounter(prometheus.CounterOpts{
    44  			Name: "indexheader_lazy_load_total",
    45  			Help: "Total number of index-header lazy load operations.",
    46  		}),
    47  		loadFailedCount: promauto.With(reg).NewCounter(prometheus.CounterOpts{
    48  			Name: "indexheader_lazy_load_failed_total",
    49  			Help: "Total number of failed index-header lazy load operations.",
    50  		}),
    51  		unloadCount: promauto.With(reg).NewCounter(prometheus.CounterOpts{
    52  			Name: "indexheader_lazy_unload_total",
    53  			Help: "Total number of index-header lazy unload operations.",
    54  		}),
    55  		unloadFailedCount: promauto.With(reg).NewCounter(prometheus.CounterOpts{
    56  			Name: "indexheader_lazy_unload_failed_total",
    57  			Help: "Total number of failed index-header lazy unload operations.",
    58  		}),
    59  		loadDuration: promauto.With(reg).NewHistogram(prometheus.HistogramOpts{
    60  			Name:    "indexheader_lazy_load_duration_seconds",
    61  			Help:    "Duration of the index-header lazy loading in seconds.",
    62  			Buckets: []float64{0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 15, 30, 60, 120, 300},
    63  		}),
    64  	}
    65  }
    66  
    67  // LazyBinaryReader wraps BinaryReader and loads (mmap) the index-header only upon
    68  // the first Reader function is called.
    69  type LazyBinaryReader struct {
    70  	ctx                         context.Context
    71  	logger                      log.Logger
    72  	bkt                         objstore.BucketReader
    73  	dir                         string
    74  	id                          ulid.ULID
    75  	postingOffsetsInMemSampling int
    76  	metrics                     *LazyBinaryReaderMetrics
    77  	onClosed                    func(*LazyBinaryReader)
    78  
    79  	readerMx  sync.RWMutex
    80  	reader    *BinaryReader
    81  	readerErr error
    82  
    83  	// Keep track of the last time it was used.
    84  	usedAt *atomic.Int64
    85  }
    86  
    87  // NewLazyBinaryReader makes a new LazyBinaryReader. If the index-header does not exist
    88  // on the local disk at dir location, this function will build it downloading required
    89  // sections from the full index stored in the bucket. However, this function doesn't load
    90  // (mmap) the index-header; it will be loaded at first Reader function call.
    91  func NewLazyBinaryReader(
    92  	ctx context.Context,
    93  	logger log.Logger,
    94  	bkt objstore.BucketReader,
    95  	dir string,
    96  	id ulid.ULID,
    97  	postingOffsetsInMemSampling int,
    98  	metrics *LazyBinaryReaderMetrics,
    99  	onClosed func(*LazyBinaryReader),
   100  ) (*LazyBinaryReader, error) {
   101  	if dir != "" {
   102  		indexHeaderFile := filepath.Join(dir, id.String(), block.IndexHeaderFilename)
   103  		// If the index-header doesn't exist we should download it.
   104  		if _, err := os.Stat(indexHeaderFile); err != nil {
   105  			if !os.IsNotExist(err) {
   106  				return nil, errors.Wrap(err, "read index header")
   107  			}
   108  
   109  			level.Debug(logger).Log("msg", "the index-header doesn't exist on disk; recreating", "path", indexHeaderFile)
   110  
   111  			start := time.Now()
   112  			if _, err := WriteBinary(ctx, bkt, id, indexHeaderFile); err != nil {
   113  				return nil, errors.Wrap(err, "write index header")
   114  			}
   115  
   116  			level.Debug(logger).Log("msg", "built index-header file", "path", indexHeaderFile, "elapsed", time.Since(start))
   117  		}
   118  	}
   119  
   120  	return &LazyBinaryReader{
   121  		ctx:                         ctx,
   122  		logger:                      logger,
   123  		bkt:                         bkt,
   124  		dir:                         dir,
   125  		id:                          id,
   126  		postingOffsetsInMemSampling: postingOffsetsInMemSampling,
   127  		metrics:                     metrics,
   128  		usedAt:                      atomic.NewInt64(time.Now().UnixNano()),
   129  		onClosed:                    onClosed,
   130  	}, nil
   131  }
   132  
   133  // Close implements Reader. It unloads the index-header from memory (releasing the mmap
   134  // area), but a subsequent call to any other Reader function will automatically reload it.
   135  func (r *LazyBinaryReader) Close() error {
   136  	if r.onClosed != nil {
   137  		defer r.onClosed(r)
   138  	}
   139  
   140  	// Unload without checking if idle.
   141  	return r.unloadIfIdleSince(0)
   142  }
   143  
   144  // IndexVersion implements Reader.
   145  func (r *LazyBinaryReader) IndexVersion() (int, error) {
   146  	r.readerMx.RLock()
   147  	defer r.readerMx.RUnlock()
   148  
   149  	if err := r.load(); err != nil {
   150  		return 0, err
   151  	}
   152  
   153  	r.usedAt.Store(time.Now().UnixNano())
   154  	return r.reader.IndexVersion()
   155  }
   156  
   157  // PostingsOffset implements Reader.
   158  func (r *LazyBinaryReader) PostingsOffset(name, value string) (index.Range, error) {
   159  	r.readerMx.RLock()
   160  	defer r.readerMx.RUnlock()
   161  
   162  	if err := r.load(); err != nil {
   163  		return index.Range{}, err
   164  	}
   165  
   166  	r.usedAt.Store(time.Now().UnixNano())
   167  	return r.reader.PostingsOffset(name, value)
   168  }
   169  
   170  // LookupSymbol implements Reader.
   171  func (r *LazyBinaryReader) LookupSymbol(o uint32) (string, error) {
   172  	r.readerMx.RLock()
   173  	defer r.readerMx.RUnlock()
   174  
   175  	if err := r.load(); err != nil {
   176  		return "", err
   177  	}
   178  
   179  	r.usedAt.Store(time.Now().UnixNano())
   180  	return r.reader.LookupSymbol(o)
   181  }
   182  
   183  // LabelValues implements Reader.
   184  func (r *LazyBinaryReader) LabelValues(name string) ([]string, error) {
   185  	r.readerMx.RLock()
   186  	defer r.readerMx.RUnlock()
   187  
   188  	if err := r.load(); err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	r.usedAt.Store(time.Now().UnixNano())
   193  	return r.reader.LabelValues(name)
   194  }
   195  
   196  // LabelNames implements Reader.
   197  func (r *LazyBinaryReader) LabelNames() ([]string, error) {
   198  	r.readerMx.RLock()
   199  	defer r.readerMx.RUnlock()
   200  
   201  	if err := r.load(); err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	r.usedAt.Store(time.Now().UnixNano())
   206  	return r.reader.LabelNames()
   207  }
   208  
   209  // load ensures the underlying binary index-header reader has been successfully loaded. Returns
   210  // an error on failure. This function MUST be called with the read lock already acquired.
   211  func (r *LazyBinaryReader) load() (returnErr error) {
   212  	// Nothing to do if we already tried loading it.
   213  	if r.reader != nil {
   214  		return nil
   215  	}
   216  	if r.readerErr != nil {
   217  		return r.readerErr
   218  	}
   219  
   220  	// Take the write lock to ensure we'll try to load it only once. Take again
   221  	// the read lock once done.
   222  	r.readerMx.RUnlock()
   223  	r.readerMx.Lock()
   224  	defer func() {
   225  		r.readerMx.Unlock()
   226  		r.readerMx.RLock()
   227  
   228  		// Between the write unlock and the subsequent read lock, the unload() may have run,
   229  		// so we make sure to catch this edge case.
   230  		if returnErr == nil && r.reader == nil {
   231  			returnErr = errUnloadedWhileLoading
   232  		}
   233  	}()
   234  
   235  	// Ensure none else tried to load it in the meanwhile.
   236  	if r.reader != nil {
   237  		return nil
   238  	}
   239  	if r.readerErr != nil {
   240  		return r.readerErr
   241  	}
   242  
   243  	level.Debug(r.logger).Log("msg", "lazy loading index-header", "block", r.id)
   244  	r.metrics.loadCount.Inc()
   245  	startTime := time.Now()
   246  
   247  	reader, err := NewBinaryReader(r.ctx, r.logger, r.bkt, r.dir, r.id, r.postingOffsetsInMemSampling)
   248  	if err != nil {
   249  		r.metrics.loadFailedCount.Inc()
   250  		r.readerErr = err
   251  		return errors.Wrapf(err, "lazy load index-header for block %s", r.id)
   252  	}
   253  
   254  	r.reader = reader
   255  	level.Debug(r.logger).Log("msg", "lazy loaded index-header", "block", r.id, "elapsed", time.Since(startTime))
   256  	r.metrics.loadDuration.Observe(time.Since(startTime).Seconds())
   257  
   258  	return nil
   259  }
   260  
   261  // unloadIfIdleSince closes underlying BinaryReader if the reader is idle since given time (as unix nano). If idleSince is 0,
   262  // the check on the last usage is skipped. Calling this function on a already unloaded reader is a no-op.
   263  func (r *LazyBinaryReader) unloadIfIdleSince(ts int64) error {
   264  	r.readerMx.Lock()
   265  	defer r.readerMx.Unlock()
   266  
   267  	// Nothing to do if already unloaded.
   268  	if r.reader == nil {
   269  		return nil
   270  	}
   271  
   272  	// Do not unloadIfIdleSince if not idle.
   273  	if ts > 0 && r.usedAt.Load() > ts {
   274  		return errNotIdle
   275  	}
   276  
   277  	r.metrics.unloadCount.Inc()
   278  	if err := r.reader.Close(); err != nil {
   279  		r.metrics.unloadFailedCount.Inc()
   280  		return err
   281  	}
   282  
   283  	r.reader = nil
   284  	return nil
   285  }
   286  
   287  // isIdleSince returns true if the reader is idle since given time (as unix nano).
   288  func (r *LazyBinaryReader) isIdleSince(ts int64) bool {
   289  	if r.usedAt.Load() > ts {
   290  		return false
   291  	}
   292  
   293  	// A reader can be considered idle only if it's loaded.
   294  	r.readerMx.RLock()
   295  	loaded := r.reader != nil
   296  	r.readerMx.RUnlock()
   297  
   298  	return loaded
   299  }