You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
4.4 KiB

  1. // nolint: gosec
  2. package app
  3. import (
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "math"
  8. "os"
  9. "path/filepath"
  10. "sync"
  11. abci "github.com/tendermint/tendermint/abci/types"
  12. )
  13. const (
  14. snapshotChunkSize = 1e6
  15. // Keep only the most recent 10 snapshots. Older snapshots are pruned
  16. maxSnapshotCount = 10
  17. )
  18. // SnapshotStore stores state sync snapshots. Snapshots are stored simply as
  19. // JSON files, and chunks are generated on-the-fly by splitting the JSON data
  20. // into fixed-size chunks.
  21. type SnapshotStore struct {
  22. sync.RWMutex
  23. dir string
  24. metadata []abci.Snapshot
  25. }
  26. // NewSnapshotStore creates a new snapshot store.
  27. func NewSnapshotStore(dir string) (*SnapshotStore, error) {
  28. store := &SnapshotStore{dir: dir}
  29. if err := os.MkdirAll(dir, 0755); err != nil {
  30. return nil, err
  31. }
  32. if err := store.loadMetadata(); err != nil {
  33. return nil, err
  34. }
  35. return store, nil
  36. }
  37. // loadMetadata loads snapshot metadata. Does not take out locks, since it's
  38. // called internally on construction.
  39. func (s *SnapshotStore) loadMetadata() error {
  40. file := filepath.Join(s.dir, "metadata.json")
  41. metadata := []abci.Snapshot{}
  42. bz, err := os.ReadFile(file)
  43. switch {
  44. case errors.Is(err, os.ErrNotExist):
  45. case err != nil:
  46. return fmt.Errorf("failed to load snapshot metadata from %q: %w", file, err)
  47. }
  48. if len(bz) != 0 {
  49. err = json.Unmarshal(bz, &metadata)
  50. if err != nil {
  51. return fmt.Errorf("invalid snapshot data in %q: %w", file, err)
  52. }
  53. }
  54. s.metadata = metadata
  55. return nil
  56. }
  57. // saveMetadata saves snapshot metadata. Does not take out locks, since it's
  58. // called internally from e.g. Create().
  59. func (s *SnapshotStore) saveMetadata() error {
  60. bz, err := json.Marshal(s.metadata)
  61. if err != nil {
  62. return err
  63. }
  64. // save the file to a new file and move it to make saving atomic.
  65. newFile := filepath.Join(s.dir, "metadata.json.new")
  66. file := filepath.Join(s.dir, "metadata.json")
  67. err = os.WriteFile(newFile, bz, 0644) // nolint: gosec
  68. if err != nil {
  69. return err
  70. }
  71. return os.Rename(newFile, file)
  72. }
  73. // Create creates a snapshot of the given application state's key/value pairs.
  74. func (s *SnapshotStore) Create(state *State) (abci.Snapshot, error) {
  75. s.Lock()
  76. defer s.Unlock()
  77. bz, err := state.Export()
  78. if err != nil {
  79. return abci.Snapshot{}, err
  80. }
  81. snapshot := abci.Snapshot{
  82. Height: state.Height,
  83. Format: 1,
  84. Hash: hashItems(state.Values),
  85. Chunks: byteChunks(bz),
  86. }
  87. err = os.WriteFile(filepath.Join(s.dir, fmt.Sprintf("%v.json", state.Height)), bz, 0644)
  88. if err != nil {
  89. return abci.Snapshot{}, err
  90. }
  91. s.metadata = append(s.metadata, snapshot)
  92. err = s.saveMetadata()
  93. if err != nil {
  94. return abci.Snapshot{}, err
  95. }
  96. return snapshot, nil
  97. }
  98. // Prune removes old snapshots ensuring only the most recent n snapshots remain
  99. func (s *SnapshotStore) Prune(n int) error {
  100. s.Lock()
  101. defer s.Unlock()
  102. // snapshots are appended to the metadata struct, hence pruning removes from
  103. // the front of the array
  104. i := 0
  105. for ; i < len(s.metadata)-n; i++ {
  106. h := s.metadata[i].Height
  107. if err := os.Remove(filepath.Join(s.dir, fmt.Sprintf("%v.json", h))); err != nil {
  108. return err
  109. }
  110. }
  111. // update metadata by removing the deleted snapshots
  112. pruned := make([]abci.Snapshot, len(s.metadata[i:]))
  113. copy(pruned, s.metadata[i:])
  114. s.metadata = pruned
  115. return nil
  116. }
  117. // List lists available snapshots.
  118. func (s *SnapshotStore) List() ([]*abci.Snapshot, error) {
  119. s.RLock()
  120. defer s.RUnlock()
  121. snapshots := make([]*abci.Snapshot, len(s.metadata))
  122. for idx := range s.metadata {
  123. snapshots[idx] = &s.metadata[idx]
  124. }
  125. return snapshots, nil
  126. }
  127. // LoadChunk loads a snapshot chunk.
  128. func (s *SnapshotStore) LoadChunk(height uint64, format uint32, chunk uint32) ([]byte, error) {
  129. s.RLock()
  130. defer s.RUnlock()
  131. for _, snapshot := range s.metadata {
  132. if snapshot.Height == height && snapshot.Format == format {
  133. bz, err := os.ReadFile(filepath.Join(s.dir, fmt.Sprintf("%v.json", height)))
  134. if err != nil {
  135. return nil, err
  136. }
  137. return byteChunk(bz, chunk), nil
  138. }
  139. }
  140. return nil, nil
  141. }
  142. // byteChunk returns the chunk at a given index from the full byte slice.
  143. func byteChunk(bz []byte, index uint32) []byte {
  144. start := int(index * snapshotChunkSize)
  145. end := int((index + 1) * snapshotChunkSize)
  146. switch {
  147. case start >= len(bz):
  148. return nil
  149. case end >= len(bz):
  150. return bz[start:]
  151. default:
  152. return bz[start:end]
  153. }
  154. }
  155. // byteChunks calculates the number of chunks in the byte slice.
  156. func byteChunks(bz []byte) uint32 {
  157. return uint32(math.Ceil(float64(len(bz)) / snapshotChunkSize))
  158. }