mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-19 06:31:57 +00:00
db: migration script for key format change (#6355)
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
// Package keymigrate translates all legacy formatted keys to their
|
||||
// new components.
|
||||
//
|
||||
// The key migration operation as implemented provides a potential
|
||||
// model for database migration operations. Crucially, the migration
|
||||
// as implemented does not depend on any tendermint code.
|
||||
package keymigrate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/google/orderedcode"
|
||||
"github.com/pkg/errors"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
type (
|
||||
keyID []byte
|
||||
migrateFunc func(keyID) (keyID, error)
|
||||
)
|
||||
|
||||
func getAllLegacyKeys(db dbm.DB) ([]keyID, error) {
|
||||
out := []keyID{}
|
||||
|
||||
iter, err := db.Iterator(nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for ; iter.Valid(); iter.Next() {
|
||||
k := iter.Key()
|
||||
|
||||
// make sure it's a key with a legacy format, and skip
|
||||
// all other keys, to make it safe to resume the migration.
|
||||
if !keyIsLegacy(k) {
|
||||
continue
|
||||
}
|
||||
|
||||
// there's inconsistency around tm-db's handling of
|
||||
// key copies.
|
||||
nk := make([]byte, len(k))
|
||||
copy(nk, k)
|
||||
out = append(out, nk)
|
||||
}
|
||||
|
||||
if err = iter.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = iter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func makeKeyChan(keys []keyID) <-chan keyID {
|
||||
out := make(chan keyID, len(keys))
|
||||
defer close(out)
|
||||
|
||||
for _, key := range keys {
|
||||
out <- key
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func keyIsLegacy(key keyID) bool {
|
||||
for _, prefix := range []keyID{
|
||||
// core "store"
|
||||
keyID("consensusParamsKey:"),
|
||||
keyID("abciResponsesKey:"),
|
||||
keyID("validatorsKey:"),
|
||||
keyID("stateKey"),
|
||||
keyID("H:"),
|
||||
keyID("P:"),
|
||||
keyID("C:"),
|
||||
keyID("SC:"),
|
||||
keyID("BH:"),
|
||||
// light
|
||||
keyID("size"),
|
||||
keyID("lb/"),
|
||||
// evidence
|
||||
keyID([]byte{0x00}),
|
||||
keyID([]byte{0x01}),
|
||||
// tx index
|
||||
keyID("tx.height/"),
|
||||
keyID("tx.hash/"),
|
||||
} {
|
||||
if bytes.HasPrefix(key, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// this means it's a tx index...
|
||||
if bytes.Count(key, []byte("/")) >= 3 {
|
||||
return true
|
||||
}
|
||||
|
||||
return keyIsHash(key)
|
||||
}
|
||||
|
||||
func keyIsHash(key keyID) bool {
|
||||
return len(key) == 32 && !bytes.Contains(key, []byte("/"))
|
||||
}
|
||||
|
||||
func migarateKey(key keyID) (keyID, error) {
|
||||
switch {
|
||||
case bytes.HasPrefix(key, keyID("H:")):
|
||||
val, err := strconv.Atoi(string(key[2:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, int64(0), int64(val))
|
||||
case bytes.HasPrefix(key, keyID("P:")):
|
||||
parts := bytes.Split(key[2:], []byte(":"))
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("block parts key has %d rather than 2 components",
|
||||
len(parts))
|
||||
}
|
||||
valOne, err := strconv.Atoi(string(parts[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valTwo, err := strconv.Atoi(string(parts[1]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, int64(1), int64(valOne), int64(valTwo))
|
||||
case bytes.HasPrefix(key, keyID("C:")):
|
||||
val, err := strconv.Atoi(string(key[2:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, int64(2), int64(val))
|
||||
case bytes.HasPrefix(key, keyID("SC:")):
|
||||
val, err := strconv.Atoi(string(key[3:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, int64(3), int64(val))
|
||||
case bytes.HasPrefix(key, keyID("BH:")):
|
||||
val, err := strconv.Atoi(string(key[3:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, int64(4), int64(val))
|
||||
case bytes.HasPrefix(key, keyID("validatorsKey:")):
|
||||
val, err := strconv.Atoi(string(key[14:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, int64(5), int64(val))
|
||||
case bytes.HasPrefix(key, keyID("consensusParamsKey:")):
|
||||
val, err := strconv.Atoi(string(key[19:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, int64(6), int64(val))
|
||||
case bytes.HasPrefix(key, keyID("abciResponsesKey:")):
|
||||
val, err := strconv.Atoi(string(key[17:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, int64(7), int64(val))
|
||||
case bytes.HasPrefix(key, keyID("stateKey")):
|
||||
return orderedcode.Append(nil, int64(8))
|
||||
case bytes.HasPrefix(key, []byte{0x00}): // committed evidence
|
||||
return convertEvidence(key, 9)
|
||||
case bytes.HasPrefix(key, []byte{0x01}): // pending evidence
|
||||
return convertEvidence(key, 10)
|
||||
case bytes.HasPrefix(key, keyID("lb/")):
|
||||
if len(key) < 24 {
|
||||
return nil, fmt.Errorf("light block evidence %q in invalid format", string(key))
|
||||
}
|
||||
|
||||
val, err := strconv.Atoi(string(key[len(key)-20:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, int64(11), int64(val))
|
||||
case bytes.HasPrefix(key, keyID("size")):
|
||||
return orderedcode.Append(nil, int64(12))
|
||||
case bytes.HasPrefix(key, keyID("tx.height")):
|
||||
parts := bytes.Split(key, []byte("/"))
|
||||
if len(parts) != 4 {
|
||||
return nil, fmt.Errorf("key has %d parts rather than 4", len(parts))
|
||||
}
|
||||
parts = parts[1:] // drop prefix
|
||||
|
||||
elems := make([]interface{}, 0, len(parts)+1)
|
||||
elems = append(elems, "tx.height")
|
||||
|
||||
for idx, pt := range parts {
|
||||
val, err := strconv.Atoi(string(pt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if idx == 0 {
|
||||
elems = append(elems, fmt.Sprintf("%d", val))
|
||||
} else {
|
||||
elems = append(elems, int64(val))
|
||||
}
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, elems...)
|
||||
case bytes.Count(key, []byte("/")) >= 3: // tx indexer
|
||||
parts := bytes.Split(key, []byte("/"))
|
||||
|
||||
elems := make([]interface{}, 0, 4)
|
||||
if len(parts) == 4 {
|
||||
elems = append(elems, string(parts[0]), string(parts[1]))
|
||||
|
||||
val, err := strconv.Atoi(string(parts[2]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elems = append(elems, int64(val))
|
||||
|
||||
val2, err := strconv.Atoi(string(parts[3]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elems = append(elems, int64(val2))
|
||||
} else {
|
||||
elems = append(elems, string(parts[0]))
|
||||
parts = parts[1:]
|
||||
|
||||
val, err := strconv.Atoi(string(parts[len(parts)-1]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
val2, err := strconv.Atoi(string(parts[len(parts)-2]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appKey := bytes.Join(parts[:len(parts)-3], []byte("/"))
|
||||
elems = append(elems, string(appKey), int64(val), int64(val2))
|
||||
}
|
||||
return orderedcode.Append(nil, elems...)
|
||||
case keyIsHash(key):
|
||||
return orderedcode.Append(nil, "tx.hash", string(key))
|
||||
default:
|
||||
return nil, fmt.Errorf("key %q is in the wrong format", string(key))
|
||||
}
|
||||
}
|
||||
|
||||
func convertEvidence(key keyID, newPrefix int64) ([]byte, error) {
|
||||
parts := bytes.Split(key[1:], []byte("/"))
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("evidence key is malformed with %d parts not 2",
|
||||
len(parts))
|
||||
}
|
||||
|
||||
hb, err := hex.DecodeString(string(parts[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
evidenceHash, err := hex.DecodeString(string(parts[1]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderedcode.Append(nil, newPrefix, binary.BigEndian.Uint64(hb), string(evidenceHash))
|
||||
}
|
||||
|
||||
func replaceKey(db dbm.DB, key keyID, gooseFn migrateFunc) error {
|
||||
exists, err := db.Has(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
newKey, err := gooseFn(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val, err := db.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
batch := db.NewBatch()
|
||||
|
||||
if err = batch.Set(newKey, val); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = batch.Delete(key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 10% of the time, force a write to disk, but mostly don't,
|
||||
// because it's faster.
|
||||
if rand.Intn(100)%10 == 0 { // nolint:gosec
|
||||
if err = batch.WriteSync(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = batch.Write(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = batch.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate converts all legacy key formats to new key formats. The
|
||||
// operation is idempotent, so it's safe to resume a failed
|
||||
// operation. The operation is somewhat parallelized, relying on the
|
||||
// concurrency safety of the underlying databases.
|
||||
//
|
||||
// Migrate has "continue on error" semantics and will iterate through
|
||||
// all legacy keys attempt to migrate them, and will collect all
|
||||
// errors and will return only at the end of the operation.
|
||||
//
|
||||
// The context allows for a safe termination of the operation
|
||||
// (e.g connected to a singal handler,) to abort the operation
|
||||
// in-between migration operations.
|
||||
func Migrate(ctx context.Context, db dbm.DB) error {
|
||||
keys, err := getAllLegacyKeys(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
numWorkers := runtime.NumCPU()
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
errs := make(chan error, numWorkers)
|
||||
|
||||
keyCh := makeKeyChan(keys)
|
||||
|
||||
// run migrations.
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for key := range keyCh {
|
||||
err := replaceKey(db, key, migarateKey)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// collect and process the errors.
|
||||
errStrs := []string{}
|
||||
signal := make(chan struct{})
|
||||
go func() {
|
||||
defer close(signal)
|
||||
for err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
errStrs = append(errStrs, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for everything to be done.
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
<-signal
|
||||
|
||||
// check the error results
|
||||
if len(errs) != 0 {
|
||||
return errors.Errorf("encountered errors during migration: %v", errStrs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user