walletdb/migration: add new migration package with Manager interface

In this commit, we add a new sub-package to the walletdb package:
migration. In this package, we define a new interface, Manager, which
will expose all of the necessary functions required to abstract the
migration logic of different sub-services within the wallet, like the
address and transaction managers. The implementations of this interface
will then be able to use the migration logic within the Upgrade
function with no additional complexity.
This commit is contained in:
Wilmer Paulino 2018-11-02 18:42:12 -07:00
parent c4dd27e481
commit 541ad708c7
No known key found for this signature in database
GPG key ID: 6DF57B9F9514972F
3 changed files with 548 additions and 0 deletions

43
walletdb/migration/log.go Normal file
View file

@ -0,0 +1,43 @@
package migration
import "github.com/btcsuite/btclog"
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
DisableLog()
}
// DisableLog disables all library log output. Logging output is disabled
// by default until either UseLogger or SetLogWriter are called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}
// LogClosure is a closure that can be printed with %v to be used to
// generate expensive-to-create data for a detailed log level and avoid doing
// the work if the data isn't printed.
type logClosure func() string
// String invokes the log closure and returns the results string.
func (c logClosure) String() string {
return c()
}
// newLogClosure returns a new closure over the passed function which allows
// it to be used as a parameter in a logging function that is only invoked when
// the logging level is such that the message will actually be logged.
func newLogClosure(c func() string) logClosure {
return logClosure(c)
}

View file

@ -0,0 +1,162 @@
package migration
import (
"errors"
"sort"
"github.com/btcsuite/btcwallet/walletdb"
)
var (
// ErrReversion is an error returned when an attempt to revert to a
// previous version is detected. This is done to provide safety to users
// as some upgrades may not be backwards-compatible.
ErrReversion = errors.New("reverting to a previous version is not " +
"supported")
)
// Version denotes the version number of the database. A migration can be used
// to bring a previous version of the database to a later one.
type Version struct {
// Number represents the number of this version.
Number uint32
// Migration represents a migration function that modifies the database
// state. Care must be taken so that consequent migrations build off of
// the previous one in order to ensure the consistency of the database.
Migration func(walletdb.ReadWriteBucket) error
}
// Manager is an interface that exposes the necessary methods needed in order to
// migrate/upgrade a service. Each service (i.e., an implementation of this
// interface) can then use the Upgrade function to perform any required database
// migrations.
type Manager interface {
// Name returns the name of the service we'll be attempting to upgrade.
Name() string
// Namespace returns the top-level bucket of the service.
Namespace() walletdb.ReadWriteBucket
// CurrentVersion returns the current version of the service's database.
CurrentVersion(walletdb.ReadBucket) (uint32, error)
// SetVersion sets the version of the service's database.
SetVersion(walletdb.ReadWriteBucket, uint32) error
// Versions returns all of the available database versions of the
// service.
Versions() []Version
}
// GetLatestVersion returns the latest version available from the given slice.
func GetLatestVersion(versions []Version) uint32 {
if len(versions) == 0 {
return 0
}
// Before determining the latest version number, we'll sort the slice to
// ensure it reflects the last element.
sort.Slice(versions, func(i, j int) bool {
return versions[i].Number < versions[j].Number
})
return versions[len(versions)-1].Number
}
// VersionsToApply determines which versions should be applied as migrations
// based on the current version.
func VersionsToApply(currentVersion uint32, versions []Version) []Version {
// Assuming the migration versions are in increasing order, we'll apply
// any migrations that have a version number lower than our current one.
var upgradeVersions []Version
for _, version := range versions {
if version.Number > currentVersion {
upgradeVersions = append(upgradeVersions, version)
}
}
// Before returning, we'll sort the slice by its version number to
// ensure the migrations are applied in their intended order.
sort.Slice(upgradeVersions, func(i, j int) bool {
return upgradeVersions[i].Number < upgradeVersions[j].Number
})
return upgradeVersions
}
// Upgrade attempts to upgrade a group of services exposed through the Manager
// interface. Each service will go through its available versions and determine
// whether it needs to apply any.
//
// NOTE: In order to guarantee fault-tolerance, each service upgrade should
// happen within the same database transaction.
func Upgrade(mgrs ...Manager) error {
for _, mgr := range mgrs {
if err := upgrade(mgr); err != nil {
return err
}
}
return nil
}
// upgrade attempts to upgrade a service expose through its implementation of
// the Manager interface. This function will determine whether any new versions
// need to be applied based on the service's current version and latest
// available one.
func upgrade(mgr Manager) error {
// We'll start by fetching the service's current and latest version.
ns := mgr.Namespace()
currentVersion, err := mgr.CurrentVersion(ns)
if err != nil {
return err
}
versions := mgr.Versions()
latestVersion := GetLatestVersion(versions)
switch {
// If the current version is greater than the latest, then the service
// is attempting to revert to a previous version that's possibly
// backwards-incompatible. To prevent this, we'll return an error
// indicating so.
case currentVersion > latestVersion:
return ErrReversion
// If the current version is behind the latest version, we'll need to
// apply all of the newer versions in order to catch up to the latest.
case currentVersion < latestVersion:
versions := VersionsToApply(currentVersion, versions)
mgrName := mgr.Name()
ns := mgr.Namespace()
for _, version := range versions {
log.Infof("Applying %v migration #%d", mgrName,
version.Number)
// We'll only run a migration if there is one available
// for this version.
if version.Migration != nil {
err := version.Migration(ns)
if err != nil {
log.Errorf("Unable to apply %v "+
"migration #%d: %v", mgrName,
version.Number, err)
return err
}
}
}
// With all of the versions applied, we can now reflect the
// latest version upon the service.
if err := mgr.SetVersion(ns, latestVersion); err != nil {
return err
}
// If the current version matches the latest one, there's no upgrade
// needed and we can safely exit.
case currentVersion == latestVersion:
}
return nil
}

View file

@ -0,0 +1,343 @@
package migration_test
import (
"errors"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/walletdb/migration"
"github.com/davecgh/go-spew/spew"
)
type mockMigrationManager struct {
currentVersion uint32
versions []migration.Version
}
var _ migration.Manager = (*mockMigrationManager)(nil)
func (m *mockMigrationManager) Name() string {
return "mock"
}
func (m *mockMigrationManager) Namespace() walletdb.ReadWriteBucket {
return nil
}
func (m *mockMigrationManager) CurrentVersion(_ walletdb.ReadBucket) (uint32, error) {
return m.currentVersion, nil
}
func (m *mockMigrationManager) SetVersion(_ walletdb.ReadWriteBucket, version uint32) error {
m.currentVersion = version
return nil
}
func (m *mockMigrationManager) Versions() []migration.Version {
return m.versions
}
// TestGetLatestVersion ensures that we can properly retrieve the latest version
// from a slice of versions.
func TestGetLatestVersion(t *testing.T) {
t.Parallel()
tests := []struct {
versions []migration.Version
latestVersion uint32
}{
{
versions: []migration.Version{},
latestVersion: 0,
},
{
versions: []migration.Version{
{
Number: 1,
Migration: nil,
},
},
latestVersion: 1,
},
{
versions: []migration.Version{
{
Number: 1,
Migration: nil,
},
{
Number: 2,
Migration: nil,
},
},
latestVersion: 2,
},
{
versions: []migration.Version{
{
Number: 2,
Migration: nil,
},
{
Number: 0,
Migration: nil,
},
{
Number: 1,
Migration: nil,
},
},
latestVersion: 2,
},
}
for i, test := range tests {
latestVersion := migration.GetLatestVersion(test.versions)
if latestVersion != test.latestVersion {
t.Fatalf("test %d: expected latest version %d, got %d",
i, test.latestVersion, latestVersion)
}
}
}
// TestVersionsToApply ensures that the proper versions that needs to be applied
// are returned given the current version.
func TestVersionsToApply(t *testing.T) {
t.Parallel()
tests := []struct {
currentVersion uint32
versions []migration.Version
versionsToApply []migration.Version
}{
{
currentVersion: 0,
versions: []migration.Version{
{
Number: 0,
Migration: nil,
},
},
versionsToApply: nil,
},
{
currentVersion: 1,
versions: []migration.Version{
{
Number: 0,
Migration: nil,
},
},
versionsToApply: nil,
},
{
currentVersion: 0,
versions: []migration.Version{
{
Number: 0,
Migration: nil,
},
{
Number: 1,
Migration: nil,
},
{
Number: 2,
Migration: nil,
},
},
versionsToApply: []migration.Version{
{
Number: 1,
Migration: nil,
},
{
Number: 2,
Migration: nil,
},
},
},
{
currentVersion: 0,
versions: []migration.Version{
{
Number: 2,
Migration: nil,
},
{
Number: 0,
Migration: nil,
},
{
Number: 1,
Migration: nil,
},
},
versionsToApply: []migration.Version{
{
Number: 1,
Migration: nil,
},
{
Number: 2,
Migration: nil,
},
},
},
}
for i, test := range tests {
versionsToApply := migration.VersionsToApply(
test.currentVersion, test.versions,
)
if !reflect.DeepEqual(versionsToApply, test.versionsToApply) {
t.Fatalf("test %d: versions to apply mismatch\n"+
"expected: %v\ngot: %v", i,
spew.Sdump(test.versionsToApply),
spew.Sdump(versionsToApply))
}
}
}
// TestUpgradeRevert ensures that we are not able to revert to a previous
// version.
func TestUpgradeRevert(t *testing.T) {
t.Parallel()
m := &mockMigrationManager{
currentVersion: 1,
versions: []migration.Version{
{
Number: 0,
Migration: nil,
},
},
}
if err := migration.Upgrade(m); err != migration.ErrReversion {
t.Fatalf("expected Upgrade to fail with ErrReversion, got %v",
err)
}
}
// TestUpgradeSameVersion ensures that no upgrades happen if the current version
// matches the latest.
func TestUpgradeSameVersion(t *testing.T) {
t.Parallel()
m := &mockMigrationManager{
currentVersion: 1,
versions: []migration.Version{
{
Number: 0,
Migration: nil,
},
{
Number: 1,
Migration: func(walletdb.ReadWriteBucket) error {
return errors.New("migration should " +
"not happen due to already " +
"being on the latest version")
},
},
},
}
if err := migration.Upgrade(m); err != nil {
t.Fatalf("unable to upgrade: %v", err)
}
}
// TestUpgradeNewVersion ensures that we can properly upgrade to a newer version
// if available.
func TestUpgradeNewVersion(t *testing.T) {
t.Parallel()
versions := []migration.Version{
{
Number: 0,
Migration: nil,
},
{
Number: 1,
Migration: func(walletdb.ReadWriteBucket) error {
return nil
},
},
}
m := &mockMigrationManager{
currentVersion: 0,
versions: versions,
}
if err := migration.Upgrade(m); err != nil {
t.Fatalf("unable to upgrade: %v", err)
}
latestVersion := migration.GetLatestVersion(versions)
if m.currentVersion != latestVersion {
t.Fatalf("expected current version to match latest: "+
"current=%d vs latest=%d", m.currentVersion,
latestVersion)
}
}
// TestUpgradeMultipleVersions ensures that we can go through multiple upgrades
// in-order to reach the latest version.
func TestUpgradeMultipleVersions(t *testing.T) {
t.Parallel()
previousVersion := uint32(0)
versions := []migration.Version{
{
Number: previousVersion,
Migration: nil,
},
{
Number: 1,
Migration: func(walletdb.ReadWriteBucket) error {
if previousVersion != 0 {
return fmt.Errorf("expected previous "+
"version to be %d, got %d", 0,
previousVersion)
}
previousVersion = 1
return nil
},
},
{
Number: 2,
Migration: func(walletdb.ReadWriteBucket) error {
if previousVersion != 1 {
return fmt.Errorf("expected previous "+
"version to be %d, got %d", 1,
previousVersion)
}
previousVersion = 2
return nil
},
},
}
m := &mockMigrationManager{
currentVersion: 0,
versions: versions,
}
if err := migration.Upgrade(m); err != nil {
t.Fatalf("unable to upgrade: %v", err)
}
latestVersion := migration.GetLatestVersion(versions)
if m.currentVersion != latestVersion {
t.Fatalf("expected current version to match latest: "+
"current=%d vs latest=%d", m.currentVersion,
latestVersion)
}
}