diff --git a/.gitignore b/.gitignore index 69eaef4..de71701 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ cov.out *mem.out /webapi/test.db /database/test.db -/database/test.db-backup # Go workspace go.work diff --git a/cmd/vspd/main.go b/cmd/vspd/main.go index 630b77c..4831b60 100644 --- a/cmd/vspd/main.go +++ b/cmd/vspd/main.go @@ -78,14 +78,18 @@ func run() int { defer log.Criticalf("Shutdown complete") // 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 { log.Errorf("Database error: %v", err) requestShutdown() shutdownWg.Wait() 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 // checking the status of fee transactions). diff --git a/database/database.go b/database/database.go index 0463f0e..62114fb 100644 --- a/database/database.go +++ b/database/database.go @@ -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 // database file is found at the provided path. -func Open(shutdownCtx context.Context, shutdownWg *sync.WaitGroup, log slog.Logger, dbFile string, - backupInterval time.Duration, maxVoteChangeRecords int) (*VspDatabase, error) { - +func Open(dbFile string, log slog.Logger, maxVoteChangeRecords int) (*VspDatabase, error) { // 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 // 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) } - // 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) go func() { for { @@ -224,7 +230,7 @@ func Open(shutdownCtx context.Context, shutdownWg *sync.WaitGroup, log slog.Logg case <-time.After(backupInterval): err := vdb.writeHotBackupFile() 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(): 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 -// backup location. -func (vdb *VspDatabase) Close() { +// Close will close the database and, if requested, make a copy of the database +// to the backup location. +func (vdb *VspDatabase) Close(writeBackup bool) { // Make a copy of the db path here because once the db is closed, db.Path // returns empty string. @@ -257,6 +261,10 @@ func (vdb *VspDatabase) Close() { vdb.log.Debug("Database closed") + if !writeBackup { + return + } + // Ensure the database backup file is up-to-date. backupPath := dbPath + "-backup" tempPath := backupPath + "~" @@ -291,7 +299,7 @@ func (vdb *VspDatabase) Close() { 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. diff --git a/database/database_test.go b/database/database_test.go index b0ee598..721eb6e 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -5,7 +5,6 @@ package database import ( - "context" "crypto/ed25519" "io" "math/rand" @@ -13,7 +12,6 @@ import ( "net/http/httptest" "os" "strconv" - "sync" "testing" "time" @@ -22,7 +20,6 @@ import ( const ( testDb = "test.db" - backupDb = "test.db-backup" feeXPub = "feexpub" maxVoteChangeRecords = 3 @@ -70,7 +67,6 @@ func stdoutLogger() slog.Logger { func TestDatabase(t *testing.T) { // Ensure we are starting with a clean environment. os.Remove(testDb) - os.Remove(backupDb) // All sub-tests to run. tests := map[string]func(*testing.T){ @@ -95,14 +91,13 @@ func TestDatabase(t *testing.T) { for testName, test := range tests { // Create a new blank database for each sub-test. - var err error - var wg sync.WaitGroup - ctx, cancel := context.WithCancel(context.TODO()) - err = CreateNew(testDb, feeXPub, log) + err := CreateNew(testDb, feeXPub, log) if err != nil { 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 { t.Fatalf("error opening test database: %v", err) } @@ -110,14 +105,9 @@ func TestDatabase(t *testing.T) { // Run the sub-test. t.Run(testName, test) - // Request database shutdown and wait for it to complete. - cancel() - wg.Wait() - - db.Close() - + writeBackup := false + db.Close(writeBackup) os.Remove(testDb) - os.Remove(backupDb) } } diff --git a/webapi/setaltsignaddr_test.go b/webapi/setaltsignaddr_test.go index 98cf6e4..6cf424e 100644 --- a/webapi/setaltsignaddr_test.go +++ b/webapi/setaltsignaddr_test.go @@ -6,7 +6,6 @@ package webapi import ( "bytes" - "context" "crypto/ed25519" "encoding/json" "errors" @@ -15,7 +14,6 @@ import ( "net/http" "net/http/httptest" "os" - "sync" "testing" "time" @@ -33,7 +31,6 @@ const ( // (base64 encoding). sigCharset = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/=" testDb = "test.db" - backupDb = "test.db-backup" ) var ( @@ -72,17 +69,15 @@ func TestMain(m *testing.M) { // Create a database to use. // Ensure we are starting with a clean environment. os.Remove(testDb) - os.Remove(backupDb) // Create a new blank database for all tests. - var err error - var wg sync.WaitGroup - ctx, cancel := context.WithCancel(context.Background()) - err = database.CreateNew(testDb, feeXPub, log) + err := database.CreateNew(testDb, feeXPub, log) if err != nil { 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 { panic(fmt.Errorf("error opening test database: %w", err)) } @@ -97,12 +92,9 @@ func TestMain(m *testing.M) { // Run tests. exitCode := m.Run() - // Request database shutdown and wait for it to complete. - cancel() - wg.Wait() - db.Close() + writeBackup := false + db.Close(writeBackup) os.Remove(testDb) - os.Remove(backupDb) os.Exit(exitCode) }