/
store.go
249 lines (209 loc) · 7.03 KB
/
store.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
package nodebuilder
import (
"errors"
"fmt"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/dgraph-io/badger/v4/options"
"github.com/gofrs/flock"
"github.com/ipfs/go-datastore"
dsbadger "github.com/ipfs/go-ds-badger4"
"github.com/mitchellh/go-homedir"
"github.com/celestiaorg/celestia-node/libs/keystore"
"github.com/celestiaorg/celestia-node/share"
)
var (
// ErrOpened is thrown on attempt to open already open/in-use Store.
ErrOpened = errors.New("node: store is in use")
// ErrNotInited is thrown on attempt to open Store without initialization.
ErrNotInited = errors.New("node: store is not initialized")
)
// Store encapsulates storage for the Node. Basically, it is the Store of all Stores.
// It provides access for the Node data stored in root directory e.g. '~/.celestia'.
type Store interface {
// Path reports the FileSystem path of Store.
Path() string
// Keystore provides a Keystore to access keys.
Keystore() (keystore.Keystore, error)
// Datastore provides a Datastore - a KV store for arbitrary data to be stored on disk.
Datastore() (datastore.Batching, error)
// Config loads the stored Node config.
Config() (*Config, error)
// PutConfig alters the stored Node config.
PutConfig(*Config) error
// Close closes the Store freeing up acquired resources and locks.
Close() error
}
// OpenStore creates new FS Store under the given 'path'.
// To be opened the Store must be initialized first, otherwise ErrNotInited is thrown.
// OpenStore takes a file Lock on directory, hence only one Store can be opened at a time under the
// given 'path', otherwise ErrOpened is thrown.
func OpenStore(path string, ring keyring.Keyring) (Store, error) {
path, err := storePath(path)
if err != nil {
return nil, err
}
flk := flock.New(lockPath(path))
ok, err := flk.TryLock()
if err != nil {
return nil, fmt.Errorf("locking file: %w", err)
}
if !ok {
return nil, ErrOpened
}
if !IsInit(path) {
err := errors.Join(ErrNotInited, flk.Unlock())
return nil, err
}
ks, err := keystore.NewFSKeystore(keysPath(path), ring)
if err != nil {
err = errors.Join(err, flk.Unlock())
return nil, err
}
return &fsStore{
path: path,
dirLock: flk,
keys: ks,
}, nil
}
func (f *fsStore) Path() string {
return f.path
}
func (f *fsStore) Config() (*Config, error) {
cfg, err := LoadConfig(configPath(f.path))
if err != nil {
return nil, fmt.Errorf("node: can't load Config: %w", err)
}
return cfg, nil
}
func (f *fsStore) PutConfig(cfg *Config) error {
err := SaveConfig(configPath(f.path), cfg)
if err != nil {
return fmt.Errorf("node: can't save Config: %w", err)
}
return nil
}
func (f *fsStore) Keystore() (_ keystore.Keystore, err error) {
if f.keys == nil {
return nil, fmt.Errorf("node: no Keystore found")
}
return f.keys, nil
}
func (f *fsStore) Datastore() (datastore.Batching, error) {
f.dataMu.Lock()
defer f.dataMu.Unlock()
if f.data != nil {
return f.data, nil
}
cfg := constraintBadgerConfig()
ds, err := dsbadger.NewDatastore(dataPath(f.path), cfg)
if err != nil {
return nil, fmt.Errorf("node: can't open Badger Datastore: %w", err)
}
f.data = ds
return ds, nil
}
func (f *fsStore) Close() (err error) {
err = errors.Join(err, f.dirLock.Close())
f.dataMu.Lock()
if f.data != nil {
err = errors.Join(err, f.data.Close())
}
f.dataMu.Unlock()
return
}
type fsStore struct {
path string
dataMu sync.Mutex
data datastore.Batching
keys keystore.Keystore
dirLock *flock.Flock // protects directory
}
func storePath(path string) (string, error) {
return homedir.Expand(filepath.Clean(path))
}
func configPath(base string) string {
return filepath.Join(base, "config.toml")
}
func lockPath(base string) string {
return filepath.Join(base, ".lock")
}
func keysPath(base string) string {
return filepath.Join(base, "keys")
}
func blocksPath(base string) string {
return filepath.Join(base, "blocks")
}
func transientsPath(base string) string {
// we don't actually use the transients directory anymore, but it could be populated from previous
// versions.
return filepath.Join(base, "transients")
}
func indexPath(base string) string {
return filepath.Join(base, "index")
}
func dataPath(base string) string {
return filepath.Join(base, "data")
}
// constraintBadgerConfig returns BadgerDB configuration optimized for low memory usage and more frequent
// compaction which prevents memory spikes.
// This is particularly important for LNs with restricted memory resources.
//
// With the following configuration, a LN uses up to 300iB of RAM during initial sync/sampling
// and up to 200MiB during normal operation. (on 4 core CPU, 8GiB RAM droplet)
//
// With the following configuration and "-tags=jemalloc", a LN uses no more than 180MiB during initial
// sync/sampling and up to 100MiB during normal operation. (same hardware spec)
// NOTE: To enable jemalloc, build celestia-node with "-tags=jemalloc" flag, which configures Badger to
// use jemalloc instead of Go's default allocator.
//
// TODO(@Wondertan): Consider alternative less constraint configuration for FN/BN
// TODO(@Wondertan): Consider dynamic memory allocation based on available RAM
func constraintBadgerConfig() *dsbadger.Options {
opts := dsbadger.DefaultOptions // this must be copied
// ValueLog:
// 2mib default => share.Size - makes sure headers and samples are stored in value log
// This *tremendously* reduces the amount of memory used by the node, up to 10 times less during
// compaction
opts.ValueThreshold = share.Size
// make sure we don't have any limits for stored headers
opts.ValueLogMaxEntries = 100000000
// run value log GC more often to spread the work over time
opts.GcInterval = time.Minute * 1
// default 0.5 => 0.125 - makes sure value log GC is more aggressive on reclaiming disk space
opts.GcDiscardRatio = 0.125
// badger stores checksum for every value, but doesn't verify it by default
// enabling this option may allow us to see detect corrupted data
opts.ChecksumVerificationMode = options.OnBlockRead
opts.VerifyValueChecksum = true
// default 64mib => 0 - disable block cache
// most of our component maintain their own caches, so this is not needed
opts.BlockCacheSize = 0
// not much gain as it compresses the LSM only as well compression requires block cache
opts.Compression = options.None
// MemTables:
// default 64mib => 16mib - decreases memory usage and makes compaction more often
opts.MemTableSize = 16 << 20
// default 5 => 3
opts.NumMemtables = 3
// default 5 => 3
opts.NumLevelZeroTables = 3
// default 15 => 5 - this prevents memory growth on CPU constraint systems by blocking all writers
opts.NumLevelZeroTablesStall = 5
// Compaction:
// Dynamic compactor allocation
compactors := runtime.NumCPU() / 2
if compactors < 2 {
compactors = 2 // can't be less than 2
}
if compactors > opts.MaxLevels { // ensure there is no more compactors than db table levels
compactors = opts.MaxLevels
}
opts.NumCompactors = compactors
// makes sure badger is always compacted on shutdown
opts.CompactL0OnClose = true
return &opts
}