mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 18:52:41 +01:00
Merge pull request #246 from slingamn/db_autoupgrade.1
implement database auto-upgrades (#243)
This commit is contained in:
commit
4a17eadbce
@ -229,7 +229,8 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Datastore struct {
|
Datastore struct {
|
||||||
Path string
|
Path string
|
||||||
|
AutoUpgrade bool
|
||||||
}
|
}
|
||||||
|
|
||||||
Accounts AccountConfig
|
Accounts AccountConfig
|
||||||
|
112
irc/database.go
112
irc/database.go
@ -11,9 +11,11 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/oragono/oragono/irc/modes"
|
"github.com/oragono/oragono/irc/modes"
|
||||||
"github.com/oragono/oragono/irc/passwd"
|
"github.com/oragono/oragono/irc/passwd"
|
||||||
|
"github.com/oragono/oragono/irc/utils"
|
||||||
|
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
)
|
)
|
||||||
@ -38,6 +40,22 @@ type SchemaChange struct {
|
|||||||
// maps an initial version to a schema change capable of upgrading it
|
// maps an initial version to a schema change capable of upgrading it
|
||||||
var schemaChanges map[string]SchemaChange
|
var schemaChanges map[string]SchemaChange
|
||||||
|
|
||||||
|
type incompatibleSchemaError struct {
|
||||||
|
currentVersion string
|
||||||
|
requiredVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
|
||||||
|
return &incompatibleSchemaError{
|
||||||
|
currentVersion: currentVersion,
|
||||||
|
requiredVersion: latestDbSchema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *incompatibleSchemaError) Error() string {
|
||||||
|
return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
// InitDB creates the database.
|
// InitDB creates the database.
|
||||||
func InitDB(path string) {
|
func InitDB(path string) {
|
||||||
// prepare kvstore db
|
// prepare kvstore db
|
||||||
@ -69,36 +87,80 @@ func InitDB(path string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OpenDatabase returns an existing database, performing a schema version check.
|
// OpenDatabase returns an existing database, performing a schema version check.
|
||||||
func OpenDatabase(path string) (*buntdb.DB, error) {
|
func OpenDatabase(config *Config) (*buntdb.DB, error) {
|
||||||
// open data store
|
return openDatabaseInternal(config, config.Datastore.AutoUpgrade)
|
||||||
db, err := buntdb.Open(path)
|
}
|
||||||
|
|
||||||
|
// open the database, giving it at most one chance to auto-upgrade the schema
|
||||||
|
func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, err error) {
|
||||||
|
db, err = buntdb.Open(config.Datastore.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// check db version
|
defer func() {
|
||||||
err = db.View(func(tx *buntdb.Tx) error {
|
if err != nil && db != nil {
|
||||||
version, _ := tx.Get(keySchemaVersion)
|
db.Close()
|
||||||
if version != latestDbSchema {
|
db = nil
|
||||||
return fmt.Errorf("Database must be updated. Expected schema v%s, got v%s", latestDbSchema, version)
|
|
||||||
}
|
}
|
||||||
return nil
|
}()
|
||||||
})
|
|
||||||
|
|
||||||
|
// read the current version string
|
||||||
|
var version string
|
||||||
|
err = db.View(func(tx *buntdb.Tx) error {
|
||||||
|
version, err = tx.Get(keySchemaVersion)
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// close the db
|
return
|
||||||
db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return db, nil
|
if version == latestDbSchema {
|
||||||
|
// success
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX quiesce the DB so we can be sure it's safe to make a backup copy
|
||||||
|
db.Close()
|
||||||
|
db = nil
|
||||||
|
if allowAutoupgrade {
|
||||||
|
err = performAutoUpgrade(version, config)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// successful autoupgrade, let's try this again:
|
||||||
|
return openDatabaseInternal(config, false)
|
||||||
|
} else {
|
||||||
|
err = IncompatibleSchemaError(version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func performAutoUpgrade(currentVersion string, config *Config) (err error) {
|
||||||
|
path := config.Datastore.Path
|
||||||
|
log.Printf("attempting to auto-upgrade schema from version %s to %s\n", currentVersion, latestDbSchema)
|
||||||
|
timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
|
||||||
|
backupPath := fmt.Sprintf("%s.v%s.%s.bak", path, currentVersion, timestamp)
|
||||||
|
log.Printf("making a backup of current database at %s\n", backupPath)
|
||||||
|
err = utils.CopyFile(path, backupPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = UpgradeDB(config)
|
||||||
|
if err != nil {
|
||||||
|
// database upgrade is a single transaction, so we don't need to restore the backup;
|
||||||
|
// we can just delete it
|
||||||
|
os.Remove(backupPath)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpgradeDB upgrades the datastore to the latest schema.
|
// UpgradeDB upgrades the datastore to the latest schema.
|
||||||
func UpgradeDB(config *Config) {
|
func UpgradeDB(config *Config) (err error) {
|
||||||
store, err := buntdb.Open(config.Datastore.Path)
|
store, err := buntdb.Open(config.Datastore.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(fmt.Sprintf("Failed to open datastore: %s", err.Error()))
|
return err
|
||||||
}
|
}
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
@ -108,9 +170,14 @@ func UpgradeDB(config *Config) {
|
|||||||
version, _ = tx.Get(keySchemaVersion)
|
version, _ = tx.Get(keySchemaVersion)
|
||||||
change, schemaNeedsChange := schemaChanges[version]
|
change, schemaNeedsChange := schemaChanges[version]
|
||||||
if !schemaNeedsChange {
|
if !schemaNeedsChange {
|
||||||
break
|
if version == latestDbSchema {
|
||||||
|
// success!
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// unable to upgrade to the desired version, roll back
|
||||||
|
return IncompatibleSchemaError(version)
|
||||||
}
|
}
|
||||||
log.Println("attempting to update store from version " + version)
|
log.Println("attempting to update schema from version " + version)
|
||||||
err := change.Changer(config, tx)
|
err := change.Changer(config, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -119,16 +186,15 @@ func UpgradeDB(config *Config) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println("successfully updated store to version " + change.TargetVersion)
|
log.Println("successfully updated schema to version " + change.TargetVersion)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Could not update datastore:", err.Error())
|
log.Println("database upgrade failed and was rolled back")
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|
||||||
|
@ -127,7 +127,6 @@ type Server struct {
|
|||||||
signals chan os.Signal
|
signals chan os.Signal
|
||||||
snomasks *SnoManager
|
snomasks *SnoManager
|
||||||
store *buntdb.DB
|
store *buntdb.DB
|
||||||
storeFilename string
|
|
||||||
stsEnabled bool
|
stsEnabled bool
|
||||||
webirc []webircConfig
|
webirc []webircConfig
|
||||||
whoWas *WhoWasList
|
whoWas *WhoWasList
|
||||||
@ -753,7 +752,7 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
|||||||
return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted")
|
return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted")
|
||||||
} else if server.name != config.Server.Name {
|
} else if server.name != config.Server.Name {
|
||||||
return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
|
return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
|
||||||
} else if server.storeFilename != config.Datastore.Path {
|
} else if server.config.Datastore.Path != config.Datastore.Path {
|
||||||
return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
|
return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -973,10 +972,9 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
|||||||
server.config = config
|
server.config = config
|
||||||
server.configurableStateMutex.Unlock()
|
server.configurableStateMutex.Unlock()
|
||||||
|
|
||||||
server.storeFilename = config.Datastore.Path
|
server.logger.Info("rehash", "Using datastore", config.Datastore.Path)
|
||||||
server.logger.Info("rehash", "Using datastore", server.storeFilename)
|
|
||||||
if initial {
|
if initial {
|
||||||
if err := server.loadDatastore(server.storeFilename); err != nil {
|
if err := server.loadDatastore(config); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1073,11 +1071,11 @@ func (server *Server) loadMOTD(motdPath string, useFormatting bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) loadDatastore(datastorePath string) error {
|
func (server *Server) loadDatastore(config *Config) error {
|
||||||
// open the datastore and load server state for which it (rather than config)
|
// open the datastore and load server state for which it (rather than config)
|
||||||
// is the source of truth
|
// is the source of truth
|
||||||
|
|
||||||
db, err := OpenDatabase(datastorePath)
|
db, err := OpenDatabase(config)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
server.store = db
|
server.store = db
|
||||||
} else {
|
} else {
|
||||||
|
31
irc/utils/os.go
Normal file
31
irc/utils/os.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2018 Shivaram Lingamneni
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// implementation of `cp` (go should really provide this...)
|
||||||
|
func CopyFile(src string, dst string) (err error) {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
closeError := out.Close()
|
||||||
|
if err == nil {
|
||||||
|
err = closeError
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if _, err = io.Copy(out, in); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
@ -84,7 +84,10 @@ Options:
|
|||||||
log.Println("database initialized: ", config.Datastore.Path)
|
log.Println("database initialized: ", config.Datastore.Path)
|
||||||
}
|
}
|
||||||
} else if arguments["upgradedb"].(bool) {
|
} else if arguments["upgradedb"].(bool) {
|
||||||
irc.UpgradeDB(config)
|
err = irc.UpgradeDB(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error while upgrading db:", err.Error())
|
||||||
|
}
|
||||||
if !arguments["--quiet"].(bool) {
|
if !arguments["--quiet"].(bool) {
|
||||||
log.Println("database upgraded: ", config.Datastore.Path)
|
log.Println("database upgraded: ", config.Datastore.Path)
|
||||||
}
|
}
|
||||||
|
@ -342,6 +342,11 @@ datastore:
|
|||||||
# path to the datastore
|
# path to the datastore
|
||||||
path: ircd.db
|
path: ircd.db
|
||||||
|
|
||||||
|
# if the database schema requires an upgrade, `autoupgrade` will attempt to
|
||||||
|
# perform it automatically on startup. the database will be backed
|
||||||
|
# up, and if the upgrade fails, the original database will be restored.
|
||||||
|
autoupgrade: true
|
||||||
|
|
||||||
# languages config
|
# languages config
|
||||||
languages:
|
languages:
|
||||||
# whether to load languages
|
# whether to load languages
|
||||||
|
Loading…
Reference in New Issue
Block a user