Make database backups optional.

Periodic database backups are no longer started automatically in database.Open(), and the backup written by database.Close() can now be disabled.

Only vspd itself requires backups, they are not useful for test code or future upcoming tools such as vote-validator.
This commit is contained in:
jholdstock 2022-09-27 14:50:49 +01:00 committed by Jamie Holdstock
parent 11401c5369
commit 32790984fe
5 changed files with 37 additions and 44 deletions

1
.gitignore vendored
View File

@ -9,7 +9,6 @@ cov.out
*mem.out *mem.out
/webapi/test.db /webapi/test.db
/database/test.db /database/test.db
/database/test.db-backup
# Go workspace # Go workspace
go.work go.work

View File

@ -78,14 +78,18 @@ func run() int {
defer log.Criticalf("Shutdown complete") defer log.Criticalf("Shutdown complete")
// Open database. // Open database.
db, err := database.Open(shutdownCtx, &shutdownWg, dbLog, cfg.dbPath, cfg.BackupInterval, maxVoteChangeRecords) db, err := database.Open(cfg.dbPath, dbLog, maxVoteChangeRecords)
if err != nil { if err != nil {
log.Errorf("Database error: %v", err) log.Errorf("Database error: %v", err)
requestShutdown() requestShutdown()
shutdownWg.Wait() shutdownWg.Wait()
return 1 return 1
} }
defer db.Close()
writeBackup := true
defer db.Close(writeBackup)
db.WritePeriodicBackups(shutdownCtx, &shutdownWg, cfg.BackupInterval)
// Create RPC client for local dcrd instance (used for broadcasting and // Create RPC client for local dcrd instance (used for broadcasting and
// checking the status of fee transactions). // checking the status of fee transactions).

View File

@ -175,9 +175,7 @@ func CreateNew(dbFile, feeXPub string, log slog.Logger) error {
// Open initializes and returns an open database. An error is returned if no // Open initializes and returns an open database. An error is returned if no
// database file is found at the provided path. // database file is found at the provided path.
func Open(shutdownCtx context.Context, shutdownWg *sync.WaitGroup, log slog.Logger, dbFile string, func Open(dbFile string, log slog.Logger, maxVoteChangeRecords int) (*VspDatabase, error) {
backupInterval time.Duration, maxVoteChangeRecords int) (*VspDatabase, error) {
// Error if db file does not exist. This is needed because bolt.Open will // Error if db file does not exist. This is needed because bolt.Open will
// silently create a new empty database if the file does not exist. A new // silently create a new empty database if the file does not exist. A new
// vspd database should be created with the CreateNew() function. // vspd database should be created with the CreateNew() function.
@ -216,7 +214,15 @@ func Open(shutdownCtx context.Context, shutdownWg *sync.WaitGroup, log slog.Logg
return nil, fmt.Errorf("upgrade failed: %w", err) return nil, fmt.Errorf("upgrade failed: %w", err)
} }
// Periodically update the database backup file. return vdb, nil
}
// WritePeriodicBackups starts a goroutine to periodically write a database backup file.
// It can be stopped by cancelling the provided context, and uses the provided
// WaitGroup to signal that it has finished.
func (vdb *VspDatabase) WritePeriodicBackups(shutdownCtx context.Context, shutdownWg *sync.WaitGroup,
backupInterval time.Duration) {
shutdownWg.Add(1) shutdownWg.Add(1)
go func() { go func() {
for { for {
@ -224,7 +230,7 @@ func Open(shutdownCtx context.Context, shutdownWg *sync.WaitGroup, log slog.Logg
case <-time.After(backupInterval): case <-time.After(backupInterval):
err := vdb.writeHotBackupFile() err := vdb.writeHotBackupFile()
if err != nil { if err != nil {
log.Errorf("Failed to write database backup: %v", err) vdb.log.Errorf("Failed to write database backup: %v", err)
} }
case <-shutdownCtx.Done(): case <-shutdownCtx.Done():
shutdownWg.Done() shutdownWg.Done()
@ -232,13 +238,11 @@ func Open(shutdownCtx context.Context, shutdownWg *sync.WaitGroup, log slog.Logg
} }
} }
}() }()
return vdb, nil
} }
// Close will close the database and then make a copy of the database to the // Close will close the database and, if requested, make a copy of the database
// backup location. // to the backup location.
func (vdb *VspDatabase) Close() { func (vdb *VspDatabase) Close(writeBackup bool) {
// Make a copy of the db path here because once the db is closed, db.Path // Make a copy of the db path here because once the db is closed, db.Path
// returns empty string. // returns empty string.
@ -257,6 +261,10 @@ func (vdb *VspDatabase) Close() {
vdb.log.Debug("Database closed") vdb.log.Debug("Database closed")
if !writeBackup {
return
}
// Ensure the database backup file is up-to-date. // Ensure the database backup file is up-to-date.
backupPath := dbPath + "-backup" backupPath := dbPath + "-backup"
tempPath := backupPath + "~" tempPath := backupPath + "~"
@ -291,7 +299,7 @@ func (vdb *VspDatabase) Close() {
return return
} }
vdb.log.Tracef("Database backup written to %s", backupPath) vdb.log.Debugf("Database backup written to %s", backupPath)
} }
// KeyPair retrieves the keypair used to sign API responses from the database. // KeyPair retrieves the keypair used to sign API responses from the database.

View File

@ -5,7 +5,6 @@
package database package database
import ( import (
"context"
"crypto/ed25519" "crypto/ed25519"
"io" "io"
"math/rand" "math/rand"
@ -13,7 +12,6 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"strconv" "strconv"
"sync"
"testing" "testing"
"time" "time"
@ -22,7 +20,6 @@ import (
const ( const (
testDb = "test.db" testDb = "test.db"
backupDb = "test.db-backup"
feeXPub = "feexpub" feeXPub = "feexpub"
maxVoteChangeRecords = 3 maxVoteChangeRecords = 3
@ -70,7 +67,6 @@ func stdoutLogger() slog.Logger {
func TestDatabase(t *testing.T) { func TestDatabase(t *testing.T) {
// Ensure we are starting with a clean environment. // Ensure we are starting with a clean environment.
os.Remove(testDb) os.Remove(testDb)
os.Remove(backupDb)
// All sub-tests to run. // All sub-tests to run.
tests := map[string]func(*testing.T){ tests := map[string]func(*testing.T){
@ -95,14 +91,13 @@ func TestDatabase(t *testing.T) {
for testName, test := range tests { for testName, test := range tests {
// Create a new blank database for each sub-test. // Create a new blank database for each sub-test.
var err error err := CreateNew(testDb, feeXPub, log)
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.TODO())
err = CreateNew(testDb, feeXPub, log)
if err != nil { if err != nil {
t.Fatalf("error creating test database: %v", err) t.Fatalf("error creating test database: %v", err)
} }
db, err = Open(ctx, &wg, log, testDb, time.Hour, maxVoteChangeRecords)
// Open the newly created database so it is ready to use.
db, err = Open(testDb, log, maxVoteChangeRecords)
if err != nil { if err != nil {
t.Fatalf("error opening test database: %v", err) t.Fatalf("error opening test database: %v", err)
} }
@ -110,14 +105,9 @@ func TestDatabase(t *testing.T) {
// Run the sub-test. // Run the sub-test.
t.Run(testName, test) t.Run(testName, test)
// Request database shutdown and wait for it to complete. writeBackup := false
cancel() db.Close(writeBackup)
wg.Wait()
db.Close()
os.Remove(testDb) os.Remove(testDb)
os.Remove(backupDb)
} }
} }

View File

@ -6,7 +6,6 @@ package webapi
import ( import (
"bytes" "bytes"
"context"
"crypto/ed25519" "crypto/ed25519"
"encoding/json" "encoding/json"
"errors" "errors"
@ -15,7 +14,6 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"sync"
"testing" "testing"
"time" "time"
@ -33,7 +31,6 @@ const (
// (base64 encoding). // (base64 encoding).
sigCharset = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/=" sigCharset = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/="
testDb = "test.db" testDb = "test.db"
backupDb = "test.db-backup"
) )
var ( var (
@ -72,17 +69,15 @@ func TestMain(m *testing.M) {
// Create a database to use. // Create a database to use.
// Ensure we are starting with a clean environment. // Ensure we are starting with a clean environment.
os.Remove(testDb) os.Remove(testDb)
os.Remove(backupDb)
// Create a new blank database for all tests. // Create a new blank database for all tests.
var err error err := database.CreateNew(testDb, feeXPub, log)
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
err = database.CreateNew(testDb, feeXPub, log)
if err != nil { if err != nil {
panic(fmt.Errorf("error creating test database: %w", err)) panic(fmt.Errorf("error creating test database: %w", err))
} }
db, err := database.Open(ctx, &wg, log, testDb, time.Hour, maxVoteChangeRecords)
// Open the newly created database so it is ready to use.
db, err := database.Open(testDb, log, maxVoteChangeRecords)
if err != nil { if err != nil {
panic(fmt.Errorf("error opening test database: %w", err)) panic(fmt.Errorf("error opening test database: %w", err))
} }
@ -97,12 +92,9 @@ func TestMain(m *testing.M) {
// Run tests. // Run tests.
exitCode := m.Run() exitCode := m.Run()
// Request database shutdown and wait for it to complete. writeBackup := false
cancel() db.Close(writeBackup)
wg.Wait()
db.Close()
os.Remove(testDb) os.Remove(testDb)
os.Remove(backupDb)
os.Exit(exitCode) os.Exit(exitCode)
} }