mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
Add account state migration support to go-ethereum.
Adds support to migrate the account state for a state snapshot, which is identified by its corresponding root hash. It is a low-level migration between two ethdb.Database instances in its merklized form; encompassing the account states, storage values, and code segments within the snapshot. The design has been rigorously optimized for performance: - Concurrent traversal of account state and storage tries - Concurrent traversal of paths within a given trie - Worker pools for database reads and writes - Breaking down the migration into batches - Sorting keys to align with what LevelDB was designed for - Pipelining the various tasks outlined above
This commit is contained in:
parent
34c85def3e
commit
9a9d03c289
4 changed files with 703 additions and 0 deletions
526
core/state/migrator.go
Normal file
526
core/state/migrator.go
Normal file
|
|
@ -0,0 +1,526 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Migrator is an entity that copies over account state at a particular snapshot
|
||||||
|
// from a source database to a destination database. Each Migrator instance's
|
||||||
|
// lifetime only lasts for a single migration.
|
||||||
|
//
|
||||||
|
// A Migrator is meant to be invoked as follows:
|
||||||
|
//
|
||||||
|
// rootHash := common.Hash{}
|
||||||
|
// srcDB := ethdb.NewLDBDatabase("existing", /* cache */ 1024, /* handles */ 1024)
|
||||||
|
// dstDB := ethdb.NewLDBDatabase("migrated", /* cache */ 1024, /* handles */ 1024)
|
||||||
|
// m := NewStateMigrator(dstDB, srcDB, rootHash, /* numWorkers */ 1, /* batchSize */ 1)
|
||||||
|
// m.Start()
|
||||||
|
// if err := m.Wait(); err != nil {
|
||||||
|
// // handle error
|
||||||
|
// . }
|
||||||
|
//
|
||||||
|
// A typical state snapshot contains such a large amount of data that it can be prohibitively
|
||||||
|
// expensive to perform if implemented naively. As a result, the Migrator has taken on the following
|
||||||
|
// architecture below, which will be described at a high-level. It is worth noting that this
|
||||||
|
// framework has been specifically designed for LevelDB-based databases. More information can
|
||||||
|
// be found by looking at each component's corresponding struct. The components are managed as a
|
||||||
|
// a set of go-routines (boxes) connected by channels (arrows) operated within an errgroup.Group.
|
||||||
|
//
|
||||||
|
// unprocessed
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// | |
|
||||||
|
// v |
|
||||||
|
// ----------- unsorted ---------- sorted ---------- results ---------- unsaved ----------
|
||||||
|
// | batcher |-----------> | sorter |--------->| getter |----------->| fanout |---------->| putter |
|
||||||
|
// ----------- ---------- ---------- ---------- ----------
|
||||||
|
//
|
||||||
|
// Components:
|
||||||
|
// - batcher: schedules and aggregates a group of database keys to look up in the source database
|
||||||
|
// - sorter: sorts the keys to make the accesses to LevelDB more efficient
|
||||||
|
// - getter: performs lookups in source database
|
||||||
|
// - fanout: sends retrieved key-value pairs to multiple consumers
|
||||||
|
// - putter: stores key-value pairs in destination database
|
||||||
|
//
|
||||||
|
type Migrator struct {
|
||||||
|
group *errgroup.Group
|
||||||
|
|
||||||
|
resultFanout *resultFanout
|
||||||
|
batcher *batcher
|
||||||
|
sorter *sorter
|
||||||
|
getter *getter
|
||||||
|
putter *putter
|
||||||
|
|
||||||
|
numWorkers int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMigrator returns a new Migrator instance that migrates account information from the
|
||||||
|
// source database srcDB to the destination database dstDB for the corresponding account state at
|
||||||
|
// the specified rootHash. Additionally the number of workers for getting/putting data from the two
|
||||||
|
// databases numWorkers is specified (where numWorkers are individually used for getting and
|
||||||
|
// putting) and the maximum number of items to migrate at once batchSize.
|
||||||
|
func NewMigrator(dstDB ethdb.Database, srcDB trie.DatabaseReader, rootHash common.Hash, numWorkers, batchSize int) *Migrator {
|
||||||
|
// Channel sizes of one allow one batch to be buffered so that the batcher, sorter, and getter
|
||||||
|
// do not block one another.
|
||||||
|
unsorted := make(chan []common.Hash, 1)
|
||||||
|
sorted := make(chan []common.Hash, 1)
|
||||||
|
// Channel sizes of numWorkers allow the getter, resultFanout, and putter to not block.
|
||||||
|
results := make(chan []trie.SyncResult, numWorkers)
|
||||||
|
unprocessed := make(chan []trie.SyncResult, numWorkers)
|
||||||
|
unsaved := make(chan []trie.SyncResult, numWorkers)
|
||||||
|
// Round up to ensure that chunkSize is at least 1 when numWorkers > batchSize.
|
||||||
|
chunkSize := (batchSize + numWorkers - 1) / numWorkers
|
||||||
|
// The errgroup is used to manage all of the processing components (including workers).
|
||||||
|
group, ctx := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
|
return &Migrator{
|
||||||
|
group: group,
|
||||||
|
resultFanout: newResultFanout(ctx, results, unprocessed, unsaved),
|
||||||
|
batcher: newBatcher(ctx, rootHash, unsorted, unprocessed, batchSize),
|
||||||
|
sorter: newSorter(ctx, unsorted, sorted),
|
||||||
|
getter: newGetter(ctx, srcDB, sorted, results, chunkSize),
|
||||||
|
putter: newPutter(ctx, dstDB, unsaved),
|
||||||
|
numWorkers: numWorkers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait blocks until the state data migration has finished or encountered an error.
|
||||||
|
func (m *Migrator) Wait() error {
|
||||||
|
return m.group.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins the state data migration process.
|
||||||
|
func (m *Migrator) Start() {
|
||||||
|
m.group.Go(m.batcher.run)
|
||||||
|
m.group.Go(m.sorter.run)
|
||||||
|
m.group.Go(m.resultFanout.run)
|
||||||
|
m.group.Go(m.getter.runManager)
|
||||||
|
for i := 0; i < m.numWorkers; i++ {
|
||||||
|
m.group.Go(m.getter.runWorker)
|
||||||
|
m.group.Go(m.putter.runWorker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A resultFanout broadcasts []*trie.SyncResult batches to consumers.
|
||||||
|
//
|
||||||
|
// consumer[0] ----------
|
||||||
|
// -------------------> | worker |
|
||||||
|
// | ----------
|
||||||
|
// |
|
||||||
|
// ---------- consumer[1] ----------
|
||||||
|
// producer ----->| fanout |--------------> | worker |
|
||||||
|
// ---------- ----------
|
||||||
|
// |
|
||||||
|
// | consumer[2] ----------
|
||||||
|
// -------------------> | worker |
|
||||||
|
// ----------
|
||||||
|
type resultFanout struct {
|
||||||
|
ctx context.Context
|
||||||
|
producer <-chan []trie.SyncResult
|
||||||
|
consumers []chan<- []trie.SyncResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// newResultFanout returns a new resultFanout instance monitoring the context ctx, ingesting
|
||||||
|
// results from producer and then broadcasting them out to consumers.
|
||||||
|
//
|
||||||
|
// Each of the consumers will be closed when the resultFanout has completed, which occurs either
|
||||||
|
// by having its incoming producer channel closed or being signaled via context.
|
||||||
|
func newResultFanout(ctx context.Context, producer <-chan []trie.SyncResult, consumers ...chan<- []trie.SyncResult) *resultFanout {
|
||||||
|
return &resultFanout{
|
||||||
|
ctx,
|
||||||
|
producer,
|
||||||
|
consumers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *resultFanout) run() error {
|
||||||
|
defer f.closeConsumers()
|
||||||
|
|
||||||
|
for results := range f.producer {
|
||||||
|
for _, consumer := range f.consumers {
|
||||||
|
select {
|
||||||
|
case <-f.ctx.Done():
|
||||||
|
return nil
|
||||||
|
case consumer <- results:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *resultFanout) closeConsumers() {
|
||||||
|
for _, c := range f.consumers {
|
||||||
|
close(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A batcher schedules and groups hashes together for other components to operate upon as a unit
|
||||||
|
// of work.
|
||||||
|
type batcher struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
sched *trie.Sync
|
||||||
|
|
||||||
|
maxBatchSize int
|
||||||
|
|
||||||
|
reqs chan<- []common.Hash
|
||||||
|
resps <-chan []trie.SyncResult
|
||||||
|
|
||||||
|
reqsInflight int
|
||||||
|
queue []common.Hash
|
||||||
|
putter *droppingPutter
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBatcher returns a new batcher instance monitoring the context ctx, starting to schedule hashes
|
||||||
|
// to look up from the rootHash of the account state trie, sending batches up to size maxBatchSize
|
||||||
|
// through reqs and then creating new batches based on the results received through resps.
|
||||||
|
//
|
||||||
|
// resps will be closed when the batcher has completed, which occurs either by running out of hashes
|
||||||
|
// to retrieve or being signaled via context.
|
||||||
|
func newBatcher(ctx context.Context, rootHash common.Hash, reqs chan<- []common.Hash, resps <-chan []trie.SyncResult, maxBatchSize int) *batcher {
|
||||||
|
return &batcher{
|
||||||
|
ctx: ctx,
|
||||||
|
sched: NewStateSync(rootHash, &emptyTrieReader{}),
|
||||||
|
reqs: reqs,
|
||||||
|
resps: resps,
|
||||||
|
maxBatchSize: maxBatchSize,
|
||||||
|
putter: &droppingPutter{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A droppingPutter is an implementation of ethdb.Putter that discards the values inserted into it.
|
||||||
|
// The key-value pairs are placed into the destination database by the putter component instead.
|
||||||
|
//
|
||||||
|
// The sync.Trie logic uses a ethdb.Putter to flush requests that have been completed. The migrator
|
||||||
|
// does this independently within the putter component for two reasons:
|
||||||
|
// 1. Performance: the putter component has been designed to take large, key-sorted batches whereas
|
||||||
|
// the trie.sync logic inserts results one-at-time in the order they were requested.
|
||||||
|
// 2. Memory efficiency: the trie.Sync logic holds on to completed requests
|
||||||
|
// that have not been stored until trie.Sync.Commit has been called. Unlike the original
|
||||||
|
// peer-based sync use case, there can be millions of requests needing to be stored at any
|
||||||
|
// given time which can lead to holding onto their corresponding allocated memory for too longs.
|
||||||
|
type droppingPutter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*droppingPutter) Put(key []byte, value []byte) error {
|
||||||
|
// Does nothing.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// An emptyTrieReader is an implementation of a trie.DatabaseReader that does not contain any values
|
||||||
|
// to read.
|
||||||
|
//
|
||||||
|
// trie.Sync uses a trie.Database reader to check whether or not a value being retrieved is already
|
||||||
|
// present in the destination database (which is useful for syncing from peers that can be cancelled
|
||||||
|
// and restarted). However, the Migrator performs the copy within a single invocation so this check
|
||||||
|
// does not add value and in fact incurs a performance penalty as database reads can be expensive.
|
||||||
|
type emptyTrieReader struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*emptyTrieReader) Get(key []byte) (value []byte, err error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*emptyTrieReader) Has(key []byte) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *batcher) run() error {
|
||||||
|
defer close(b.reqs)
|
||||||
|
|
||||||
|
// Send root hash.
|
||||||
|
b.fillQueue()
|
||||||
|
b.sendNextBatch()
|
||||||
|
|
||||||
|
for resps := range b.resps {
|
||||||
|
b.processResponses(resps)
|
||||||
|
b.fillQueue()
|
||||||
|
|
||||||
|
if b.noRequestsPending() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.shouldSendNextBatch() {
|
||||||
|
if !b.sendNextBatch() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *batcher) processResponses(resps []trie.SyncResult) {
|
||||||
|
b.sched.Process(resps)
|
||||||
|
b.sched.Commit(b.putter)
|
||||||
|
b.reqsInflight -= len(resps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *batcher) sendNextBatch() bool {
|
||||||
|
select {
|
||||||
|
case <-b.ctx.Done():
|
||||||
|
return false
|
||||||
|
case b.reqs <- b.queue:
|
||||||
|
b.reqsInflight += len(b.queue)
|
||||||
|
b.queue = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *batcher) shouldSendNextBatch() bool {
|
||||||
|
return b.isIdle() || b.isUnderCapacity()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *batcher) isIdle() bool {
|
||||||
|
return b.reqsInflight == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// The batcher system only allows 2 * b.maxBatchsize requests to be inflight at a given time
|
||||||
|
// in order to not overwhelm the system. At the same time, the system wants to batch up enough
|
||||||
|
// requests to amortize overheads and exploit sequential locality between request keys.
|
||||||
|
//
|
||||||
|
// Not that deadlock is prevented in the event len(b.queue) < b.maxBatchSize, as the system will
|
||||||
|
// eventually become idle as detected by batcher.isIdle, thus allowing the next batch to be sent
|
||||||
|
// when batcher.shouldSendNextBatch is invoked (from receiving the batch that results in idleness).
|
||||||
|
func (b *batcher) isUnderCapacity() bool {
|
||||||
|
return len(b.queue) >= b.maxBatchSize && b.reqsInflight+len(b.queue) < 2*b.maxBatchSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *batcher) noRequestsPending() bool {
|
||||||
|
return b.sched.Pending() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *batcher) fillQueue() {
|
||||||
|
if len(b.queue) < b.maxBatchSize {
|
||||||
|
b.queue = append(b.queue, b.sched.Missing(b.maxBatchSize-len(b.queue))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sorter sorts lists of hashes.
|
||||||
|
type sorter struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
unsorted <-chan []common.Hash
|
||||||
|
sorted chan<- []common.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSorter returns a new sorter instance monitoring the context ctx, taking groups of hashes from
|
||||||
|
// unsorted and then outputting them to sorted once the sort is complete.
|
||||||
|
//
|
||||||
|
// sorted will be closed when the sort has completed, which occurs either by running out of hashes to
|
||||||
|
// sort or being signaled via context.
|
||||||
|
func newSorter(ctx context.Context, unsorted <-chan []common.Hash, sorted chan<- []common.Hash) *sorter {
|
||||||
|
return &sorter{
|
||||||
|
ctx: ctx,
|
||||||
|
unsorted: unsorted,
|
||||||
|
sorted: sorted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sorter) run() error {
|
||||||
|
defer close(s.sorted)
|
||||||
|
|
||||||
|
for data := range s.unsorted {
|
||||||
|
sort.Sort(keys(data))
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return nil
|
||||||
|
case s.sorted <- data:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type keys []common.Hash
|
||||||
|
|
||||||
|
func (b keys) Len() int {
|
||||||
|
return len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b keys) Less(i, j int) bool {
|
||||||
|
return bytes.Compare(b[i].Bytes(), b[j].Bytes()) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b keys) Swap(i, j int) {
|
||||||
|
b[j], b[i] = b[i], b[j]
|
||||||
|
}
|
||||||
|
|
||||||
|
// A getter retrieves values from an underlying database.
|
||||||
|
//
|
||||||
|
// The getter internally uses workers to concurrently retrieve values within a batch sent to it.
|
||||||
|
// Requests are sent to a manager which then breaks the batch into smaller chunks that are then
|
||||||
|
// distributed to workers within a pool to perform the actual retrievals.
|
||||||
|
//
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// | getter |
|
||||||
|
// | ---------- |
|
||||||
|
// | ----> | worker |---- |
|
||||||
|
// | | ---------- | |
|
||||||
|
// | | | |
|
||||||
|
// | ----------- chunks | ---------- | |
|
||||||
|
// hashes ----|--->| manager |--------------> | worker |---------|---> results
|
||||||
|
// | ----------- | ---------- | |
|
||||||
|
// | | | |
|
||||||
|
// | | ---------- | |
|
||||||
|
// | ----> | worker |---- |
|
||||||
|
// | ---------- |
|
||||||
|
// | |
|
||||||
|
// ----------------------------------------------------
|
||||||
|
//
|
||||||
|
type getter struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
db trie.DatabaseReader
|
||||||
|
|
||||||
|
hashes <-chan []common.Hash
|
||||||
|
results chan<- []trie.SyncResult
|
||||||
|
chunks chan []common.Hash
|
||||||
|
|
||||||
|
chunkSize int
|
||||||
|
|
||||||
|
closeChannels sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// newGetter returns a new getter instance monitoring the context ctx, taking groups of keys from
|
||||||
|
// hashes and the resulting key-value pairs found in db through results. The manager sends chunks
|
||||||
|
// of maximum size chunkSize to its workers.
|
||||||
|
//
|
||||||
|
// results will be closed when the getter has completed, which occurs either by running out of
|
||||||
|
// hashes to look up or being signaled via context.
|
||||||
|
func newGetter(ctx context.Context, db trie.DatabaseReader, hashes <-chan []common.Hash, results chan<- []trie.SyncResult, chunkSize int) *getter {
|
||||||
|
return &getter{
|
||||||
|
ctx: ctx,
|
||||||
|
db: db,
|
||||||
|
hashes: hashes,
|
||||||
|
results: results,
|
||||||
|
chunks: make(chan []common.Hash),
|
||||||
|
chunkSize: chunkSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *getter) runManager() error {
|
||||||
|
defer g.closeOutboundChannels()
|
||||||
|
|
||||||
|
for hashes := range g.hashes {
|
||||||
|
for _, chunk := range g.splitIntoChunks(hashes) {
|
||||||
|
select {
|
||||||
|
case <-g.ctx.Done():
|
||||||
|
return nil
|
||||||
|
case g.chunks <- chunk:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeOutboundChannels closes the channels that output data from the getter (as shown in the
|
||||||
|
// block diagram above). It can be called by either the getter manager or workers as there are
|
||||||
|
// situations where either could halt execution of the getter (and Migrator as a whole). This is
|
||||||
|
// performed implicitly by closing channels. If a worker fails it will close the channel,
|
||||||
|
// otherwise the manager will close the channel. This method uses a sync.Once to
|
||||||
|
// ensure that multiple closes are not applied to the outbound channels.
|
||||||
|
func (g *getter) closeOutboundChannels() {
|
||||||
|
g.closeChannels.Do(func() {
|
||||||
|
close(g.chunks)
|
||||||
|
g.chunks = nil
|
||||||
|
close(g.results)
|
||||||
|
g.chunks = nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *getter) splitIntoChunks(hashes []common.Hash) [][]common.Hash {
|
||||||
|
var chunks [][]common.Hash
|
||||||
|
for len(hashes) > g.chunkSize {
|
||||||
|
hashes, chunks = hashes[g.chunkSize:], append(chunks, hashes[:g.chunkSize])
|
||||||
|
}
|
||||||
|
chunks = append(chunks, hashes)
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *getter) runWorker() error {
|
||||||
|
defer g.closeOutboundChannels()
|
||||||
|
|
||||||
|
for hashes := range g.chunks {
|
||||||
|
var results []trie.SyncResult
|
||||||
|
for _, hash := range hashes {
|
||||||
|
data, err := g.db.Get(hash.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error retrieving %s from database: %s", hex.EncodeToString(hash.Bytes()), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
result := trie.SyncResult{Hash: hash, Data: data}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-g.ctx.Done():
|
||||||
|
return nil
|
||||||
|
case g.results <- results:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A putter stores key-value pairs into an underlying database.
|
||||||
|
//
|
||||||
|
// ------------------------
|
||||||
|
// | putter |
|
||||||
|
// | ---------- |
|
||||||
|
// | ----> | worker | |
|
||||||
|
// | | ---------- |
|
||||||
|
// | | |
|
||||||
|
// | | ---------- |
|
||||||
|
// results ----|-------> | worker | |
|
||||||
|
// | | ---------- |
|
||||||
|
// | | |
|
||||||
|
// | | ---------- |
|
||||||
|
// | ----> | worker | |
|
||||||
|
// | ---------- |
|
||||||
|
// | |
|
||||||
|
// ------------------------
|
||||||
|
//
|
||||||
|
type putter struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
db ethdb.Database
|
||||||
|
|
||||||
|
results <-chan []trie.SyncResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPutter returns a new putter instance monitoring the context ctx, taking groups of key-value
|
||||||
|
// pairs from results to store in db.
|
||||||
|
func newPutter(ctx context.Context, db ethdb.Database, results <-chan []trie.SyncResult) *putter {
|
||||||
|
return &putter{
|
||||||
|
ctx: ctx,
|
||||||
|
db: db,
|
||||||
|
results: results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *putter) runWorker() error {
|
||||||
|
for batch := range p.results {
|
||||||
|
writeBatch := p.db.NewBatch()
|
||||||
|
for _, r := range batch {
|
||||||
|
if err := writeBatch.Put(r.Hash.Bytes(), r.Data); err != nil {
|
||||||
|
return fmt.Errorf("error inserting pair (%s, %s) to batch: %s", hex.EncodeToString(r.Hash.Bytes()), hex.EncodeToString(r.Data), err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := writeBatch.Write(); err != nil {
|
||||||
|
return fmt.Errorf("error batch into database: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
105
core/state/migrator_test.go
Normal file
105
core/state/migrator_test.go
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMigrator(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
numWorkers, batchSize int
|
||||||
|
}{
|
||||||
|
{1, 1},
|
||||||
|
{2, 1},
|
||||||
|
{1, 2},
|
||||||
|
{2, 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
name := fmt.Sprintf("%d_Workers_%d_BatchSize", tc.numWorkers, tc.batchSize)
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
srcDB, srcRoot, srcAccounts := makeTestState()
|
||||||
|
// Ensure nodes are persisted to the underlying database.
|
||||||
|
srcDB.TrieDB().Commit(srcRoot, false)
|
||||||
|
dstDB := ethdb.NewMemDatabase()
|
||||||
|
|
||||||
|
m := NewMigrator(dstDB, srcDB.TrieDB().DiskDB(), srcRoot, tc.numWorkers, tc.batchSize)
|
||||||
|
m.Start()
|
||||||
|
if err := m.Wait(); err != nil {
|
||||||
|
t.Fatalf("m.Wait() = %v, want <nil>", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStateAccounts(t, dstDB, srcRoot, srcAccounts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrator_SrcDBReturnsError_ShouldReturnError(t *testing.T) {
|
||||||
|
srcDB := &failingDB{}
|
||||||
|
dstDB := ethdb.NewMemDatabase()
|
||||||
|
|
||||||
|
m := NewMigrator(dstDB, srcDB, common.Hash{} /* numWorkers */, 1 /* batchSize */, 1)
|
||||||
|
m.Start()
|
||||||
|
|
||||||
|
if err := m.Wait(); err == nil {
|
||||||
|
t.Fatal("m.Wait() = <nil>, want <error>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrator_DstDBReturnsError_ShouldReturnError(t *testing.T) {
|
||||||
|
srcDB, srcRoot, _ := makeTestState()
|
||||||
|
// Ensure nodes are persisted to the underlying database.
|
||||||
|
srcDB.TrieDB().Commit(srcRoot, false)
|
||||||
|
dstDB := &failingDB{}
|
||||||
|
|
||||||
|
m := NewMigrator(dstDB, srcDB.TrieDB().DiskDB(), srcRoot /* numWorkers */, 1 /* batchSize */, 1)
|
||||||
|
m.Start()
|
||||||
|
|
||||||
|
if err := m.Wait(); err == nil {
|
||||||
|
t.Fatal("m.Wait() = <nil>, want <error>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// failingDB implements trie.DatabaseReader and ethdb.Database, but
|
||||||
|
// returns a dummy error any time a method that returns an error is invoked.
|
||||||
|
type failingDB struct{}
|
||||||
|
|
||||||
|
func (*failingDB) Put(key, value []byte) error {
|
||||||
|
return errors.New("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*failingDB) Delete(key []byte) error {
|
||||||
|
return errors.New("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*failingDB) Get(key []byte) (value []byte, err error) {
|
||||||
|
return nil, errors.New("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*failingDB) Has(key []byte) (bool, error) {
|
||||||
|
return false, errors.New("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*failingDB) Close() {}
|
||||||
|
|
||||||
|
func (*failingDB) NewBatch() ethdb.Batch {
|
||||||
|
return &failingBatch{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type failingBatch struct {
|
||||||
|
failingDB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*failingBatch) ValueSize() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*failingBatch) Write() error {
|
||||||
|
return errors.New("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*failingBatch) Reset() {}
|
||||||
66
vendor/golang.org/x/sync/errgroup/errgroup.go
generated
vendored
Normal file
66
vendor/golang.org/x/sync/errgroup/errgroup.go
generated
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright 2016 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package errgroup provides synchronization, error propagation, and Context
|
||||||
|
// cancelation for groups of goroutines working on subtasks of a common task.
|
||||||
|
package errgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Group is a collection of goroutines working on subtasks that are part of
|
||||||
|
// the same overall task.
|
||||||
|
//
|
||||||
|
// A zero Group is valid and does not cancel on error.
|
||||||
|
type Group struct {
|
||||||
|
cancel func()
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
errOnce sync.Once
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext returns a new Group and an associated Context derived from ctx.
|
||||||
|
//
|
||||||
|
// The derived Context is canceled the first time a function passed to Go
|
||||||
|
// returns a non-nil error or the first time Wait returns, whichever occurs
|
||||||
|
// first.
|
||||||
|
func WithContext(ctx context.Context) (*Group, context.Context) {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
return &Group{cancel: cancel}, ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait blocks until all function calls from the Go method have returned, then
|
||||||
|
// returns the first non-nil error (if any) from them.
|
||||||
|
func (g *Group) Wait() error {
|
||||||
|
g.wg.Wait()
|
||||||
|
if g.cancel != nil {
|
||||||
|
g.cancel()
|
||||||
|
}
|
||||||
|
return g.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go calls the given function in a new goroutine.
|
||||||
|
//
|
||||||
|
// The first call to return a non-nil error cancels the group; its error will be
|
||||||
|
// returned by Wait.
|
||||||
|
func (g *Group) Go(f func() error) {
|
||||||
|
g.wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer g.wg.Done()
|
||||||
|
|
||||||
|
if err := f(); err != nil {
|
||||||
|
g.errOnce.Do(func() {
|
||||||
|
g.err = err
|
||||||
|
if g.cancel != nil {
|
||||||
|
g.cancel()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
|
|
@ -778,6 +778,12 @@
|
||||||
"revision": "b4690f45fa1cafc47b1c280c2e75116efe40cc13",
|
"revision": "b4690f45fa1cafc47b1c280c2e75116efe40cc13",
|
||||||
"revisionTime": "2017-02-15T08:41:58Z"
|
"revisionTime": "2017-02-15T08:41:58Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "iEK5hCRfrkdc1JOJsaiWuymHmeQ=",
|
||||||
|
"path": "golang.org/x/sync/errgroup",
|
||||||
|
"revision": "e225da77a7e68af35c70ccbf71af2b83e6acac3c",
|
||||||
|
"revisionTime": "2019-02-15T22:36:53Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "4TEYFKrAUuwBMqExjQBsnf/CgjQ=",
|
"checksumSHA1": "4TEYFKrAUuwBMqExjQBsnf/CgjQ=",
|
||||||
"path": "golang.org/x/sync/syncmap",
|
"path": "golang.org/x/sync/syncmap",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue