github.com/Finschia/finschia-sdk@v0.48.1/snapshots/store.go (about)

     1  package snapshots
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/binary"
     6  	"io"
     7  	"math"
     8  	"os"
     9  	"path/filepath"
    10  	"strconv"
    11  	"sync"
    12  
    13  	"github.com/gogo/protobuf/proto"
    14  	dbm "github.com/tendermint/tm-db"
    15  
    16  	"github.com/Finschia/finschia-sdk/snapshots/types"
    17  	sdkerrors "github.com/Finschia/finschia-sdk/types/errors"
    18  )
    19  
    20  const (
    21  	// keyPrefixSnapshot is the prefix for snapshot database keys
    22  	keyPrefixSnapshot byte = 0x01
    23  )
    24  
    25  // Store is a snapshot store, containing snapshot metadata and binary chunks.
    26  type Store struct {
    27  	db  dbm.DB
    28  	dir string
    29  
    30  	mtx    sync.Mutex
    31  	saving map[uint64]bool // heights currently being saved
    32  }
    33  
    34  // NewStore creates a new snapshot store.
    35  func NewStore(db dbm.DB, dir string) (*Store, error) {
    36  	if dir == "" {
    37  		return nil, sdkerrors.Wrap(sdkerrors.ErrLogic, "snapshot directory not given")
    38  	}
    39  	err := os.MkdirAll(dir, 0o755)
    40  	if err != nil {
    41  		return nil, sdkerrors.Wrapf(err, "failed to create snapshot directory %q", dir)
    42  	}
    43  
    44  	return &Store{
    45  		db:     db,
    46  		dir:    dir,
    47  		saving: make(map[uint64]bool),
    48  	}, nil
    49  }
    50  
    51  // Delete deletes a snapshot.
    52  func (s *Store) Delete(height uint64, format uint32) error {
    53  	s.mtx.Lock()
    54  	saving := s.saving[height]
    55  	s.mtx.Unlock()
    56  	if saving {
    57  		return sdkerrors.Wrapf(sdkerrors.ErrConflict,
    58  			"snapshot for height %v format %v is currently being saved", height, format)
    59  	}
    60  	err := s.db.DeleteSync(encodeKey(height, format))
    61  	if err != nil {
    62  		return sdkerrors.Wrapf(err, "failed to delete snapshot for height %v format %v",
    63  			height, format)
    64  	}
    65  	err = os.RemoveAll(s.pathSnapshot(height, format))
    66  	return sdkerrors.Wrapf(err, "failed to delete snapshot chunks for height %v format %v",
    67  		height, format)
    68  }
    69  
    70  // Get fetches snapshot info from the database.
    71  func (s *Store) Get(height uint64, format uint32) (*types.Snapshot, error) {
    72  	bytes, err := s.db.Get(encodeKey(height, format))
    73  	if err != nil {
    74  		return nil, sdkerrors.Wrapf(err, "failed to fetch snapshot metadata for height %v format %v",
    75  			height, format)
    76  	}
    77  	if bytes == nil {
    78  		return nil, nil
    79  	}
    80  	snapshot := &types.Snapshot{}
    81  	err = proto.Unmarshal(bytes, snapshot)
    82  	if err != nil {
    83  		return nil, sdkerrors.Wrapf(err, "failed to decode snapshot metadata for height %v format %v",
    84  			height, format)
    85  	}
    86  	if snapshot.Metadata.ChunkHashes == nil {
    87  		snapshot.Metadata.ChunkHashes = [][]byte{}
    88  	}
    89  	return snapshot, nil
    90  }
    91  
    92  // Get fetches the latest snapshot from the database, if any.
    93  func (s *Store) GetLatest() (*types.Snapshot, error) {
    94  	iter, err := s.db.ReverseIterator(encodeKey(0, 0), encodeKey(uint64(math.MaxUint64), math.MaxUint32))
    95  	if err != nil {
    96  		return nil, sdkerrors.Wrap(err, "failed to find latest snapshot")
    97  	}
    98  	defer iter.Close()
    99  
   100  	var snapshot *types.Snapshot
   101  	if iter.Valid() {
   102  		snapshot = &types.Snapshot{}
   103  		err := proto.Unmarshal(iter.Value(), snapshot)
   104  		if err != nil {
   105  			return nil, sdkerrors.Wrap(err, "failed to decode latest snapshot")
   106  		}
   107  	}
   108  	err = iter.Error()
   109  	return snapshot, sdkerrors.Wrap(err, "failed to find latest snapshot")
   110  }
   111  
   112  // List lists snapshots, in reverse order (newest first).
   113  func (s *Store) List() ([]*types.Snapshot, error) {
   114  	iter, err := s.db.ReverseIterator(encodeKey(0, 0), encodeKey(uint64(math.MaxUint64), math.MaxUint32))
   115  	if err != nil {
   116  		return nil, sdkerrors.Wrap(err, "failed to list snapshots")
   117  	}
   118  	defer iter.Close()
   119  
   120  	snapshots := make([]*types.Snapshot, 0)
   121  	for ; iter.Valid(); iter.Next() {
   122  		snapshot := &types.Snapshot{}
   123  		err := proto.Unmarshal(iter.Value(), snapshot)
   124  		if err != nil {
   125  			return nil, sdkerrors.Wrap(err, "failed to decode snapshot info")
   126  		}
   127  		snapshots = append(snapshots, snapshot)
   128  	}
   129  	return snapshots, iter.Error()
   130  }
   131  
   132  // Load loads a snapshot (both metadata and binary chunks). The chunks must be consumed and closed.
   133  // Returns nil if the snapshot does not exist.
   134  func (s *Store) Load(height uint64, format uint32) (*types.Snapshot, <-chan io.ReadCloser, error) {
   135  	snapshot, err := s.Get(height, format)
   136  	if snapshot == nil || err != nil {
   137  		return nil, nil, err
   138  	}
   139  
   140  	ch := make(chan io.ReadCloser)
   141  	go func() {
   142  		defer close(ch)
   143  		for i := uint32(0); i < snapshot.Chunks; i++ {
   144  			pr, pw := io.Pipe()
   145  			ch <- pr
   146  			chunk, err := s.loadChunkFile(height, format, i)
   147  			if err != nil {
   148  				pw.CloseWithError(err)
   149  				return
   150  			}
   151  			defer chunk.Close()
   152  			_, err = io.Copy(pw, chunk)
   153  			if err != nil {
   154  				pw.CloseWithError(err)
   155  				return
   156  			}
   157  			chunk.Close()
   158  			pw.Close()
   159  		}
   160  	}()
   161  
   162  	return snapshot, ch, nil
   163  }
   164  
   165  // LoadChunk loads a chunk from disk, or returns nil if it does not exist. The caller must call
   166  // Close() on it when done.
   167  func (s *Store) LoadChunk(height uint64, format uint32, chunk uint32) (io.ReadCloser, error) {
   168  	path := s.pathChunk(height, format, chunk)
   169  	file, err := os.Open(path)
   170  	if os.IsNotExist(err) {
   171  		return nil, nil
   172  	}
   173  	return file, err
   174  }
   175  
   176  // loadChunkFile loads a chunk from disk, and errors if it does not exist.
   177  func (s *Store) loadChunkFile(height uint64, format uint32, chunk uint32) (io.ReadCloser, error) {
   178  	path := s.pathChunk(height, format, chunk)
   179  	return os.Open(path)
   180  }
   181  
   182  // Prune removes old snapshots. The given number of most recent heights (regardless of format) are retained.
   183  func (s *Store) Prune(retain uint32) (uint64, error) {
   184  	iter, err := s.db.ReverseIterator(encodeKey(0, 0), encodeKey(uint64(math.MaxUint64), math.MaxUint32))
   185  	if err != nil {
   186  		return 0, sdkerrors.Wrap(err, "failed to prune snapshots")
   187  	}
   188  	defer iter.Close()
   189  
   190  	pruned := uint64(0)
   191  	prunedHeights := make(map[uint64]bool)
   192  	skip := make(map[uint64]bool)
   193  	for ; iter.Valid(); iter.Next() {
   194  		height, format, err := decodeKey(iter.Key())
   195  		if err != nil {
   196  			return 0, sdkerrors.Wrap(err, "failed to prune snapshots")
   197  		}
   198  		if skip[height] || uint32(len(skip)) < retain {
   199  			skip[height] = true
   200  			continue
   201  		}
   202  		err = s.Delete(height, format)
   203  		if err != nil {
   204  			return 0, sdkerrors.Wrap(err, "failed to prune snapshots")
   205  		}
   206  		pruned++
   207  		prunedHeights[height] = true
   208  	}
   209  	// Since Delete() deletes a specific format, while we want to prune a height, we clean up
   210  	// the height directory as well
   211  	for height, ok := range prunedHeights {
   212  		if ok {
   213  			err = os.Remove(s.pathHeight(height))
   214  			if err != nil {
   215  				return 0, sdkerrors.Wrapf(err, "failed to remove snapshot directory for height %v", height)
   216  			}
   217  		}
   218  	}
   219  	return pruned, iter.Error()
   220  }
   221  
   222  // Save saves a snapshot to disk, returning it.
   223  func (s *Store) Save(
   224  	height uint64, format uint32, chunks <-chan io.ReadCloser,
   225  ) (*types.Snapshot, error) {
   226  	defer DrainChunks(chunks)
   227  	if height == 0 {
   228  		return nil, sdkerrors.Wrap(sdkerrors.ErrLogic, "snapshot height cannot be 0")
   229  	}
   230  
   231  	s.mtx.Lock()
   232  	saving := s.saving[height]
   233  	s.saving[height] = true
   234  	s.mtx.Unlock()
   235  	if saving {
   236  		return nil, sdkerrors.Wrapf(sdkerrors.ErrConflict,
   237  			"a snapshot for height %v is already being saved", height)
   238  	}
   239  	defer func() {
   240  		s.mtx.Lock()
   241  		delete(s.saving, height)
   242  		s.mtx.Unlock()
   243  	}()
   244  
   245  	exists, err := s.db.Has(encodeKey(height, format))
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  	if exists {
   250  		return nil, sdkerrors.Wrapf(sdkerrors.ErrConflict,
   251  			"snapshot already exists for height %v format %v", height, format)
   252  	}
   253  
   254  	snapshot := &types.Snapshot{
   255  		Height: height,
   256  		Format: format,
   257  	}
   258  	index := uint32(0)
   259  	snapshotHasher := sha256.New()
   260  	chunkHasher := sha256.New()
   261  	for chunkBody := range chunks {
   262  		defer chunkBody.Close() //nolint: staticcheck
   263  		dir := s.pathSnapshot(height, format)
   264  		err = os.MkdirAll(dir, 0o755)
   265  		if err != nil {
   266  			return nil, sdkerrors.Wrapf(err, "failed to create snapshot directory %q", dir)
   267  		}
   268  		path := s.pathChunk(height, format, index)
   269  		file, err := os.Create(path)
   270  		if err != nil {
   271  			return nil, sdkerrors.Wrapf(err, "failed to create snapshot chunk file %q", path)
   272  		}
   273  		defer file.Close() //nolint: staticcheck
   274  
   275  		chunkHasher.Reset()
   276  		_, err = io.Copy(io.MultiWriter(file, chunkHasher, snapshotHasher), chunkBody)
   277  		if err != nil {
   278  			return nil, sdkerrors.Wrapf(err, "failed to generate snapshot chunk %v", index)
   279  		}
   280  		err = file.Close()
   281  		if err != nil {
   282  			return nil, sdkerrors.Wrapf(err, "failed to close snapshot chunk %v", index)
   283  		}
   284  		err = chunkBody.Close()
   285  		if err != nil {
   286  			return nil, sdkerrors.Wrapf(err, "failed to close snapshot chunk %v", index)
   287  		}
   288  		snapshot.Metadata.ChunkHashes = append(snapshot.Metadata.ChunkHashes, chunkHasher.Sum(nil))
   289  		index++
   290  	}
   291  	snapshot.Chunks = index
   292  	snapshot.Hash = snapshotHasher.Sum(nil)
   293  	return snapshot, s.saveSnapshot(snapshot)
   294  }
   295  
   296  // saveSnapshot saves snapshot metadata to the database.
   297  func (s *Store) saveSnapshot(snapshot *types.Snapshot) error {
   298  	value, err := proto.Marshal(snapshot)
   299  	if err != nil {
   300  		return sdkerrors.Wrap(err, "failed to encode snapshot metadata")
   301  	}
   302  	err = s.db.SetSync(encodeKey(snapshot.Height, snapshot.Format), value)
   303  	return sdkerrors.Wrap(err, "failed to store snapshot")
   304  }
   305  
   306  // pathHeight generates the path to a height, containing multiple snapshot formats.
   307  func (s *Store) pathHeight(height uint64) string {
   308  	return filepath.Join(s.dir, strconv.FormatUint(height, 10))
   309  }
   310  
   311  // pathSnapshot generates a snapshot path, as a specific format under a height.
   312  func (s *Store) pathSnapshot(height uint64, format uint32) string {
   313  	return filepath.Join(s.pathHeight(height), strconv.FormatUint(uint64(format), 10))
   314  }
   315  
   316  // pathChunk generates a snapshot chunk path.
   317  func (s *Store) pathChunk(height uint64, format uint32, chunk uint32) string {
   318  	return filepath.Join(s.pathSnapshot(height, format), strconv.FormatUint(uint64(chunk), 10))
   319  }
   320  
   321  // decodeKey decodes a snapshot key.
   322  func decodeKey(k []byte) (uint64, uint32, error) {
   323  	if len(k) != 13 {
   324  		return 0, 0, sdkerrors.Wrapf(sdkerrors.ErrLogic, "invalid snapshot key with length %v", len(k))
   325  	}
   326  	if k[0] != keyPrefixSnapshot {
   327  		return 0, 0, sdkerrors.Wrapf(sdkerrors.ErrLogic, "invalid snapshot key prefix %x", k[0])
   328  	}
   329  	height := binary.BigEndian.Uint64(k[1:9])
   330  	format := binary.BigEndian.Uint32(k[9:13])
   331  	return height, format, nil
   332  }
   333  
   334  // encodeKey encodes a snapshot key.
   335  func encodeKey(height uint64, format uint32) []byte {
   336  	k := make([]byte, 13)
   337  	k[0] = keyPrefixSnapshot
   338  	binary.BigEndian.PutUint64(k[1:], height)
   339  	binary.BigEndian.PutUint32(k[9:], format)
   340  	return k
   341  }