github.com/decred/dcrlnd@v0.7.6/kvdb/backend.go (about) 1 //go:build !js 2 // +build !js 3 4 package kvdb 5 6 import ( 7 "context" 8 "crypto/sha256" 9 "encoding/binary" 10 "encoding/hex" 11 "fmt" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "time" 16 17 _ "github.com/btcsuite/btcwallet/walletdb/bdb" // Import to register backend. 18 ) 19 20 const ( 21 // DefaultTempDBFileName is the default name of the temporary bolt DB 22 // file that we'll use to atomically compact the primary DB file on 23 // startup. 24 DefaultTempDBFileName = "temp-dont-use.db" 25 26 // LastCompactionFileNameSuffix is the suffix we append to the file name 27 // of a database file to record the timestamp when the last compaction 28 // occurred. 29 LastCompactionFileNameSuffix = ".last-compacted" 30 ) 31 32 var ( 33 byteOrder = binary.BigEndian 34 ) 35 36 // fileExists returns true if the file exists, and false otherwise. 37 func fileExists(path string) bool { 38 if _, err := os.Stat(path); err != nil { 39 if os.IsNotExist(err) { 40 return false 41 } 42 } 43 44 return true 45 } 46 47 // BoltBackendConfig is a struct that holds settings specific to the bolt 48 // database backend. 49 type BoltBackendConfig struct { 50 // DBPath is the directory path in which the database file should be 51 // stored. 52 DBPath string 53 54 // DBFileName is the name of the database file. 55 DBFileName string 56 57 // NoFreelistSync, if true, prevents the database from syncing its 58 // freelist to disk, resulting in improved performance at the expense of 59 // increased startup time. 60 NoFreelistSync bool 61 62 // AutoCompact specifies if a Bolt based database backend should be 63 // automatically compacted on startup (if the minimum age of the 64 // database file is reached). This will require additional disk space 65 // for the compacted copy of the database but will result in an overall 66 // lower database size after the compaction. 67 AutoCompact bool 68 69 // AutoCompactMinAge specifies the minimum time that must have passed 70 // since a bolt database file was last compacted for the compaction to 71 // be considered again. 72 AutoCompactMinAge time.Duration 73 74 // DBTimeout specifies the timeout value to use when opening the wallet 75 // database. 76 DBTimeout time.Duration 77 } 78 79 // GetBoltBackend opens (or creates if doesn't exits) a bbolt backed database 80 // and returns a kvdb.Backend wrapping it. 81 func GetBoltBackend(cfg *BoltBackendConfig) (Backend, error) { 82 dbFilePath := filepath.Join(cfg.DBPath, cfg.DBFileName) 83 84 // Is this a new database? 85 if !fileExists(dbFilePath) { 86 if !fileExists(cfg.DBPath) { 87 if err := os.MkdirAll(cfg.DBPath, 0700); err != nil { 88 return nil, err 89 } 90 } 91 92 return Create( 93 BoltBackendName, dbFilePath, 94 cfg.NoFreelistSync, cfg.DBTimeout, 95 ) 96 } 97 98 // This is an existing database. We might want to compact it on startup 99 // to free up some space. 100 if cfg.AutoCompact { 101 if err := compactAndSwap(cfg); err != nil { 102 return nil, err 103 } 104 } 105 106 return Open( 107 BoltBackendName, dbFilePath, 108 cfg.NoFreelistSync, cfg.DBTimeout, 109 ) 110 } 111 112 // compactAndSwap will attempt to write a new temporary DB file to disk with 113 // the compacted database content, then atomically swap (via rename) the old 114 // file for the new file by updating the name of the new file to the old. 115 func compactAndSwap(cfg *BoltBackendConfig) error { 116 sourceName := cfg.DBFileName 117 118 // If the main DB file isn't set, then we can't proceed. 119 if sourceName == "" { 120 return fmt.Errorf("cannot compact DB with empty name") 121 } 122 sourceFilePath := filepath.Join(cfg.DBPath, sourceName) 123 tempDestFilePath := filepath.Join(cfg.DBPath, DefaultTempDBFileName) 124 125 // Let's find out how long ago the last compaction of the source file 126 // occurred and possibly skip compacting it again now. 127 lastCompactionDate, err := lastCompactionDate(sourceFilePath) 128 if err != nil { 129 return fmt.Errorf("cannot determine last compaction date of "+ 130 "source DB file: %v", err) 131 } 132 compactAge := time.Since(lastCompactionDate) 133 if cfg.AutoCompactMinAge != 0 && compactAge <= cfg.AutoCompactMinAge { 134 log.Infof("Not compacting database file at %v, it was last "+ 135 "compacted at %v (%v ago), min age is set to %v", 136 sourceFilePath, lastCompactionDate, 137 compactAge.Truncate(time.Second), cfg.AutoCompactMinAge) 138 return nil 139 } 140 141 log.Infof("Compacting database file at %v", sourceFilePath) 142 143 // If the old temporary DB file still exists, then we'll delete it 144 // before proceeding. 145 if _, err := os.Stat(tempDestFilePath); err == nil { 146 log.Infof("Found old temp DB @ %v, removing before swap", 147 tempDestFilePath) 148 149 err = os.Remove(tempDestFilePath) 150 if err != nil { 151 return fmt.Errorf("unable to remove old temp DB file: "+ 152 "%v", err) 153 } 154 } 155 156 // Now that we know the staging area is clear, we'll create the new 157 // temporary DB file and close it before we write the new DB to it. 158 tempFile, err := os.Create(tempDestFilePath) 159 if err != nil { 160 return fmt.Errorf("unable to create temp DB file: %v", err) 161 } 162 if err := tempFile.Close(); err != nil { 163 return fmt.Errorf("unable to close file: %v", err) 164 } 165 166 // With the file created, we'll start the compaction and remove the 167 // temporary file all together once this method exits. 168 defer func() { 169 // This will only succeed if the rename below fails. If the 170 // compaction is successful, the file won't exist on exit 171 // anymore so no need to log an error here. 172 _ = os.Remove(tempDestFilePath) 173 }() 174 c := &compacter{ 175 srcPath: sourceFilePath, 176 dstPath: tempDestFilePath, 177 dbTimeout: cfg.DBTimeout, 178 } 179 initialSize, newSize, err := c.execute() 180 if err != nil { 181 return fmt.Errorf("error during compact: %v", err) 182 } 183 184 log.Infof("DB compaction of %v successful, %d -> %d bytes (gain=%.2fx)", 185 sourceFilePath, initialSize, newSize, 186 float64(initialSize)/float64(newSize)) 187 188 // We try to store the current timestamp in a file with the suffix 189 // .last-compacted so we can figure out how long ago the last compaction 190 // was. But since this shouldn't fail the compaction process itself, we 191 // only log the error. Worst case if this file cannot be written is that 192 // we compact on every startup. 193 err = updateLastCompactionDate(sourceFilePath) 194 if err != nil { 195 log.Warnf("Could not update last compaction timestamp in "+ 196 "%s%s: %v", sourceFilePath, 197 LastCompactionFileNameSuffix, err) 198 } 199 200 log.Infof("Swapping old DB file from %v to %v", tempDestFilePath, 201 sourceFilePath) 202 203 // Finally, we'll attempt to atomically rename the temporary file to 204 // the main back up file. If this succeeds, then we'll only have a 205 // single file on disk once this method exits. 206 return os.Rename(tempDestFilePath, sourceFilePath) 207 } 208 209 // lastCompactionDate returns the date the given database file was last 210 // compacted or a zero time.Time if no compaction was recorded before. The 211 // compaction date is read from a file in the same directory and with the same 212 // name as the DB file, but with the suffix ".last-compacted". 213 func lastCompactionDate(dbFile string) (time.Time, error) { 214 zeroTime := time.Unix(0, 0) 215 216 tsFile := fmt.Sprintf("%s%s", dbFile, LastCompactionFileNameSuffix) 217 if !fileExists(tsFile) { 218 return zeroTime, nil 219 } 220 221 tsBytes, err := ioutil.ReadFile(tsFile) 222 if err != nil { 223 return zeroTime, err 224 } 225 226 tsNano := byteOrder.Uint64(tsBytes) 227 return time.Unix(0, int64(tsNano)), nil 228 } 229 230 // updateLastCompactionDate stores the current time as a timestamp in a file 231 // in the same directory and with the same name as the DB file, but with the 232 // suffix ".last-compacted". 233 func updateLastCompactionDate(dbFile string) error { 234 var tsBytes [8]byte 235 byteOrder.PutUint64(tsBytes[:], uint64(time.Now().UnixNano())) 236 237 tsFile := fmt.Sprintf("%s%s", dbFile, LastCompactionFileNameSuffix) 238 return ioutil.WriteFile(tsFile, tsBytes[:], 0600) 239 } 240 241 // GetTestBackend opens (or creates if doesn't exist) a bbolt or etcd 242 // backed database (for testing), and returns a kvdb.Backend and a cleanup 243 // func. Whether to create/open bbolt or embedded etcd database is based 244 // on the TestBackend constant which is conditionally compiled with build tag. 245 // The passed path is used to hold all db files, while the name is only used 246 // for bolt. 247 func GetTestBackend(path, name string) (Backend, func(), error) { 248 empty := func() {} 249 250 switch { 251 case PostgresBackend: 252 key := filepath.Join(path, name) 253 keyHash := sha256.Sum256([]byte(key)) 254 255 f, err := NewPostgresFixture("test_" + hex.EncodeToString(keyHash[:])) 256 if err != nil { 257 return nil, func() {}, err 258 } 259 return f.DB(), func() { 260 _ = f.DB().Close() 261 }, nil 262 263 case TestBackend == BoltBackendName: 264 db, err := GetBoltBackend(&BoltBackendConfig{ 265 DBPath: path, 266 DBFileName: name, 267 NoFreelistSync: true, 268 DBTimeout: DefaultDBTimeout, 269 }) 270 if err != nil { 271 return nil, nil, err 272 } 273 return db, empty, nil 274 275 case TestBackend == EtcdBackendName: 276 etcdConfig, cancel, err := StartEtcdTestBackend(path, 0, 0, "") 277 if err != nil { 278 return nil, empty, err 279 } 280 backend, err := Open( 281 EtcdBackendName, context.TODO(), etcdConfig, 282 ) 283 return backend, cancel, err 284 285 } 286 287 return nil, nil, fmt.Errorf("unknown backend") 288 }