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 }