go-ethereum/eth/downloader/statesync.go
2017-06-19 13:15:44 +03:00

339 lines
9.5 KiB
Go

// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package downloader
import (
"fmt"
"hash"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/crypto/sha3"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/trie"
)
type stateReq struct {
items []common.Hash
tasks map[common.Hash]*stateTask
timeout time.Duration
timer *time.Timer // fires when timeout has elapsed
peer *peer // the peer that we're requesting from
response [][]byte // the response. this is nil for timed-out requests.
}
func (req *stateReq) timedOut() bool {
return req.response == nil
}
type stateSyncStats struct {
done uint64 // number of entries pulled
pending uint64 // number of pending entries
}
// syncPivotState starts downloading state with the given root hash.
func (d *Downloader) syncState(root common.Hash) *stateSync {
s := newStateSync(d, root)
select {
case d.stateSyncStart <- s:
case <-d.quitCh:
s.err = errCancelStateFetch
close(s.done)
}
return s
}
// stateFetcher manages the active state sync and accepts requests
// on its behalf.
func (d *Downloader) stateFetcher() {
for {
select {
case s := <-d.stateSyncStart:
for next := s; next != nil; {
next = d.runStateSync(next)
}
case <-d.stateCh:
// Ignore state responses while no sync is running.
case <-d.quitCh:
return
}
}
}
// runStateSync runs s until it completes or another stateSync is requested.
func (d *Downloader) runStateSync(s *stateSync) *stateSync {
var (
activeReqs = make(map[string]*stateReq)
finishedReqs []*stateReq
timeout = make(chan *stateReq)
)
defer func() {
// Cancel active request timers on exit. Also set peers to idle so they're
// available for the next sync.
for _, req := range activeReqs {
req.timer.Stop()
req.peer.SetNodeDataIdle(0)
}
}()
// Run the state sync.
go s.run()
defer s.Cancel()
for {
// Enable sending of the first buffered element if there is one.
var deliverReq *stateReq
var deliverReqCh chan *stateReq
if len(finishedReqs) > 0 {
deliverReq = finishedReqs[0]
deliverReqCh = s.deliver
}
select {
// The stateSync lifecycle:
case next := <-d.stateSyncStart:
return next
case <-s.done:
return nil
// Send the next finished request to the current sync:
case deliverReqCh <- deliverReq:
finishedReqs = append(finishedReqs[:0], finishedReqs[1:]...)
// Handle incoming state packs:
case pack := <-d.stateCh:
req := activeReqs[pack.PeerId()]
if req == nil {
log.Debug("Unrequested node data", "peer", pack.PeerId(), "len", pack.Items())
continue
}
delete(activeReqs, pack.PeerId())
req.timer.Stop()
req.response = pack.(*statePack).states
finishedReqs = append(finishedReqs, req)
// Handle timed-out requests:
case req := <-timeout:
if activeReqs[req.peer.id] != req {
continue // ignore old timeout
}
finishedReqs = append(finishedReqs, req)
delete(activeReqs, req.peer.id)
// Track outgoing state requests:
case req := <-d.trackStateReq:
activeReqs[req.peer.id] = req
req.timer = time.AfterFunc(req.timeout, func() {
select {
case timeout <- req:
case <-s.done:
// Prevent leaking of timer goroutines in the unlikely case where a
// timer is fired just before exiting runStateSync.
}
})
}
}
}
// stateSync schedules requests for downloading a particular state root.
type stateSync struct {
d *Downloader
sched *state.StateSync
keccak hash.Hash
tasksAvailable map[common.Hash]*stateTask
deliver chan *stateReq
cancel chan struct{}
cancelOnce sync.Once
done chan struct{}
err error // set after done is closed
}
type stateTask struct {
triedPeers map[string]struct{}
numTries int
}
func newStateSync(d *Downloader, root common.Hash) *stateSync {
return &stateSync{
d: d,
sched: state.NewStateSync(root, d.stateDB),
keccak: sha3.NewKeccak256(),
tasksAvailable: make(map[common.Hash]*stateTask),
deliver: make(chan *stateReq),
cancel: make(chan struct{}),
done: make(chan struct{}, 1),
}
}
// Wait blocks until the sync is done or canceled.
func (s *stateSync) Wait() error {
<-s.done
return s.err
}
// Cancel cancels the sync and waits until it has shut down.
func (s *stateSync) Cancel() error {
s.cancelOnce.Do(func() { close(s.cancel) })
return s.Wait()
}
func (s *stateSync) run() {
s.err = s.loop()
close(s.done)
}
func (s *stateSync) loop() error {
newPeer := make(chan *peer, 200)
peerSub := s.d.peers.SubscribeNewPeers(newPeer)
defer peerSub.Unsubscribe()
for s.sched.Pending() > 0 {
if err := s.assignTasks(); err != nil {
return err
}
select {
case <-newPeer:
// assign new tasks
case <-s.cancel:
return errCancelStateFetch
case req := <-s.deliver:
// response or timeout
log.Trace("Received node data response", "peer", req.peer.id, "count", len(req.items), "timeout", req.timedOut())
req.peer.SetNodeDataIdle(len(req.response))
procStart := time.Now()
n, err := s.process(req)
if err != nil {
log.Warn("Node data write error", "err", err)
return err
}
if len(req.items) == 1 && req.timedOut() {
log.Warn("Node data timeout, dropping peer", "peer", req.peer.id)
s.d.dropPeer(req.peer.id)
}
s.updateStats(n, time.Since(procStart))
}
}
return nil
}
func (s *stateSync) sendReq(req *stateReq) {
req.peer.log.Trace("Requesting new batch of data", "type", "state", "count", len(req.items))
select {
case s.d.trackStateReq <- req:
req.peer.FetchNodeData(req.items)
case <-s.cancel:
}
}
func (s *stateSync) assignTasks() error {
peers, _ := s.d.peers.NodeDataIdlePeers()
for _, p := range peers {
cap := p.NodeDataCapacity(s.d.requestRTT())
req := &stateReq{peer: p, timeout: s.d.requestTTL()}
if err := s.popTasks(cap, req); err != nil {
return err
}
if len(req.items) > 0 {
s.sendReq(req)
}
}
return nil
}
func (s *stateSync) popTasks(n int, req *stateReq) error {
// Refill available tasks from the scheduler.
if len(s.tasksAvailable) < n {
new := s.sched.Missing(n - len(s.tasksAvailable))
for _, hash := range new {
s.tasksAvailable[hash] = &stateTask{make(map[string]struct{}), 0}
}
}
// Find tasks that haven't been tried with the request's peer.
req.items = make([]common.Hash, 0, n)
req.tasks = make(map[common.Hash]*stateTask, n)
for hash, t := range s.tasksAvailable {
if len(req.items) == n {
break
}
if _, ok := t.triedPeers[req.peer.id]; ok {
continue
}
t.triedPeers[req.peer.id] = struct{}{}
t.numTries++
req.items = append(req.items, hash)
req.tasks[hash] = t
delete(s.tasksAvailable, hash)
}
return nil
}
func (s *stateSync) process(req *stateReq) (nproc int, err error) {
batch := s.d.stateDB.NewBatch()
for _, blob := range req.response {
hash, err := s.processNodeData(blob, batch)
if err != nil && err != trie.ErrNotRequested {
return 0, fmt.Errorf("invalid state node %s: %v", hash.TerminalString(), err)
} else if err == nil {
nproc++
delete(req.tasks, hash)
}
}
if err := batch.Write(); err != nil {
return 0, err
}
if nproc > 0 && atomic.LoadUint32(&s.d.fsPivotFails) > 1 {
log.Trace("Fast-sync progressed, resetting fail counter", "previous", atomic.LoadUint32(&s.d.fsPivotFails))
atomic.StoreUint32(&s.d.fsPivotFails, 1) // Don't ever reset to 0, as that will unlock the pivot block
}
// Put unfulfilled tasks back.
npeers := s.d.peers.Len()
for hash, task := range req.tasks {
if len(req.response) > 0 || req.timedOut() {
// Ensure that the item will be retried because it may have been excluded due
// to a protocol limit.
delete(task.triedPeers, req.peer.id)
}
// Check retry limit.
if len(task.triedPeers) >= npeers {
return nproc, fmt.Errorf("state node %s failed with all peers (%d tries, %d peers)", hash.TerminalString(), len(task.triedPeers), npeers)
}
s.tasksAvailable[hash] = task
}
return nproc, nil
}
func (s *stateSync) processNodeData(blob []byte, batch ethdb.Batch) (common.Hash, error) {
res := trie.SyncResult{Data: blob}
s.keccak.Reset()
s.keccak.Write(blob)
s.keccak.Sum(res.Hash[:0])
_, _, err := s.sched.Process([]trie.SyncResult{res}, batch)
return res.Hash, err
}
func (s *stateSync) updateStats(processed int, duration time.Duration) {
s.d.syncStatsLock.Lock()
defer s.d.syncStatsLock.Unlock()
s.d.syncStatsState.pending = uint64(s.sched.Pending())
s.d.syncStatsState.done += uint64(processed)
log.Info("Imported new state entries", "count", processed, "elapsed", common.PrettyDuration(duration), "processed", s.d.syncStatsState.done, "pending", s.d.syncStatsState.pending, "retry", len(s.tasksAvailable))
}