Files
tendermint/scripts/scmigrate/migrate.go
2022-03-29 12:31:37 -04:00

163 lines
3.4 KiB
Go

// Package scmigrate implements a migration for SeenCommit data
// between 0.34 and 0.35
//
// The Migrate implementation is idempotent and finds all seen commit
// records and deletes all *except* the record corresponding to the
// highest height.
package scmigrate
import (
"bytes"
"context"
"errors"
"fmt"
"sort"
"github.com/gogo/protobuf/proto"
"github.com/google/orderedcode"
dbm "github.com/tendermint/tm-db"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/types"
)
type toMigrate struct {
key []byte
commit *types.Commit
}
const prefixSeenCommit = int64(3)
func makeKeyFromPrefix(ids ...int64) []byte {
vals := make([]interface{}, len(ids))
for idx := range ids {
vals[idx] = ids[idx]
}
key, err := orderedcode.Append(nil, vals...)
if err != nil {
panic(err)
}
return key
}
func makeToMigrate(val []byte) (*types.Commit, error) {
if len(val) == 0 {
return nil, errors.New("empty value")
}
var pbc = new(tmproto.Commit)
if err := proto.Unmarshal(val, pbc); err != nil {
return nil, fmt.Errorf("error reading block seen commit: %w", err)
}
commit, err := types.CommitFromProto(pbc)
if commit == nil {
// theoretically we should error for all errors, but
// there's no reason to keep junk data in the
// database, and it makes testing easier.
if err != nil {
return nil, fmt.Errorf("error from proto commit: %w", err)
}
return nil, fmt.Errorf("missing commit")
}
return commit, nil
}
func sortMigrations(scData []toMigrate) {
// put this in it's own function just to make it testable
sort.SliceStable(scData, func(i, j int) bool {
return scData[i].commit.Height > scData[j].commit.Height
})
}
func getMigrationsToDelete(in []toMigrate) []toMigrate { return in[1:] }
func getAllSeenCommits(ctx context.Context, db dbm.DB) ([]toMigrate, error) {
scKeyPrefix := makeKeyFromPrefix(prefixSeenCommit)
iter, err := db.Iterator(
scKeyPrefix,
makeKeyFromPrefix(prefixSeenCommit+1),
)
if err != nil {
return nil, err
}
scData := []toMigrate{}
for ; iter.Valid(); iter.Next() {
if err := ctx.Err(); err != nil {
return nil, err
}
k := iter.Key()
nk := make([]byte, len(k))
copy(nk, k)
if !bytes.HasPrefix(nk, scKeyPrefix) {
break
}
commit, err := makeToMigrate(iter.Value())
if err != nil {
return nil, err
}
scData = append(scData, toMigrate{
key: nk,
commit: commit,
})
}
if err := iter.Error(); err != nil {
return nil, err
}
if err := iter.Close(); err != nil {
return nil, err
}
return scData, nil
}
func deleteRecords(ctx context.Context, db dbm.DB, scData []toMigrate) error {
// delete all the remaining stale values in a single batch
batch := db.NewBatch()
for _, mg := range scData {
if err := batch.Delete(mg.key); err != nil {
return err
}
}
if err := batch.WriteSync(); err != nil {
return err
}
if err := batch.Close(); err != nil {
return err
}
return nil
}
func Migrate(ctx context.Context, db dbm.DB) error {
scData, err := getAllSeenCommits(ctx, db)
if err != nil {
return fmt.Errorf("sourcing tasks to migrate: %w", err)
}
// sort earliest->latest commits.
sortMigrations(scData)
// trim the one we want to save:
scData = getMigrationsToDelete(scData)
if len(scData) <= 1 {
return nil
}
// write the migration (remove )
if err := deleteRecords(ctx, db, scData); err != nil {
return fmt.Errorf("writing data: %w", err)
}
return nil
}