Files
upstream-bleve/index/scorch/persister.go
upstream-mirror f8d3d9f0cf
Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled
snapshot of https://github.com/blevesearch/bleve.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 22:27:16 +05:00

1682 lines
50 KiB
Go

// Copyright (c) 2017 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scorch
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"log"
"math"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/RoaringBitmap/roaring/v2"
"github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
segment "github.com/blevesearch/scorch_segment_api/v2"
bolt "go.etcd.io/bbolt"
)
const persister = "persister"
var (
// DefaultPersisterNapTimeMSec is kept to zero as this helps in direct
// persistence of segments with the default safe batch option.
// If the default safe batch option results in high number of
// files on disk, then users may initialise this configuration parameter
// with higher values so that the persister will nap a bit within it's
// work loop to favour better in-memory merging of segments to result
// in fewer segment files on disk. But that may come with an indexing
// performance overhead.
// Unsafe batch users are advised to override this to higher value
// for better performance especially with high data density.
DefaultPersisterNapTimeMSec int = 0 // ms
// DefaultPersisterNapUnderNumFiles helps in controlling the pace of
// persister. At times of a slow merger progress with heavy file merging
// operations, its better to pace down the persister for letting the merger
// to catch up within a range defined by this parameter.
// Fewer files on disk (as per the merge plan) would result in keeping the
// file handle usage under limit, faster disk merger and a healthier index.
// Its been observed that such a loosely sync'ed introducer-persister-merger
// trio results in better overall performance.
DefaultPersisterNapUnderNumFiles int = 1000
// MemoryPressurePauseThreshold lets persister to have a better leeway
// for prudently performing the memory merge of segments on a memory
// pressure situation. Here the config value is an upper threshold
// for the number of paused application threads. The default value would
// be a very high number to always favour the merging of memory segments.
DefaultMemoryPressurePauseThreshold uint64 = math.MaxUint64
// DefaultMinSegmentsForInMemoryMerge represents the default number of
// in-memory zap segments that persistSnapshotMaybeMerge() needs to
// see in an IndexSnapshot before it decides to merge and persist
// those segments
DefaultMinSegmentsForInMemoryMerge int = 2
// number workers which parallely perform an in-memory merge of the segments
// followed by a flush operation.
DefaultNumPersisterWorkers int = 1
// maximum size of data that a single worker is allowed to perform the in-memory
// merge operation.
DefaultMaxSizeInMemoryMergePerWorker int = 0
// NumSnapshotsToKeep represents how many recent, old snapshots to
// keep around per Scorch instance. Useful for apps that require
// rollback'ability.
NumSnapshotsToKeep int = 1
// RollbackSamplingInterval controls how far back we are looking
// in the history to get the rollback points.
// If we have to keep N snapshots ( = NumSnapshotsToKeep ), then upon
// including the very latest snapshot there should be N snapshots.
// If the timestamp of the latest snapshot is T, and the
// RollbackSamplingInterval is D, then the very last one would be T - (N - 1) * D
// A value of 0 means we will not sample, and will keep the very latest N snapshots.
// Example:
// - For N = 3, and the below timing diagram:
// a b c d
// |-----|-----|-----| --> time
// | D | D | D |
// X Y Z T
// - The rollback points would be b, c, and d. a would be too old.
RollbackSamplingInterval time.Duration = 0
// Controls what portion of the earlier rollback points to retain during
// a infrequent/sparse mutation scenario
RollbackRetentionFactor float64 = 0.5
)
type persisterOptions struct {
// PersisterNapTimeMSec controls the wait/delay injected into
// persistence workloop to improve the chances for
// a healthier and heavier in-memory merging
PersisterNapTimeMSec int
// PersisterNapTimeMSec > 0, and the number of files is less than
// PersisterNapUnderNumFiles, then the persister will sleep
// PersisterNapTimeMSec amount of time to improve the chances for
// a healthier and heavier in-memory merging
PersisterNapUnderNumFiles int
// MemoryPressurePauseThreshold lets persister to have a better leeway
// for prudently performing the memory merge of segments on a memory
// pressure situation. Here the config value is an upper threshold
// for the number of paused application threads.
MemoryPressurePauseThreshold uint64
// NumPersisterWorkers decides the number of parallel workers that will
// perform the in-memory merge of segments followed by a flush operation.
NumPersisterWorkers int
// MaxSizeInMemoryMerge is the maximum size of data that a single persister
// worker is allowed to work on
MaxSizeInMemoryMergePerWorker int
}
type notificationChan chan struct{}
func (s *Scorch) persisterLoop() {
defer func() {
if r := recover(); r != nil {
s.fireAsyncError(NewScorchError(
persister,
fmt.Sprintf("panic: %v, path: %s", r, s.path),
ErrAsyncPanic,
))
}
s.asyncTasks.Done()
}()
var persistWatchers []*epochWatcher
var lastPersistedEpoch, lastMergedEpoch uint64
var ew *epochWatcher
var unpersistedCallbacks []index.BatchCallback
OUTER:
for {
atomic.AddUint64(&s.stats.TotPersistLoopBeg, 1)
select {
case <-s.closeCh:
break OUTER
case ew = <-s.persisterNotifier:
persistWatchers = append(persistWatchers, ew)
default:
}
if ew != nil && ew.epoch > lastMergedEpoch {
lastMergedEpoch = ew.epoch
}
lastMergedEpoch, persistWatchers = s.pausePersisterForMergerCatchUp(lastPersistedEpoch,
lastMergedEpoch, persistWatchers, s.persisterOptions)
var ourSnapshot *IndexSnapshot
var ourPersisted []chan error
var ourPersistedCallbacks []index.BatchCallback
// check to see if there is a new snapshot to persist
s.rootLock.Lock()
if s.root != nil && s.root.epoch > lastPersistedEpoch {
ourSnapshot = s.root
ourSnapshot.AddRef()
ourPersisted = s.rootPersisted
s.rootPersisted = nil
ourPersistedCallbacks = s.persistedCallbacks
s.persistedCallbacks = nil
atomic.StoreUint64(&s.iStats.persistSnapshotSize, uint64(ourSnapshot.Size()))
atomic.StoreUint64(&s.iStats.persistEpoch, ourSnapshot.epoch)
}
s.rootLock.Unlock()
if ourSnapshot != nil {
startTime := time.Now()
err := s.persistSnapshot(ourSnapshot, s.persisterOptions)
for _, ch := range ourPersisted {
if err != nil {
ch <- err
}
close(ch)
}
if err != nil {
atomic.StoreUint64(&s.iStats.persistEpoch, 0)
if err == segment.ErrClosed {
// index has been closed
_ = ourSnapshot.DecRef()
break OUTER
}
// save this current snapshot's persistedCallbacks, to invoke during
// the retry attempt
unpersistedCallbacks = append(unpersistedCallbacks, ourPersistedCallbacks...)
s.fireAsyncError(NewScorchError(
persister,
fmt.Sprintf("got err persisting snapshot: %v", err),
ErrPersist,
))
_ = ourSnapshot.DecRef()
atomic.AddUint64(&s.stats.TotPersistLoopErr, 1)
continue OUTER
}
if unpersistedCallbacks != nil {
// in the event of this being a retry attempt for persisting a snapshot
// that had earlier failed, prepend the persistedCallbacks associated
// with earlier segment(s) to the latest persistedCallbacks
ourPersistedCallbacks = append(unpersistedCallbacks, ourPersistedCallbacks...)
unpersistedCallbacks = nil
}
for i := range ourPersistedCallbacks {
ourPersistedCallbacks[i](err)
}
atomic.StoreUint64(&s.stats.LastPersistedEpoch, ourSnapshot.epoch)
lastPersistedEpoch = ourSnapshot.epoch
for _, ew := range persistWatchers {
close(ew.notifyCh)
}
persistWatchers = nil
_ = ourSnapshot.DecRef()
changed := false
s.rootLock.RLock()
if s.root != nil && s.root.epoch != lastPersistedEpoch {
changed = true
}
s.rootLock.RUnlock()
s.fireEvent(EventKindPersisterProgress, time.Since(startTime))
if changed {
atomic.AddUint64(&s.stats.TotPersistLoopProgress, 1)
continue OUTER
}
}
// tell the introducer we're waiting for changes
w := &epochWatcher{
epoch: lastPersistedEpoch,
notifyCh: make(notificationChan, 1),
}
select {
case <-s.closeCh:
break OUTER
case s.introducerNotifier <- w:
}
if ok := s.fireEvent(EventKindPurgerCheck, 0); ok {
s.removeOldData() // might as well cleanup while waiting
}
atomic.AddUint64(&s.stats.TotPersistLoopWait, 1)
select {
case <-s.closeCh:
break OUTER
case <-w.notifyCh:
// woken up, next loop should pick up work
atomic.AddUint64(&s.stats.TotPersistLoopWaitNotified, 1)
case ew = <-s.persisterNotifier:
// if the watchers are already caught up then let them wait,
// else let them continue to do the catch up
persistWatchers = append(persistWatchers, ew)
}
atomic.AddUint64(&s.stats.TotPersistLoopEnd, 1)
}
}
func notifyMergeWatchers(lastPersistedEpoch uint64,
persistWatchers []*epochWatcher,
) []*epochWatcher {
var watchersNext []*epochWatcher
for _, w := range persistWatchers {
if w.epoch < lastPersistedEpoch {
close(w.notifyCh)
} else {
watchersNext = append(watchersNext, w)
}
}
return watchersNext
}
func (s *Scorch) pausePersisterForMergerCatchUp(lastPersistedEpoch uint64,
lastMergedEpoch uint64, persistWatchers []*epochWatcher,
po *persisterOptions,
) (uint64, []*epochWatcher) {
// First, let the watchers proceed if they lag behind
persistWatchers = notifyMergeWatchers(lastPersistedEpoch, persistWatchers)
// Check the merger lag by counting the segment files on disk,
numFilesOnDisk, _, _ := s.diskFileStats(nil)
// On finding fewer files on disk, persister takes a short pause
// for sufficient in-memory segments to pile up for the next
// memory merge cum persist loop.
if numFilesOnDisk < uint64(po.PersisterNapUnderNumFiles) &&
po.PersisterNapTimeMSec > 0 && s.NumEventsBlocking() == 0 {
select {
case <-s.closeCh:
case <-time.After(time.Millisecond * time.Duration(po.PersisterNapTimeMSec)):
atomic.AddUint64(&s.stats.TotPersisterNapPauseCompleted, 1)
case ew := <-s.persisterNotifier:
// unblock the merger in meantime
persistWatchers = append(persistWatchers, ew)
lastMergedEpoch = ew.epoch
persistWatchers = notifyMergeWatchers(lastPersistedEpoch, persistWatchers)
atomic.AddUint64(&s.stats.TotPersisterMergerNapBreak, 1)
}
return lastMergedEpoch, persistWatchers
}
// Finding too many files on disk could be due to two reasons.
// 1. Too many older snapshots awaiting the clean up.
// 2. The merger could be lagging behind on merging the disk files.
if numFilesOnDisk > uint64(po.PersisterNapUnderNumFiles) {
if ok := s.fireEvent(EventKindPurgerCheck, 0); ok {
s.removeOldData()
}
numFilesOnDisk, _, _ = s.diskFileStats(nil)
}
// Persister pause until the merger catches up to reduce the segment
// file count under the threshold.
// But if there is memory pressure, then skip this sleep maneuvers.
OUTER:
for po.PersisterNapUnderNumFiles > 0 &&
numFilesOnDisk >= uint64(po.PersisterNapUnderNumFiles) &&
lastMergedEpoch < lastPersistedEpoch {
atomic.AddUint64(&s.stats.TotPersisterSlowMergerPause, 1)
select {
case <-s.closeCh:
break OUTER
case ew := <-s.persisterNotifier:
persistWatchers = append(persistWatchers, ew)
lastMergedEpoch = ew.epoch
}
atomic.AddUint64(&s.stats.TotPersisterSlowMergerResume, 1)
// let the watchers proceed if they lag behind
persistWatchers = notifyMergeWatchers(lastPersistedEpoch, persistWatchers)
numFilesOnDisk, _, _ = s.diskFileStats(nil)
}
return lastMergedEpoch, persistWatchers
}
func (s *Scorch) parsePersisterOptions() (*persisterOptions, error) {
po := persisterOptions{
PersisterNapTimeMSec: DefaultPersisterNapTimeMSec,
PersisterNapUnderNumFiles: DefaultPersisterNapUnderNumFiles,
MemoryPressurePauseThreshold: DefaultMemoryPressurePauseThreshold,
NumPersisterWorkers: DefaultNumPersisterWorkers,
MaxSizeInMemoryMergePerWorker: DefaultMaxSizeInMemoryMergePerWorker,
}
if v, ok := s.config["scorchPersisterOptions"]; ok {
b, err := util.MarshalJSON(v)
if err != nil {
return &po, err
}
err = util.UnmarshalJSON(b, &po)
if err != nil {
return &po, err
}
}
if err := validatePersisterOptions(&po); err != nil {
return nil, err
}
return &po, nil
}
// validatePersisterOptions validates the persister options
func validatePersisterOptions(options *persisterOptions) error {
// P < 1 is an invalid case
if options.NumPersisterWorkers <= 0 {
return fmt.Errorf("NumPersisterWorkers must be greater than 0")
}
// P = 1 and M > 0 is a valid case where one worker will serially flush all batches
// P = 1 and M = 0 is a special case which preserves the legacy one-shot in-memory merge + flush behaviour
// P > 1 and M > 0 is a valid case where multiple workers will flush batches in parallel
// P > 1 and M = 0 is an invalid case
if options.NumPersisterWorkers > 1 && options.MaxSizeInMemoryMergePerWorker == 0 {
return fmt.Errorf("MaxSizeInMemoryMergePerWorker must be greater than 0 when NumPersisterWorkers > 1")
}
return nil
}
func (s *Scorch) persistSnapshot(snapshot *IndexSnapshot, po *persisterOptions) error {
// Perform in-memory segment merging only when the memory pressure is
// below the configured threshold, else the persister performs the
// direct persistence of segments.
if s.NumEventsBlocking() < po.MemoryPressurePauseThreshold {
persisted, err := s.persistSnapshotMaybeMerge(snapshot, po)
if err != nil {
return err
}
if persisted {
return nil
}
}
return s.persistSnapshotDirect(snapshot)
}
type flushable struct {
sbsBatch []segment.Segment
sbsBatchDrops []*roaring.Bitmap
sbsBatchSnapshots []*SegmentSnapshot
}
func legacyFlushBehaviour(maxSizeInMemoryMergePerWorker, numPersisterWorkers int) bool {
// DefaultMaxSizeInMemoryMergePerWorker = 0 is a special value to preserve the legacy
// one-shot in-memory merge + flush behaviour.
return maxSizeInMemoryMergePerWorker == 0 && numPersisterWorkers == 1
}
// persistSnapshotMaybeMerge examines the snapshot and might merge and
// persist the in-memory zap segments if there are enough of them
func (s *Scorch) persistSnapshotMaybeMerge(snapshot *IndexSnapshot, po *persisterOptions) (
bool, error) {
// split our segment snapshots into unpersisted and persisted segments
var persistedSegments []*SegmentSnapshot
var unpersistedSegments []*SegmentSnapshot
numSegments := len(snapshot.segment)
// collect details of the unpersisted segments
sbs := make([]segment.Segment, 0, numSegments)
sbsDrops := make([]*roaring.Bitmap, 0, numSegments)
sbsSizes := make([]int, 0, numSegments)
for _, segmentSnapshot := range snapshot.segment {
if _, ok := segmentSnapshot.segment.(segment.PersistedSegment); ok {
persistedSegments = append(persistedSegments, segmentSnapshot)
} else {
unpersistedSegments = append(unpersistedSegments, segmentSnapshot)
sbs = append(sbs, segmentSnapshot.segment)
sbsDrops = append(sbsDrops, segmentSnapshot.deleted)
sbsSizes = append(sbsSizes, segmentSnapshot.Size())
}
}
numPersistedSegments := len(persistedSegments)
numUnpersistedSegments := len(unpersistedSegments)
// if we don't have enough segments to persist, then just return
if numUnpersistedSegments < DefaultMinSegmentsForInMemoryMerge {
return false, nil
}
// create our flushSet, which will be an array of flushable objects,
// each of which will be a batch of segments to merge and persist.
// We are guaranteed that at least one flushable object will be created,
// since we have already ensured that at least one unpersisted segment is present.
var flushSet []*flushable
if legacyFlushBehaviour(po.MaxSizeInMemoryMergePerWorker, po.NumPersisterWorkers) {
val := &flushable{
sbsBatch: slices.Clone(sbs),
sbsBatchDrops: slices.Clone(sbsDrops),
sbsBatchSnapshots: slices.Clone(unpersistedSegments),
}
flushSet = append(flushSet, val)
} else {
sbsBatch := make([]segment.Segment, 0, numUnpersistedSegments)
sbsBatchDrops := make([]*roaring.Bitmap, 0, numUnpersistedSegments)
sbsBatchSnapshots := make([]*SegmentSnapshot, 0, numUnpersistedSegments)
runningSize := 0
for i := 0; i < numUnpersistedSegments; i++ {
sbsBatch = append(sbsBatch, sbs[i])
sbsBatchDrops = append(sbsBatchDrops, sbsDrops[i])
sbsBatchSnapshots = append(sbsBatchSnapshots, unpersistedSegments[i])
runningSize += sbsSizes[i]
if runningSize >= po.MaxSizeInMemoryMergePerWorker && len(sbsBatch) >= DefaultMinSegmentsForInMemoryMerge {
flushSet = append(flushSet, &flushable{
sbsBatch: slices.Clone(sbsBatch),
sbsBatchDrops: slices.Clone(sbsBatchDrops),
sbsBatchSnapshots: slices.Clone(sbsBatchSnapshots),
})
sbsBatch, sbsBatchDrops, sbsBatchSnapshots = sbsBatch[:0], sbsBatchDrops[:0], sbsBatchSnapshots[:0]
runningSize = 0
}
}
if len(sbsBatch) > 0 {
flushSet = append(flushSet, &flushable{
sbsBatch: slices.Clone(sbsBatch),
sbsBatchDrops: slices.Clone(sbsBatchDrops),
sbsBatchSnapshots: slices.Clone(sbsBatchSnapshots),
})
}
}
// now merge each batch into a new segment, and persist all of them to disk,
// and construct a new snapshot with the merged segments
newSnapshot, newSegmentIDs, err := s.mergeAndPersistInMemorySegments(flushSet, po)
if err != nil {
return false, err
}
if newSnapshot == nil {
return false, nil
}
defer func() {
_ = newSnapshot.DecRef()
}()
// construct a snapshot that's logically equivalent to the input
// snapshot, but with merged segments replaced by the new segment
equiv := &IndexSnapshot{
parent: snapshot.parent,
segment: make([]*SegmentSnapshot, 0, numPersistedSegments+len(newSegmentIDs)),
internal: snapshot.internal,
epoch: snapshot.epoch,
creator: "persistSnapshotMaybeMerge",
}
// first add all the segments that were already persisted
// and did not participate in the merge
for _, segment := range persistedSegments {
equiv.segment = append(equiv.segment, segment)
}
// next, add all the segments that were unpersisted and hence
// participated in the merge, from the merged snapshot
for _, segment := range newSnapshot.segment {
if _, ok := newSegmentIDs[segment.id]; ok {
equiv.segment = append(equiv.segment, &SegmentSnapshot{
id: segment.id,
segment: segment.segment,
deleted: nil, // nil since merging handled deletions
stats: segment.stats,
})
}
}
// finally, persist the equivalent snapshot to disk
err = s.persistSnapshotDirect(equiv)
if err != nil {
return false, err
}
return true, nil
}
func copyToDirectory(srcPath string, d index.Directory) (int64, error) {
if d == nil {
return 0, nil
}
dest, err := d.GetWriter(filepath.Join("store", filepath.Base(srcPath)))
if err != nil {
return 0, fmt.Errorf("GetWriter err: %v", err)
}
// skip
if dest == nil {
return 0, nil
}
sourceFileStat, err := os.Stat(srcPath)
if err != nil {
return 0, err
}
if !sourceFileStat.Mode().IsRegular() {
return 0, fmt.Errorf("%s is not a regular file", srcPath)
}
source, err := os.Open(srcPath)
if err != nil {
return 0, err
}
defer source.Close()
defer dest.Close()
return io.Copy(dest, source)
}
func persistToDirectory(seg segment.UnpersistedSegment, d index.Directory,
path string,
) error {
if d == nil {
return seg.Persist(path)
}
sg, ok := seg.(io.WriterTo)
if !ok {
return fmt.Errorf("no io.WriterTo segment implementation found")
}
w, err := d.GetWriter(filepath.Join("store", filepath.Base(path)))
if err != nil {
return err
}
_, err = sg.WriteTo(w)
w.Close()
return err
}
func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *util.BoltTxImpl, path string, segPlugin SegmentPlugin, d index.Directory) ([]string, map[uint64]string, error) {
snapshotsBucket, err := tx.CreateBucketIfNotExists(util.BoltSnapshotsBucket)
if err != nil {
return nil, nil, err
}
newSnapshotKey := encodeUvarintAscending(nil, snapshot.epoch)
snapshotBucket, err := snapshotsBucket.CreateBucketIfNotExists(newSnapshotKey)
if err != nil {
return nil, nil, err
}
// persist meta values
metaBucket, err := snapshotBucket.CreateBucketIfNotExists(util.BoltMetaDataKey)
if err != nil {
return nil, nil, err
}
err = metaBucket.Put(util.BoltMetaDataSegmentTypeKey, []byte(segPlugin.Type()), nil)
if err != nil {
return nil, nil, err
}
buf := make([]byte, binary.MaxVarintLen32)
binary.BigEndian.PutUint32(buf, segPlugin.Version())
err = metaBucket.Put(util.BoltMetaDataSegmentVersionKey, buf, nil)
if err != nil {
return nil, nil, err
}
// always obtain the path from the parent snapshot if available
// since that is the primary source of truth for context
if snapshot.parent != nil {
path = snapshot.parent.path
}
writer, err := util.NewFileWriter(
[]byte(path + string(os.PathSeparator) + "root.bolt"))
if err != nil {
return nil, nil, err
}
// persist the writer ID used for the bolt snapshot
err = metaBucket.Put(util.BoltMetaDataFileWriterIDKey, []byte(writer.Id()), writer)
if err != nil {
return nil, nil, err
}
// Storing the timestamp at which the current indexSnapshot
// was persisted, useful when you want to spread the
// numSnapshotsToKeep reasonably better than consecutive
// epochs.
currTimeStamp := time.Now()
timeStampBinary, err := currTimeStamp.MarshalText()
if err != nil {
return nil, nil, err
}
err = metaBucket.Put(util.BoltMetaDataTimeStamp, timeStampBinary, writer)
if err != nil {
return nil, nil, err
}
// persist internal values
internalBucket, err := snapshotBucket.CreateBucketIfNotExists(util.BoltInternalKey)
if err != nil {
return nil, nil, err
}
for k, v := range snapshot.internal {
err = internalBucket.Put([]byte(k), v, writer)
if err != nil {
return nil, nil, err
}
}
if snapshot.parent != nil {
val := make([]byte, 8)
bytesWritten := atomic.LoadUint64(&snapshot.parent.stats.TotBytesWrittenAtIndexTime)
binary.LittleEndian.PutUint64(val, bytesWritten)
err = internalBucket.Put(util.TotBytesWrittenKey, val, writer)
if err != nil {
return nil, nil, err
}
}
filenames := make([]string, 0, len(snapshot.segment))
newSegmentPaths := make(map[uint64]string, len(snapshot.segment))
// first ensure that each segment in this snapshot has been persisted
for _, segmentSnapshot := range snapshot.segment {
snapshotSegmentKey := encodeUvarintAscending(nil, segmentSnapshot.id)
snapshotSegmentBucket, err := snapshotBucket.CreateBucketIfNotExists(snapshotSegmentKey)
if err != nil {
return nil, nil, err
}
switch seg := segmentSnapshot.segment.(type) {
case segment.PersistedSegment:
segPath := seg.Path()
_, err = copyToDirectory(segPath, d)
if err != nil {
return nil, nil, fmt.Errorf("segment: %s copy err: %v", segPath, err)
}
filename := filepath.Base(segPath)
err = snapshotSegmentBucket.Put(util.BoltPathKey, []byte(filename), writer)
if err != nil {
return nil, nil, err
}
filenames = append(filenames, filename)
case segment.UnpersistedSegment:
// need to persist this to disk
filename := zapFileName(segmentSnapshot.id)
path := filepath.Join(path, filename)
err := persistToDirectory(seg, d, path)
if err != nil {
return nil, nil, fmt.Errorf("segment: %s persist err: %v", path, err)
}
newSegmentPaths[segmentSnapshot.id] = path
err = snapshotSegmentBucket.Put(util.BoltPathKey, []byte(filename), nil)
if err != nil {
return nil, nil, err
}
filenames = append(filenames, filename)
default:
return nil, nil, fmt.Errorf("unknown segment type: %T", seg)
}
// store current deleted bits
var roaringBuf bytes.Buffer
if segmentSnapshot.deleted != nil {
_, err = segmentSnapshot.deleted.WriteTo(&roaringBuf)
if err != nil {
return nil, nil, fmt.Errorf("error persisting roaring bytes: %v", err)
}
err = snapshotSegmentBucket.Put(util.BoltDeletedKey, roaringBuf.Bytes(), writer)
if err != nil {
return nil, nil, err
}
}
// store segment stats
if segmentSnapshot.stats != nil {
statsBytes, err := json.Marshal(segmentSnapshot.stats.Fetch())
if err != nil {
return nil, nil, err
}
err = snapshotSegmentBucket.Put(util.BoltStatsKey, statsBytes, writer)
if err != nil {
return nil, nil, err
}
}
// store updated field info
if segmentSnapshot.updatedFields != nil {
updatedFieldsBytes, err := json.Marshal(segmentSnapshot.updatedFields)
if err != nil {
return nil, nil, err
}
err = snapshotSegmentBucket.Put(
util.BoltUpdatedFieldsKey, updatedFieldsBytes, writer)
if err != nil {
return nil, nil, err
}
}
}
return filenames, newSegmentPaths, nil
}
func (s *Scorch) persistSnapshotDirect(snapshot *IndexSnapshot) (err error) {
// start a write transaction
tx, err := s.rootBolt.Begin(true)
if err != nil {
return err
}
// defer rollback on error
defer func() {
if err != nil {
_ = tx.Rollback()
}
}()
filenames, newSegmentPaths, err := prepareBoltSnapshot(snapshot, tx, s.path, s.segPlugin, nil)
if err != nil {
return err
}
// we need to swap in a new root only when we've persisted 1 or
// more segments -- whereby the new root would have 1-for-1
// replacements of in-memory segments with file-based segments
//
// other cases like updates to internal values only, and/or when
// there are only deletions, are already covered and persisted by
// the newly populated boltdb snapshotBucket above
if len(newSegmentPaths) > 0 {
// now try to open all the new snapshots
newSegments := make(map[uint64]segment.Segment, len(newSegmentPaths))
defer func() {
for _, s := range newSegments {
if s != nil {
// cleanup segments that were opened but not
// swapped into the new root
_ = s.Close()
}
}
}()
for segmentID, path := range newSegmentPaths {
newSegments[segmentID], err = s.segPlugin.OpenUsing(path, s.segmentConfig)
if err != nil {
return fmt.Errorf("error opening new segment at %s, %v", path, err)
}
}
persist := &persistIntroduction{
persisted: newSegments,
applied: make(notificationChan),
}
select {
case <-s.closeCh:
return segment.ErrClosed
case s.persists <- persist:
}
// blockingly wait until the persist has been applied
<-persist.applied
}
err = tx.Commit()
if err != nil {
return err
}
err = s.rootBolt.Sync()
if err != nil {
return err
}
// allow files to become eligible for removal after commit, such
// as file segments from snapshots that came from the merger
s.rootLock.Lock()
for _, filename := range filenames {
delete(s.ineligibleForRemoval, filename)
}
s.rootLock.Unlock()
return nil
}
func zapFileName(epoch uint64) string {
return fmt.Sprintf("%012x.zap", epoch)
}
// bolt snapshot code
func (s *Scorch) loadFromBolt() error {
err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
foundRoot := false
c := snapshots.Cursor()
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
_, snapshotEpoch, err := decodeUvarintAscending(k)
if err != nil {
log.Printf("unable to parse segment epoch %x, continuing", k)
continue
}
if foundRoot {
s.AddEligibleForRemoval(snapshotEpoch)
continue
}
snapshot := snapshots.GetBucket(k)
if snapshot == nil {
log.Printf("snapshot key, but bucket missing %x, continuing", k)
s.AddEligibleForRemoval(snapshotEpoch)
continue
}
indexSnapshot, err := s.loadSnapshot(snapshot)
if err != nil {
log.Printf("unable to load snapshot, %v, continuing", err)
s.AddEligibleForRemoval(snapshotEpoch)
continue
}
indexSnapshot.epoch = snapshotEpoch
// set the nextSegmentID
s.nextSegmentID, err = s.maxSegmentIDOnDisk()
if err != nil {
return err
}
s.nextSegmentID++
s.rootLock.Lock()
s.nextSnapshotEpoch = snapshotEpoch + 1
rootPrev := s.root
s.root = indexSnapshot
s.rootLock.Unlock()
if rootPrev != nil {
_ = rootPrev.DecRef()
}
foundRoot = true
}
// try init trainer and load the trained data
if trainer := initTrainer(s, s.config); trainer != nil {
s.trainer = trainer
trainerBucket := snapshots.GetBucket(util.BoltTrainerKey)
err := s.trainer.loadTrainedData(trainerBucket)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
// we initialize the checkPoints from all the persisted snapshots,
// because if we have state to load, the previous process was already
// maintaining checkpoints, whatever survives in the bucket now is that state.
persistedSnapshots, err := s.rootBoltSnapshotMetaData()
if err != nil {
return err
}
s.checkPoints = persistedSnapshots
return nil
}
// LoadSnapshot loads the segment with the specified epoch
// NOTE: this is currently ONLY intended to be used by the command-line tool
func (s *Scorch) LoadSnapshot(epoch uint64) (rv *IndexSnapshot, err error) {
err = s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
snapshotKey := encodeUvarintAscending(nil, epoch)
snapshot := snapshots.GetBucket(snapshotKey)
if snapshot == nil {
return fmt.Errorf("snapshot with epoch: %v - doesn't exist", epoch)
}
rv, err = s.loadSnapshot(snapshot)
return err
})
if err != nil {
return nil, err
}
return rv, nil
}
func (s *Scorch) loadSnapshot(snapshot *util.BoltBucketImpl) (*IndexSnapshot, error) {
rv := &IndexSnapshot{
parent: s,
internal: make(map[string][]byte),
refs: 1,
creator: "loadSnapshot",
}
// first we look for the meta-data bucket, this will tell us
// which segment type/version was used for this snapshot
// all operations for this scorch will use this type/version
metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
if metaBucket == nil {
_ = rv.DecRef()
return nil, fmt.Errorf("meta-data bucket missing")
}
segmentType, err := metaBucket.Get(util.BoltMetaDataSegmentTypeKey, nil)
if err != nil {
_ = rv.DecRef()
return nil, fmt.Errorf("segment type missing: %v", err)
}
segmentVersionBytes, err := metaBucket.Get(util.BoltMetaDataSegmentVersionKey, nil)
if err != nil {
_ = rv.DecRef()
return nil, fmt.Errorf("segment version missing: %v", err)
}
segmentVersion := binary.BigEndian.Uint32(segmentVersionBytes)
err = s.loadSegmentPlugin(string(segmentType), segmentVersion)
if err != nil {
_ = rv.DecRef()
return nil, fmt.Errorf(
"unable to load correct segment wrapper: %v", err)
}
fileWriterID, err := metaBucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
if err != nil {
_ = rv.DecRef()
return nil, fmt.Errorf("file writer id missing: %v", err)
}
reader, err := util.NewFileReader(
string(fileWriterID), []byte(s.path+string(os.PathSeparator)+"root.bolt"))
if err != nil {
_ = rv.DecRef()
return nil, fmt.Errorf("unable to load correct reader: %v", err)
}
var running uint64
c := snapshot.Cursor()
for k, _ := c.First(); k != nil; k, _ = c.Next() {
if k[0] == util.BoltInternalKey[0] {
internalBucket := snapshot.GetBucket(k)
if internalBucket == nil {
_ = rv.DecRef()
return nil, fmt.Errorf("internal bucket missing")
}
err := internalBucket.ForEach(func(key []byte, val []byte) error {
rv.internal[string(key)] = val
return nil
}, reader)
if err != nil {
_ = rv.DecRef()
return nil, err
}
} else if k[0] != util.BoltMetaDataKey[0] {
segmentBucket := snapshot.GetBucket(k)
if segmentBucket == nil {
_ = rv.DecRef()
return nil, fmt.Errorf("segment key, but bucket missing %x", k)
}
segmentSnapshot, err := s.loadSegment(segmentBucket, reader)
if err != nil {
_ = rv.DecRef()
return nil, fmt.Errorf("failed to load segment: %v", err)
}
_, segmentSnapshot.id, err = decodeUvarintAscending(k)
if err != nil {
_ = rv.DecRef()
return nil, fmt.Errorf("failed to decode segment id: %v", err)
}
rv.segment = append(rv.segment, segmentSnapshot)
rv.offsets = append(rv.offsets, running)
// Merge all segment level updated field info for use during queries
if segmentSnapshot.updatedFields != nil {
rv.MergeUpdateFieldsInfo(segmentSnapshot.updatedFields)
}
running += segmentSnapshot.segment.Count()
}
}
return rv, nil
}
func (s *Scorch) loadSegment(segmentBucket *util.BoltBucketImpl, reader util.FileReader) (
*SegmentSnapshot, error) {
pathBytes, err := segmentBucket.Get(util.BoltPathKey, nil)
if pathBytes == nil {
return nil, fmt.Errorf("segment path missing")
}
segmentPath := s.path + string(os.PathSeparator) + string(pathBytes)
seg, err := s.segPlugin.OpenUsing(segmentPath, s.segmentConfig)
if err != nil {
return nil, fmt.Errorf("error opening bolt segment: %v", err)
}
rv := &SegmentSnapshot{
segment: seg,
cachedDocs: &cachedDocs{cache: nil},
cachedMeta: newCachedMeta(),
}
deletedBytes, err := segmentBucket.Get(util.BoltDeletedKey, reader)
if err != nil {
_ = seg.Close()
return nil, fmt.Errorf("error getting deleted bytes: %v", err)
}
if deletedBytes != nil {
deletedBitmap := roaring.NewBitmap()
r := bytes.NewReader(deletedBytes)
_, err := deletedBitmap.ReadFrom(r)
if err != nil {
_ = seg.Close()
return nil, fmt.Errorf("error reading deleted bytes: %v", err)
}
if !deletedBitmap.IsEmpty() {
rv.deleted = deletedBitmap
}
}
statBytes, err := segmentBucket.Get(util.BoltStatsKey, reader)
if err != nil {
_ = seg.Close()
return nil, fmt.Errorf("error getting stat bytes: %v", err)
}
if statBytes != nil {
var statsMap map[string]map[string]uint64
err := json.Unmarshal(statBytes, &statsMap)
if err != nil {
_ = seg.Close()
return nil, fmt.Errorf("error reading stat bytes: %v", err)
}
rv.stats = &fieldStats{statMap: statsMap}
}
updatedFieldBytes, err := segmentBucket.Get(util.BoltUpdatedFieldsKey, reader)
if err != nil {
_ = seg.Close()
return nil, fmt.Errorf("error getting updated field bytes: %v", err)
}
if updatedFieldBytes != nil {
var updatedFields map[string]*index.UpdateFieldInfo
err = json.Unmarshal(updatedFieldBytes, &updatedFields)
if err != nil {
_ = seg.Close()
return nil, fmt.Errorf("error reading updated field bytes: %v", err)
}
rv.updatedFields = updatedFields
// Set the value within the segment base for use during merge
rv.UpdateFieldsInfo(rv.updatedFields)
}
return rv, nil
}
// identify all the file callback writer ids that are in use by boltdb
func (s *Scorch) boltFileWriterIDsInUse() (map[string]struct{}, error) {
idMap := make(map[string]struct{})
err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
c := snapshots.Cursor()
for k, _ := c.First(); k != nil; k, _ = c.Next() {
snapshot := snapshots.GetBucket(k)
if snapshot == nil {
continue
}
metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
if metaBucket == nil {
continue
}
id, err := metaBucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
if err != nil {
return err
}
idMap[string(id)] = struct{}{}
}
return nil
})
if err != nil {
return nil, err
}
return idMap, nil
}
// remove all content in boltdb associated with the file callback
// writer ids and process the data using the latest file writer
func (s *Scorch) removeBoltFileWriterIDs(ids map[string]struct{}) error {
filePath := s.path + string(os.PathSeparator) + "root.bolt"
writer, err := util.NewFileWriter([]byte(filePath))
if err != nil {
return err
}
err = s.rootBolt.Update(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
c := snapshots.Cursor()
for k, _ := c.First(); k != nil; k, _ = c.Next() {
snapshot := snapshots.GetBucket(k)
if snapshot == nil {
continue
}
metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
if metaBucket == nil {
continue
}
fileWriterIDBytes, err := metaBucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
if err != nil {
return err
}
fileWriterID := string(fileWriterIDBytes)
if _, ok := ids[fileWriterID]; ok {
reader, err := util.NewFileReader(fileWriterID, []byte(filePath))
if err != nil {
return fmt.Errorf("unable to load correct reader: %v", err)
}
cc := snapshot.Cursor()
for kk, _ := cc.First(); kk != nil; kk, _ = cc.Next() {
if kk[0] == util.BoltInternalKey[0] {
internalBucket := snapshot.GetBucket(kk)
if internalBucket == nil {
continue
}
// process all of the internal values and replace them with new values
internalBucketVals := make(map[string][]byte)
err := internalBucket.ForEach(func(key []byte, val []byte) error {
internalBucketVals[string(key)] = val
return nil
}, reader)
if err != nil {
return err
}
for key, val := range internalBucketVals {
err = internalBucket.Put([]byte(key), val, writer)
if err != nil {
return err
}
}
} else if kk[0] != util.BoltMetaDataKey[0] {
segmentBucket := snapshot.GetBucket(kk)
if segmentBucket == nil {
continue
}
// process the updated field key
updatedFieldBytes, err := segmentBucket.Get(util.BoltUpdatedFieldsKey, reader)
if err != nil {
return fmt.Errorf("error getting updated field bytes: %v", err)
}
if updatedFieldBytes != nil {
err = segmentBucket.Put(util.BoltUpdatedFieldsKey, updatedFieldBytes, writer)
if err != nil {
return err
}
}
// process the deleted key
deletedBytes, err := segmentBucket.Get(util.BoltDeletedKey, reader)
if err != nil {
return fmt.Errorf("error getting deleted bytes: %v", err)
}
if deletedBytes != nil {
err = segmentBucket.Put(util.BoltDeletedKey, deletedBytes, writer)
if err != nil {
return err
}
}
// process the stats key
statsBytes, err := segmentBucket.Get(util.BoltStatsKey, reader)
if err != nil {
return fmt.Errorf("error getting stats bytes: %v", err)
}
if statsBytes != nil {
err = segmentBucket.Put(util.BoltStatsKey, statsBytes, writer)
if err != nil {
return err
}
}
}
}
err = metaBucket.Put(util.BoltMetaDataFileWriterIDKey,
[]byte(writer.Id()), writer)
if err != nil {
return err
}
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func (s *Scorch) removeOldData() {
removed, err := s.removeOldBoltSnapshots()
if err != nil {
s.fireAsyncError(NewScorchError(
persister,
fmt.Sprintf("got err removing old bolt snapshots: %v", err),
ErrCleanup,
))
}
atomic.AddUint64(&s.stats.TotSnapshotsRemovedFromMetaStore, uint64(removed))
err = s.removeOldZapFiles()
if err != nil {
s.fireAsyncError(NewScorchError(
persister,
fmt.Sprintf("got err removing old zap files: %v", err),
ErrCleanup,
))
}
}
func getTimeSeriesSnapshots(maxDataPoints int, interval time.Duration,
snapshots []*snapshotMetaData) map[uint64]time.Time {
if interval == 0 || len(snapshots) == 0 || maxDataPoints <= 0 {
return map[uint64]time.Time{}
}
// the map containing the time series snapshots, i.e the timeseries of snapshots
// each of which is separated by rollbackSamplingInterval
rv := make(map[uint64]time.Time, maxDataPoints)
// the last point in the "time series", i.e. the timeseries of snapshots
// each of which is separated by rollbackSamplingInterval
ptr := len(snapshots) - 1
rv[snapshots[ptr].epoch] = snapshots[ptr].timeStamp
numSnapshotsProtected := 1
// traverse the list in reverse order, older timestamps to newer ones.
for i := ptr - 1; i >= 0 && numSnapshotsProtected < maxDataPoints; i-- {
sinceLast := snapshots[i].timeStamp.Sub(snapshots[ptr].timeStamp)
if sinceLast >= interval {
// capture the snapshot at the interval boundary: the exact match
// if there is one, otherwise the older neighbour (i+1), which was
// the last snapshot seen before the interval was crossed
idx := i
if sinceLast > interval {
idx = i + 1
}
if _, ok := rv[snapshots[idx].epoch]; !ok {
rv[snapshots[idx].epoch] = snapshots[idx].timeStamp
ptr = idx
numSnapshotsProtected++
}
}
}
return rv
}
// getProtectedSnapshots aims to fetch the epochs keep based on a timestamp basis.
// It tries to get NumSnapshotsToKeep snapshots, each of which are separated
// by a time duration of RollbackSamplingInterval.
func (s *Scorch) getProtectedSnapshots(liveSnapshots []*snapshotMetaData) map[uint64]time.Time {
// keep numSnapshotsToKeep - 1 worth of time series snapshots, because we always
// must preserve the very latest snapshot in bolt as well to avoid accidental
// deletes of bolt entries and cleanups by the purger code.
protectedEpochs := getTimeSeriesSnapshots(s.numSnapshotsToKeep-1,
s.rollbackSamplingInterval, liveSnapshots)
numProtected := len(protectedEpochs)
// always protect the latest snapshot
latestSnapshot := liveSnapshots[0]
if _, ok := protectedEpochs[latestSnapshot.epoch]; !ok {
protectedEpochs[latestSnapshot.epoch] = latestSnapshot.timeStamp
numProtected++
}
// if we still have not protected enough snapshots, then protect the next most recent snapshots
for i := 1; i < len(liveSnapshots) && numProtected < s.numSnapshotsToKeep; i++ {
if _, ok := protectedEpochs[liveSnapshots[i].epoch]; !ok {
protectedEpochs[liveSnapshots[i].epoch] = liveSnapshots[i].timeStamp
numProtected++
}
}
return protectedEpochs
}
func newCheckPoints(snapshots map[uint64]time.Time) []*snapshotMetaData {
rv := make([]*snapshotMetaData, 0, len(snapshots))
keys := make([]uint64, 0, len(snapshots))
for k := range snapshots {
keys = append(keys, k)
}
sort.SliceStable(keys, func(i, j int) bool {
return snapshots[keys[i]].After(snapshots[keys[j]])
})
for _, key := range keys {
rv = append(rv, &snapshotMetaData{
epoch: key,
timeStamp: snapshots[key],
})
}
return rv
}
func (s *Scorch) removeOldBoltSnapshots() (numRemoved int, err error) {
// first get the set of live snapshots
liveSnapshots, err := s.getLiveSnapshots()
if err != nil {
return 0, err
}
// if no live snapshots, then nothing to do
if len(liveSnapshots) == 0 {
return 0, nil
}
// then get the set of protected snapshots, which are the ones we want to keep
protectedSnapshots := s.getProtectedSnapshots(liveSnapshots)
var epochsToRemove []uint64
var newEligible []uint64
s.rootLock.Lock()
for _, epoch := range s.eligibleForRemoval {
if _, ok := protectedSnapshots[epoch]; ok {
// protected
newEligible = append(newEligible, epoch)
} else {
epochsToRemove = append(epochsToRemove, epoch)
}
}
s.eligibleForRemoval = newEligible
s.rootLock.Unlock()
s.checkPoints = newCheckPoints(protectedSnapshots)
if len(epochsToRemove) == 0 {
return 0, nil
}
tx, err := s.rootBolt.Begin(true)
if err != nil {
return 0, err
}
defer func() {
if err == nil {
err = tx.Commit()
} else {
_ = tx.Rollback()
}
if err == nil {
err = s.rootBolt.Sync()
}
}()
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return 0, nil
}
for _, epochToRemove := range epochsToRemove {
k := encodeUvarintAscending(nil, epochToRemove)
err = snapshots.DeleteBucket(k)
if err == bolt.ErrBucketNotFound {
err = nil
}
if err == nil {
numRemoved++
}
}
return numRemoved, err
}
func (s *Scorch) maxSegmentIDOnDisk() (uint64, error) {
files, err := os.ReadDir(s.path)
if err != nil {
return 0, err
}
var rv uint64
for _, f := range files {
fname := f.Name()
if filepath.Ext(fname) == ".zap" {
prefix := strings.TrimSuffix(fname, ".zap")
id, err2 := strconv.ParseUint(prefix, 16, 64)
if err2 != nil {
return 0, err2
}
if id > rv {
rv = id
}
}
}
return rv, err
}
// Removes any *.zap files which aren't listed in the rootBolt.
func (s *Scorch) removeOldZapFiles() error {
liveFileNames, err := s.loadZapFileNames()
if err != nil {
return err
}
files, err := os.ReadDir(s.path)
if err != nil {
return err
}
s.rootLock.RLock()
for _, f := range files {
fname := f.Name()
if filepath.Ext(fname) == ".zap" {
if _, exists := liveFileNames[fname]; !exists && !s.ineligibleForRemoval[fname] && (s.copyScheduled[fname] <= 0) {
err := os.Remove(s.path + string(os.PathSeparator) + fname)
if err != nil {
log.Printf("got err removing file: %s, err: %v", fname, err)
}
}
}
}
s.rootLock.RUnlock()
return nil
}
func (s *Scorch) getBoundaryCheckPoint(timeStamp time.Time) time.Time {
numCheckpoints := float64(len(s.checkPoints))
if numCheckpoints == 0 {
return timeStamp
}
checkpointIdx := int(math.Floor(numCheckpoints * s.rollbackRetentionFactor))
if checkpointIdx >= len(s.checkPoints) {
checkpointIdx = len(s.checkPoints) - 1
}
boundary := s.checkPoints[checkpointIdx]
if boundary.timeStamp.Before(timeStamp) {
return boundary.timeStamp
}
return timeStamp
}
type snapshotMetaData struct {
epoch uint64
timeStamp time.Time
}
// returns all the snapshots that are currently persisted
// in the rootBolt, sorted by latest to oldest epoch.
func (s *Scorch) rootBoltSnapshotMetaData() ([]*snapshotMetaData, error) {
var rv []*snapshotMetaData
err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
sc := snapshots.Cursor()
for sk, _ := sc.Last(); sk != nil; sk, _ = sc.Prev() {
_, snapshotEpoch, err := decodeUvarintAscending(sk)
if err != nil {
continue
}
snapshot := snapshots.GetBucket(sk)
if snapshot == nil {
continue
}
metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
if metaBucket == nil {
continue
}
timeStampBytes, err := metaBucket.Get(util.BoltMetaDataTimeStamp, nil)
if err != nil {
continue
}
var timeStamp time.Time
err = timeStamp.UnmarshalText(timeStampBytes)
if err != nil {
continue
}
meta := &snapshotMetaData{
epoch: snapshotEpoch,
timeStamp: timeStamp,
}
rv = append(rv, meta)
}
return nil
})
if err != nil {
return nil, err
}
return rv, nil
}
func (s *Scorch) getLiveSnapshots() ([]*snapshotMetaData, error) {
// get all the snapshots that are currently persisted
meta, err := s.rootBoltSnapshotMetaData()
if err != nil {
return nil, err
}
// if none persisted, then nothing to do
if len(meta) == 0 {
return nil, nil
}
// check if we have a rollback sampling interval, if not,
// then we will just return the latest numSnapshotsToKeep snapshots
if s.rollbackSamplingInterval <= 0 {
if len(meta) <= s.numSnapshotsToKeep {
return meta, nil
}
return meta[:s.numSnapshotsToKeep], nil
}
// we consider a snapshot to be live if:
// 1. it is the latest snapshot
// 2. it is within our expiration duration
var liveSnapshots []*snapshotMetaData
// always keep the latest snapshot
liveSnapshots = append(liveSnapshots, meta[0])
extraSnapshots := s.numSnapshotsToKeep - 1
if extraSnapshots <= 0 {
return liveSnapshots, nil
}
// if we need extra snapshots, compute an expiration duration
// beyond which we will not consider snapshots to be live
currTime := time.Now()
expirationDuration := time.Duration(extraSnapshots) * s.rollbackSamplingInterval
cutoffTime := currTime.Add(-expirationDuration)
// if we have previous checkpoints, then we can extend
// the cutoff time based on the boundary checkpoint
for _, snapshot := range meta[1:] {
if snapshot.timeStamp.Before(cutoffTime) {
boundary := s.getBoundaryCheckPoint(snapshot.timeStamp)
if boundary.Before(snapshot.timeStamp) {
cutoffTime = boundary
}
break
}
}
// add all snapshots that are newer than the cutoff time
for _, snapshot := range meta[1:] {
if !snapshot.timeStamp.Before(cutoffTime) {
liveSnapshots = append(liveSnapshots, snapshot)
}
}
return liveSnapshots, nil
}
func (s *Scorch) RootBoltSnapshotEpochs() ([]uint64, error) {
var rv []uint64
err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
sc := snapshots.Cursor()
for sk, _ := sc.Last(); sk != nil; sk, _ = sc.Prev() {
_, snapshotEpoch, err := decodeUvarintAscending(sk)
if err != nil {
continue
}
rv = append(rv, snapshotEpoch)
}
return nil
})
return rv, err
}
// Returns the *.zap file names that are listed in the rootBolt.
func (s *Scorch) loadZapFileNames() (map[string]struct{}, error) {
rv := map[string]struct{}{}
err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
sc := snapshots.Cursor()
for sk, _ := sc.First(); sk != nil; sk, _ = sc.Next() {
snapshot := snapshots.GetBucket(sk)
if snapshot == nil {
continue
}
segc := snapshot.Cursor()
for segk, _ := segc.First(); segk != nil; segk, _ = segc.Next() {
if segk[0] == util.BoltInternalKey[0] {
continue
}
segmentBucket := snapshot.GetBucket(segk)
if segmentBucket == nil {
continue
}
pathBytes, err := segmentBucket.Get(util.BoltPathKey, nil)
if err != nil {
continue
}
if pathBytes == nil {
continue
}
pathString := string(pathBytes)
rv[string(pathString)] = struct{}{}
}
}
return nil
})
return rv, err
}