mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 14:40:30 +01:00
This commit adds the compaction feature of the bbolt compact to our bolt backend. This will try to open the DB in read-only mode and create a compacted copy in a temporary file. Once the compaction was successful, the temporary file and the old source file are swapped atomically.
177 lines
5.5 KiB
Go
177 lines
5.5 KiB
Go
package kvdb
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
_ "github.com/btcsuite/btcwallet/walletdb/bdb" // Import to register backend.
|
|
)
|
|
|
|
const (
|
|
// DefaultTempDBFileName is the default name of the temporary bolt DB
|
|
// file that we'll use to atomically compact the primary DB file on
|
|
// startup.
|
|
DefaultTempDBFileName = "temp-dont-use.db"
|
|
)
|
|
|
|
// fileExists returns true if the file exists, and false otherwise.
|
|
func fileExists(path string) bool {
|
|
if _, err := os.Stat(path); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// BoltBackendConfig is a struct that holds settings specific to the bolt
|
|
// database backend.
|
|
type BoltBackendConfig struct {
|
|
// DBPath is the directory path in which the database file should be
|
|
// stored.
|
|
DBPath string
|
|
|
|
// DBFileName is the name of the database file.
|
|
DBFileName string
|
|
|
|
// NoFreelistSync, if true, prevents the database from syncing its
|
|
// freelist to disk, resulting in improved performance at the expense of
|
|
// increased startup time.
|
|
NoFreelistSync bool
|
|
|
|
// AutoCompact specifies if a Bolt based database backend should be
|
|
// automatically compacted on startup (if the minimum age of the
|
|
// database file is reached). This will require additional disk space
|
|
// for the compacted copy of the database but will result in an overall
|
|
// lower database size after the compaction.
|
|
AutoCompact bool
|
|
|
|
// AutoCompactMinAge specifies the minimum time that must have passed
|
|
// since a bolt database file was last compacted for the compaction to
|
|
// be considered again.
|
|
AutoCompactMinAge time.Duration
|
|
}
|
|
|
|
// GetBoltBackend opens (or creates if doesn't exits) a bbolt backed database
|
|
// and returns a kvdb.Backend wrapping it.
|
|
func GetBoltBackend(cfg *BoltBackendConfig) (Backend, error) {
|
|
dbFilePath := filepath.Join(cfg.DBPath, cfg.DBFileName)
|
|
|
|
// Is this a new database?
|
|
if !fileExists(dbFilePath) {
|
|
if !fileExists(cfg.DBPath) {
|
|
if err := os.MkdirAll(cfg.DBPath, 0700); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return Create(BoltBackendName, dbFilePath, cfg.NoFreelistSync)
|
|
}
|
|
|
|
// This is an existing database. We might want to compact it on startup
|
|
// to free up some space.
|
|
if cfg.AutoCompact {
|
|
if err := compactAndSwap(cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return Open(BoltBackendName, dbFilePath, cfg.NoFreelistSync)
|
|
}
|
|
|
|
// compactAndSwap will attempt to write a new temporary DB file to disk with
|
|
// the compacted database content, then atomically swap (via rename) the old
|
|
// file for the new file by updating the name of the new file to the old.
|
|
func compactAndSwap(cfg *BoltBackendConfig) error {
|
|
sourceName := cfg.DBFileName
|
|
|
|
// If the main DB file isn't set, then we can't proceed.
|
|
if sourceName == "" {
|
|
return fmt.Errorf("cannot compact DB with empty name")
|
|
}
|
|
sourceFilePath := filepath.Join(cfg.DBPath, sourceName)
|
|
tempDestFilePath := filepath.Join(cfg.DBPath, DefaultTempDBFileName)
|
|
|
|
log.Infof("Compacting database file at %v", sourceFilePath)
|
|
|
|
// If the old temporary DB file still exists, then we'll delete it
|
|
// before proceeding.
|
|
if _, err := os.Stat(tempDestFilePath); err == nil {
|
|
log.Infof("Found old temp DB @ %v, removing before swap",
|
|
tempDestFilePath)
|
|
|
|
err = os.Remove(tempDestFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to remove old temp DB file: "+
|
|
"%v", err)
|
|
}
|
|
}
|
|
|
|
// Now that we know the staging area is clear, we'll create the new
|
|
// temporary DB file and close it before we write the new DB to it.
|
|
tempFile, err := os.Create(tempDestFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create temp DB file: %v", err)
|
|
}
|
|
if err := tempFile.Close(); err != nil {
|
|
return fmt.Errorf("unable to close file: %v", err)
|
|
}
|
|
|
|
// With the file created, we'll start the compaction and remove the
|
|
// temporary file all together once this method exits.
|
|
defer func() {
|
|
// This will only succeed if the rename below fails. If the
|
|
// compaction is successful, the file won't exist on exit
|
|
// anymore so no need to log an error here.
|
|
_ = os.Remove(tempDestFilePath)
|
|
}()
|
|
c := &compacter{
|
|
srcPath: sourceFilePath,
|
|
dstPath: tempDestFilePath,
|
|
}
|
|
initialSize, newSize, err := c.execute()
|
|
if err != nil {
|
|
return fmt.Errorf("error during compact: %v", err)
|
|
}
|
|
|
|
log.Infof("DB compaction of %v successful, %d -> %d bytes (gain=%.2fx)",
|
|
sourceFilePath, initialSize, newSize,
|
|
float64(initialSize)/float64(newSize))
|
|
|
|
log.Infof("Swapping old DB file from %v to %v", tempDestFilePath,
|
|
sourceFilePath)
|
|
|
|
// Finally, we'll attempt to atomically rename the temporary file to
|
|
// the main back up file. If this succeeds, then we'll only have a
|
|
// single file on disk once this method exits.
|
|
return os.Rename(tempDestFilePath, sourceFilePath)
|
|
}
|
|
|
|
// GetTestBackend opens (or creates if doesn't exist) a bbolt or etcd
|
|
// backed database (for testing), and returns a kvdb.Backend and a cleanup
|
|
// func. Whether to create/open bbolt or embedded etcd database is based
|
|
// on the TestBackend constant which is conditionally compiled with build tag.
|
|
// The passed path is used to hold all db files, while the name is only used
|
|
// for bbolt.
|
|
func GetTestBackend(path, name string) (Backend, func(), error) {
|
|
empty := func() {}
|
|
|
|
if TestBackend == BoltBackendName {
|
|
db, err := GetBoltBackend(&BoltBackendConfig{
|
|
DBPath: path,
|
|
DBFileName: name,
|
|
NoFreelistSync: true,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return db, empty, nil
|
|
} else if TestBackend == EtcdBackendName {
|
|
return GetEtcdTestBackend(path, name)
|
|
}
|
|
|
|
return nil, nil, fmt.Errorf("unknown backend")
|
|
}
|