Add framework for database upgrades. (#242)
Plus a few other miscellaneous pieces which will be usedful soon: - Remove `Get` from func names `GetCookieSecret` and `GetFeeXPub`. - Add helpers to encode/decode integers/bytes.
This commit is contained in:
parent
391e436a71
commit
5f8ad656f7
@ -5,8 +5,6 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,7 +20,7 @@ func (vdb *VspDatabase) GetLastAddressIndex() (uint32, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
idx = binary.LittleEndian.Uint32(idxBytes)
|
idx = bytesToUint32(idxBytes)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -35,11 +33,7 @@ func (vdb *VspDatabase) GetLastAddressIndex() (uint32, error) {
|
|||||||
func (vdb *VspDatabase) SetLastAddressIndex(idx uint32) error {
|
func (vdb *VspDatabase) SetLastAddressIndex(idx uint32) error {
|
||||||
err := vdb.db.Update(func(tx *bolt.Tx) error {
|
err := vdb.db.Update(func(tx *bolt.Tx) error {
|
||||||
vspBkt := tx.Bucket(vspBktK)
|
vspBkt := tx.Bucket(vspBktK)
|
||||||
|
return vspBkt.Put(lastAddressIndexK, uint32ToBytes(idx))
|
||||||
idxBytes := make([]byte, 4)
|
|
||||||
binary.LittleEndian.PutUint32(idxBytes, idx)
|
|
||||||
|
|
||||||
return vspBkt.Put(lastAddressIndexK, idxBytes)
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,16 @@ func writeHotBackupFile(db *bolt.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func uint32ToBytes(i uint32) []byte {
|
||||||
|
bytes := make([]byte, 4)
|
||||||
|
binary.LittleEndian.PutUint32(bytes, i)
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToUint32(bytes []byte) uint32 {
|
||||||
|
return binary.LittleEndian.Uint32(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateNew intializes a new bbolt database with all of the necessary vspd
|
// CreateNew intializes a new bbolt database with all of the necessary vspd
|
||||||
// buckets, and inserts:
|
// buckets, and inserts:
|
||||||
// - the provided extended pubkey (to be used for deriving fee addresses).
|
// - the provided extended pubkey (to be used for deriving fee addresses).
|
||||||
@ -108,10 +118,8 @@ func CreateNew(dbFile, feeXPub string) error {
|
|||||||
return fmt.Errorf("failed to create %s bucket: %w", string(vspBktK), err)
|
return fmt.Errorf("failed to create %s bucket: %w", string(vspBktK), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize with database version 1.
|
// Initialize with initial database version (1).
|
||||||
vbytes := make([]byte, 4)
|
err = vspBkt.Put(versionK, uint32ToBytes(initialVersion))
|
||||||
binary.LittleEndian.PutUint32(vbytes, uint32(1))
|
|
||||||
err = vspBkt.Put(versionK, vbytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -188,7 +196,19 @@ func Open(ctx context.Context, shutdownWg *sync.WaitGroup, dbFile string, backup
|
|||||||
return nil, fmt.Errorf("unable to open db file: %w", err)
|
return nil, fmt.Errorf("unable to open db file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Opened database file %s", dbFile)
|
vdb := &VspDatabase{db: db, maxVoteChangeRecords: maxVoteChangeRecords}
|
||||||
|
|
||||||
|
dbVersion, err := vdb.Version()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get db version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Opened database (version=%d, file=%s)", dbVersion, dbFile)
|
||||||
|
|
||||||
|
err = vdb.Upgrade(dbVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("database upgrade failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Start a ticker to update the backup file at the specified interval.
|
// Start a ticker to update the backup file at the specified interval.
|
||||||
shutdownWg.Add(1)
|
shutdownWg.Add(1)
|
||||||
@ -209,7 +229,7 @@ func Open(ctx context.Context, shutdownWg *sync.WaitGroup, dbFile string, backup
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return &VspDatabase{db: db, maxVoteChangeRecords: maxVoteChangeRecords}, nil
|
return vdb, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close will close the database and then make a copy of the database to the
|
// Close will close the database and then make a copy of the database to the
|
||||||
@ -305,9 +325,9 @@ func (vdb *VspDatabase) KeyPair() (ed25519.PrivateKey, ed25519.PublicKey, error)
|
|||||||
return signKey, pubKey, err
|
return signKey, pubKey, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFeeXPub retrieves the extended pubkey used for generating fee addresses
|
// FeeXPub retrieves the extended pubkey used for generating fee addresses
|
||||||
// from the database.
|
// from the database.
|
||||||
func (vdb *VspDatabase) GetFeeXPub() (string, error) {
|
func (vdb *VspDatabase) FeeXPub() (string, error) {
|
||||||
var feeXPub string
|
var feeXPub string
|
||||||
err := vdb.db.View(func(tx *bolt.Tx) error {
|
err := vdb.db.View(func(tx *bolt.Tx) error {
|
||||||
vspBkt := tx.Bucket(vspBktK)
|
vspBkt := tx.Bucket(vspBktK)
|
||||||
@ -325,9 +345,9 @@ func (vdb *VspDatabase) GetFeeXPub() (string, error) {
|
|||||||
return feeXPub, err
|
return feeXPub, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCookieSecret retrieves the generated cookie store secret key from the
|
// CookieSecret retrieves the generated cookie store secret key from the
|
||||||
// database.
|
// database.
|
||||||
func (vdb *VspDatabase) GetCookieSecret() ([]byte, error) {
|
func (vdb *VspDatabase) CookieSecret() ([]byte, error) {
|
||||||
var cookieSecret []byte
|
var cookieSecret []byte
|
||||||
err := vdb.db.View(func(tx *bolt.Tx) error {
|
err := vdb.db.View(func(tx *bolt.Tx) error {
|
||||||
vspBkt := tx.Bucket(vspBktK)
|
vspBkt := tx.Bucket(vspBktK)
|
||||||
@ -345,6 +365,21 @@ func (vdb *VspDatabase) GetCookieSecret() ([]byte, error) {
|
|||||||
return cookieSecret, err
|
return cookieSecret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Version returns the current database version.
|
||||||
|
func (vdb *VspDatabase) Version() (uint32, error) {
|
||||||
|
var version uint32
|
||||||
|
err := vdb.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bytes := tx.Bucket(vspBktK).Get(versionK)
|
||||||
|
version = bytesToUint32(bytes)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
// BackupDB streams a backup of the database over an http response writer.
|
// BackupDB streams a backup of the database over an http response writer.
|
||||||
func (vdb *VspDatabase) BackupDB(w http.ResponseWriter) error {
|
func (vdb *VspDatabase) BackupDB(w http.ResponseWriter) error {
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
|||||||
@ -90,7 +90,7 @@ func testCreateNew(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A newly created DB should have a cookie secret.
|
// A newly created DB should have a cookie secret.
|
||||||
secret, err := db.GetCookieSecret()
|
secret, err := db.CookieSecret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error getting cookie secret: %v", err)
|
t.Fatalf("error getting cookie secret: %v", err)
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ func testCreateNew(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A newly created DB should store the fee xpub it was initialized with.
|
// A newly created DB should store the fee xpub it was initialized with.
|
||||||
retrievedXPub, err := db.GetFeeXPub()
|
retrievedXPub, err := db.FeeXPub()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error getting fee xpub: %v", err)
|
t.Fatalf("error getting fee xpub: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
32
database/database_upgrades.go
Normal file
32
database/database_upgrades.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// initialVersion is the version of a freshly created database which has had
|
||||||
|
// no upgrades applied.
|
||||||
|
initialVersion = 1
|
||||||
|
|
||||||
|
// latestVersion is the latest version of the bolt database that is
|
||||||
|
// understood by vspd. Databases with recorded versions higher than
|
||||||
|
// this will fail to open (meaning any upgrades prevent reverting to older
|
||||||
|
// software).
|
||||||
|
latestVersion = initialVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
// Upgrade will update the database to the latest known version.
|
||||||
|
func (vdb *VspDatabase) Upgrade(currentVersion uint32) error {
|
||||||
|
if currentVersion == latestVersion {
|
||||||
|
// No upgrades required.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentVersion > latestVersion {
|
||||||
|
// Database is too new.
|
||||||
|
return fmt.Errorf("expected database version <= %d, got %d", latestVersion, currentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@ -44,7 +43,7 @@ func (vdb *VspDatabase) SaveVoteChange(ticketHash string, record VoteChangeRecor
|
|||||||
lowest := uint32(math.MaxUint32)
|
lowest := uint32(math.MaxUint32)
|
||||||
err = bkt.ForEach(func(k, v []byte) error {
|
err = bkt.ForEach(func(k, v []byte) error {
|
||||||
count++
|
count++
|
||||||
key := binary.LittleEndian.Uint32(k)
|
key := bytesToUint32(k)
|
||||||
if key > highest {
|
if key > highest {
|
||||||
highest = key
|
highest = key
|
||||||
}
|
}
|
||||||
@ -60,9 +59,7 @@ func (vdb *VspDatabase) SaveVoteChange(ticketHash string, record VoteChangeRecor
|
|||||||
// If bucket is at (or over) the limit of max allowed records, remove
|
// If bucket is at (or over) the limit of max allowed records, remove
|
||||||
// the oldest one.
|
// the oldest one.
|
||||||
if count >= vdb.maxVoteChangeRecords {
|
if count >= vdb.maxVoteChangeRecords {
|
||||||
keyBytes := make([]byte, 4)
|
err = bkt.Delete(uint32ToBytes(lowest))
|
||||||
binary.LittleEndian.PutUint32(keyBytes, lowest)
|
|
||||||
err = bkt.Delete(keyBytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete old vote change record: %w", err)
|
return fmt.Errorf("failed to delete old vote change record: %w", err)
|
||||||
}
|
}
|
||||||
@ -75,15 +72,12 @@ func (vdb *VspDatabase) SaveVoteChange(ticketHash string, record VoteChangeRecor
|
|||||||
newKey = highest + 1
|
newKey = highest + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
keyBytes := make([]byte, 4)
|
|
||||||
binary.LittleEndian.PutUint32(keyBytes, newKey)
|
|
||||||
|
|
||||||
// Insert record.
|
// Insert record.
|
||||||
recordBytes, err := json.Marshal(record)
|
recordBytes, err := json.Marshal(record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not marshal vote change record: %w", err)
|
return fmt.Errorf("could not marshal vote change record: %w", err)
|
||||||
}
|
}
|
||||||
err = bkt.Put(keyBytes, recordBytes)
|
err = bkt.Put(uint32ToBytes(newKey), recordBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not store vote change record: %w", err)
|
return fmt.Errorf("could not store vote change record: %w", err)
|
||||||
}
|
}
|
||||||
@ -113,7 +107,7 @@ func (vdb *VspDatabase) GetVoteChanges(ticketHash string) (map[uint32]VoteChange
|
|||||||
return fmt.Errorf("could not unmarshal vote change record: %w", err)
|
return fmt.Errorf("could not unmarshal vote change record: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
records[binary.LittleEndian.Uint32(k)] = record
|
records[bytesToUint32(k)] = record
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@ -78,7 +78,7 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("db.GetLastAddressIndex error: %w", err)
|
return fmt.Errorf("db.GetLastAddressIndex error: %w", err)
|
||||||
}
|
}
|
||||||
feeXPub, err := vdb.GetFeeXPub()
|
feeXPub, err := vdb.FeeXPub()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("db.GetFeeXPub error: %w", err)
|
return fmt.Errorf("db.GetFeeXPub error: %w", err)
|
||||||
}
|
}
|
||||||
@ -88,7 +88,7 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the secret key used to initialize the cookie store.
|
// Get the secret key used to initialize the cookie store.
|
||||||
cookieSecret, err := vdb.GetCookieSecret()
|
cookieSecret, err := vdb.CookieSecret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("db.GetCookieSecret error: %w", err)
|
return fmt.Errorf("db.GetCookieSecret error: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user