core/state: lock-free subfetcher

This commit is contained in:
Arran Schlosberg 2024-11-21 13:41:41 +00:00
parent e3d61e6db0
commit ed463c1ba2
No known key found for this signature in database
GPG key ID: 5DD5567C12C5F312

View file

@ -18,7 +18,6 @@ package state
import ( import (
"errors" "errors"
"sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -230,10 +229,8 @@ type subfetcher struct {
addr common.Address // Address of the account that the trie belongs to addr common.Address // Address of the account that the trie belongs to
trie Trie // Trie being populated with nodes trie Trie // Trie being populated with nodes
tasks []*subfetcherTask // Items queued up for retrieval tasks chan []*subfetcherTask // Queue items for retrieval
lock sync.Mutex // Lock protecting the task queue
wake chan struct{} // Wake channel if a new task is scheduled
stop chan struct{} // Channel to interrupt processing stop chan struct{} // Channel to interrupt processing
term chan struct{} // Channel to signal interruption term chan struct{} // Channel to signal interruption
@ -267,7 +264,7 @@ func newSubfetcher(db Database, state common.Hash, owner common.Hash, root commo
owner: owner, owner: owner,
root: root, root: root,
addr: addr, addr: addr,
wake: make(chan struct{}, 1), tasks: make(chan []*subfetcherTask),
stop: make(chan struct{}), stop: make(chan struct{}),
term: make(chan struct{}), term: make(chan struct{}),
seenReadAddr: make(map[common.Address]struct{}), seenReadAddr: make(map[common.Address]struct{}),
@ -281,30 +278,20 @@ func newSubfetcher(db Database, state common.Hash, owner common.Hash, root commo
// schedule adds a batch of trie keys to the queue to prefetch. // schedule adds a batch of trie keys to the queue to prefetch.
func (sf *subfetcher) schedule(addrs []common.Address, slots []common.Hash, read bool) error { func (sf *subfetcher) schedule(addrs []common.Address, slots []common.Hash, read bool) error {
// Ensure the subfetcher is still alive tasks := make([]*subfetcherTask, 0, len(addrs)+len(slots))
select {
case <-sf.term:
return errTerminated
default:
}
// Append the tasks to the current queue
sf.lock.Lock()
for _, addr := range addrs { for _, addr := range addrs {
sf.tasks = append(sf.tasks, &subfetcherTask{read: read, addr: &addr}) tasks = append(tasks, &subfetcherTask{read: read, addr: &addr})
} }
for _, slot := range slots { for _, slot := range slots {
sf.tasks = append(sf.tasks, &subfetcherTask{read: read, slot: &slot}) tasks = append(tasks, &subfetcherTask{read: read, slot: &slot})
} }
sf.lock.Unlock()
// Notify the background thread to execute scheduled tasks
select { select {
case sf.wake <- struct{}{}: case sf.tasks <- tasks:
// Wake signal sent return nil
default: case <-sf.term:
// Wake signal not sent as a previous one is already queued return errTerminated
} }
return nil
} }
// wait blocks until the subfetcher terminates. This method is used to block on // wait blocks until the subfetcher terminates. This method is used to block on
@ -379,15 +366,29 @@ func (sf *subfetcher) loop() {
if err := sf.openTrie(); err != nil { if err := sf.openTrie(); err != nil {
return return
} }
var tasks []*subfetcherTask
// Adding a default case to the select statement would spin the for loop so
// we instead have a `work` channel signaller. A necessary invariant is that
// there is a buffered item i.f.f. there are tasks; therefore only
// addTasks() may append to the above slice and only the <-work branch may
// deplete it.
work := make(chan struct{}, 1)
defer close(work)
addTasks := func(ts []*subfetcherTask) {
tasks = append(tasks, ts...)
select {
case work <- struct{}{}:
default:
}
}
for { for {
select { select {
case <-sf.wake: case ts := <-sf.tasks:
// Execute all remaining tasks in a single run addTasks(ts)
sf.lock.Lock()
tasks := sf.tasks
sf.tasks = nil
sf.lock.Unlock()
case <-work:
for _, task := range tasks { for _, task := range tasks {
if task.addr != nil { if task.addr != nil {
key := *task.addr key := *task.addr
@ -451,20 +452,21 @@ func (sf *subfetcher) loop() {
} }
} }
} }
tasks = tasks[:0] // avoid reallocation
case <-sf.stop: case <-sf.stop:
// Termination is requested, abort if no more tasks are pending. If // Termination is requested, abort if no more tasks are pending. If
// there are some, exhaust them first. // there are some, exhaust them first.
sf.lock.Lock() if len(tasks) > 0 {
done := sf.tasks == nil // See earlier invariant that guarantees a receive on `work` to clear `tasks`.
sf.lock.Unlock() continue
}
if done { select {
case ts := <-sf.tasks:
addTasks(ts)
default:
return return
} }
// Some tasks are pending, loop and pick them up (that wake branch
// will be selected eventually, whilst stop remains closed to this
// branch will also run afterwards).
} }
} }
} }