|
|
@@ -17,233 +17,318 @@
|
|
|
package state
|
|
|
|
|
|
import (
|
|
|
+ "sync"
|
|
|
+
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
|
"github.com/ethereum/go-ethereum/log"
|
|
|
"github.com/ethereum/go-ethereum/metrics"
|
|
|
)
|
|
|
|
|
|
var (
|
|
|
- // trieDeliveryMeter counts how many times the prefetcher was unable to supply
|
|
|
- // the statedb with a prefilled trie. This meter should be zero -- if it's not, that
|
|
|
- // needs to be investigated
|
|
|
- trieDeliveryMissMeter = metrics.NewRegisteredMeter("trie/prefetch/deliverymiss", nil)
|
|
|
-
|
|
|
- triePrefetchFetchMeter = metrics.NewRegisteredMeter("trie/prefetch/fetch", nil)
|
|
|
- triePrefetchSkipMeter = metrics.NewRegisteredMeter("trie/prefetch/skip", nil)
|
|
|
- triePrefetchDropMeter = metrics.NewRegisteredMeter("trie/prefetch/drop", nil)
|
|
|
+ // triePrefetchMetricsPrefix is the prefix under which to publis the metrics.
|
|
|
+ triePrefetchMetricsPrefix = "trie/prefetch/"
|
|
|
)
|
|
|
|
|
|
-// TriePrefetcher is an active prefetcher, which receives accounts or storage
|
|
|
-// items on two channels, and does trie-loading of the items.
|
|
|
-// The goal is to get as much useful content into the caches as possible
|
|
|
-type TriePrefetcher struct {
|
|
|
- requestCh chan (fetchRequest) // Chan to receive requests for data to fetch
|
|
|
- cmdCh chan (*cmd) // Chan to control activity, pause/new root
|
|
|
- quitCh chan (struct{})
|
|
|
- deliveryCh chan (struct{})
|
|
|
- db Database
|
|
|
-
|
|
|
- paused bool
|
|
|
-
|
|
|
- storageTries map[common.Hash]Trie
|
|
|
- accountTrie Trie
|
|
|
- accountTrieRoot common.Hash
|
|
|
+// triePrefetcher is an active prefetcher, which receives accounts or storage
|
|
|
+// items and does trie-loading of them. The goal is to get as much useful content
|
|
|
+// into the caches as possible.
|
|
|
+//
|
|
|
+// Note, the prefetcher's API is not thread safe.
|
|
|
+type triePrefetcher struct {
|
|
|
+ db Database // Database to fetch trie nodes through
|
|
|
+ root common.Hash // Root hash of theaccount trie for metrics
|
|
|
+ fetches map[common.Hash]Trie // Partially or fully fetcher tries
|
|
|
+ fetchers map[common.Hash]*subfetcher // Subfetchers for each trie
|
|
|
+
|
|
|
+ deliveryMissMeter metrics.Meter
|
|
|
+ accountLoadMeter metrics.Meter
|
|
|
+ accountDupMeter metrics.Meter
|
|
|
+ accountSkipMeter metrics.Meter
|
|
|
+ accountWasteMeter metrics.Meter
|
|
|
+ storageLoadMeter metrics.Meter
|
|
|
+ storageDupMeter metrics.Meter
|
|
|
+ storageSkipMeter metrics.Meter
|
|
|
+ storageWasteMeter metrics.Meter
|
|
|
}
|
|
|
|
|
|
-func NewTriePrefetcher(db Database) *TriePrefetcher {
|
|
|
- return &TriePrefetcher{
|
|
|
- requestCh: make(chan fetchRequest, 200),
|
|
|
- cmdCh: make(chan *cmd),
|
|
|
- quitCh: make(chan struct{}),
|
|
|
- deliveryCh: make(chan struct{}),
|
|
|
- db: db,
|
|
|
+// newTriePrefetcher
|
|
|
+func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePrefetcher {
|
|
|
+ prefix := triePrefetchMetricsPrefix + namespace
|
|
|
+ p := &triePrefetcher{
|
|
|
+ db: db,
|
|
|
+ root: root,
|
|
|
+ fetchers: make(map[common.Hash]*subfetcher), // Active prefetchers use the fetchers map
|
|
|
+
|
|
|
+ deliveryMissMeter: metrics.GetOrRegisterMeter(prefix+"/deliverymiss", nil),
|
|
|
+ accountLoadMeter: metrics.GetOrRegisterMeter(prefix+"/account/load", nil),
|
|
|
+ accountDupMeter: metrics.GetOrRegisterMeter(prefix+"/account/dup", nil),
|
|
|
+ accountSkipMeter: metrics.GetOrRegisterMeter(prefix+"/account/skip", nil),
|
|
|
+ accountWasteMeter: metrics.GetOrRegisterMeter(prefix+"/account/waste", nil),
|
|
|
+ storageLoadMeter: metrics.GetOrRegisterMeter(prefix+"/storage/load", nil),
|
|
|
+ storageDupMeter: metrics.GetOrRegisterMeter(prefix+"/storage/dup", nil),
|
|
|
+ storageSkipMeter: metrics.GetOrRegisterMeter(prefix+"/storage/skip", nil),
|
|
|
+ storageWasteMeter: metrics.GetOrRegisterMeter(prefix+"/storage/waste", nil),
|
|
|
}
|
|
|
+ return p
|
|
|
}
|
|
|
|
|
|
-type cmd struct {
|
|
|
- root common.Hash
|
|
|
-}
|
|
|
+// close iterates over all the subfetchers, aborts any that were left spinning
|
|
|
+// and reports the stats to the metrics subsystem.
|
|
|
+func (p *triePrefetcher) close() {
|
|
|
+ for _, fetcher := range p.fetchers {
|
|
|
+ fetcher.abort() // safe to do multiple times
|
|
|
|
|
|
-type fetchRequest struct {
|
|
|
- slots []common.Hash
|
|
|
- storageRoot *common.Hash
|
|
|
- addresses []common.Address
|
|
|
-}
|
|
|
+ if metrics.Enabled {
|
|
|
+ if fetcher.root == p.root {
|
|
|
+ p.accountLoadMeter.Mark(int64(len(fetcher.seen)))
|
|
|
+ p.accountDupMeter.Mark(int64(fetcher.dups))
|
|
|
+ p.accountSkipMeter.Mark(int64(len(fetcher.tasks)))
|
|
|
|
|
|
-func (p *TriePrefetcher) Loop() {
|
|
|
- var (
|
|
|
- accountTrieRoot common.Hash
|
|
|
- accountTrie Trie
|
|
|
- storageTries map[common.Hash]Trie
|
|
|
-
|
|
|
- err error
|
|
|
- // Some tracking of performance
|
|
|
- skipped int64
|
|
|
- fetched int64
|
|
|
-
|
|
|
- paused = true
|
|
|
- )
|
|
|
- // The prefetcher loop has two distinct phases:
|
|
|
- // 1: Paused: when in this state, the accumulated tries are accessible to outside
|
|
|
- // callers.
|
|
|
- // 2: Active prefetching, awaiting slots and accounts to prefetch
|
|
|
- for {
|
|
|
- select {
|
|
|
- case <-p.quitCh:
|
|
|
- return
|
|
|
- case cmd := <-p.cmdCh:
|
|
|
- // Clear out any old requests
|
|
|
- drain:
|
|
|
- for {
|
|
|
- select {
|
|
|
- case req := <-p.requestCh:
|
|
|
- if req.slots != nil {
|
|
|
- skipped += int64(len(req.slots))
|
|
|
- } else {
|
|
|
- skipped += int64(len(req.addresses))
|
|
|
- }
|
|
|
- default:
|
|
|
- break drain
|
|
|
+ for _, key := range fetcher.used {
|
|
|
+ delete(fetcher.seen, string(key))
|
|
|
}
|
|
|
- }
|
|
|
- if paused {
|
|
|
- // Clear old data
|
|
|
- p.storageTries = nil
|
|
|
- p.accountTrie = nil
|
|
|
- p.accountTrieRoot = common.Hash{}
|
|
|
- // Resume again
|
|
|
- storageTries = make(map[common.Hash]Trie)
|
|
|
- accountTrieRoot = cmd.root
|
|
|
- accountTrie, err = p.db.OpenTrie(accountTrieRoot)
|
|
|
- if err != nil {
|
|
|
- log.Error("Trie prefetcher failed opening trie", "root", accountTrieRoot, "err", err)
|
|
|
- }
|
|
|
- if accountTrieRoot == (common.Hash{}) {
|
|
|
- log.Error("Trie prefetcher unpaused with bad root")
|
|
|
- }
|
|
|
- paused = false
|
|
|
+ p.accountWasteMeter.Mark(int64(len(fetcher.seen)))
|
|
|
} else {
|
|
|
- // Update metrics at new block events
|
|
|
- triePrefetchFetchMeter.Mark(fetched)
|
|
|
- triePrefetchSkipMeter.Mark(skipped)
|
|
|
- fetched, skipped = 0, 0
|
|
|
- // Make the tries accessible
|
|
|
- p.accountTrie = accountTrie
|
|
|
- p.storageTries = storageTries
|
|
|
- p.accountTrieRoot = accountTrieRoot
|
|
|
- if cmd.root != (common.Hash{}) {
|
|
|
- log.Error("Trie prefetcher paused with non-empty root")
|
|
|
- }
|
|
|
- paused = true
|
|
|
- }
|
|
|
- p.deliveryCh <- struct{}{}
|
|
|
- case req := <-p.requestCh:
|
|
|
- if paused {
|
|
|
- continue
|
|
|
- }
|
|
|
- if sRoot := req.storageRoot; sRoot != nil {
|
|
|
- // Storage slots to fetch
|
|
|
- var (
|
|
|
- storageTrie Trie
|
|
|
- err error
|
|
|
- )
|
|
|
- if storageTrie = storageTries[*sRoot]; storageTrie == nil {
|
|
|
- if storageTrie, err = p.db.OpenTrie(*sRoot); err != nil {
|
|
|
- log.Warn("trie prefetcher failed opening storage trie", "root", *sRoot, "err", err)
|
|
|
- skipped += int64(len(req.slots))
|
|
|
- continue
|
|
|
- }
|
|
|
- storageTries[*sRoot] = storageTrie
|
|
|
- }
|
|
|
- for _, key := range req.slots {
|
|
|
- storageTrie.TryGet(key[:])
|
|
|
- }
|
|
|
- fetched += int64(len(req.slots))
|
|
|
- } else { // an account
|
|
|
- for _, addr := range req.addresses {
|
|
|
- accountTrie.TryGet(addr[:])
|
|
|
+ p.storageLoadMeter.Mark(int64(len(fetcher.seen)))
|
|
|
+ p.storageDupMeter.Mark(int64(fetcher.dups))
|
|
|
+ p.storageSkipMeter.Mark(int64(len(fetcher.tasks)))
|
|
|
+
|
|
|
+ for _, key := range fetcher.used {
|
|
|
+ delete(fetcher.seen, string(key))
|
|
|
}
|
|
|
- fetched += int64(len(req.addresses))
|
|
|
+ p.storageWasteMeter.Mark(int64(len(fetcher.seen)))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ // Clear out all fetchers (will crash on a second call, deliberate)
|
|
|
+ p.fetchers = nil
|
|
|
}
|
|
|
|
|
|
-// Close stops the prefetcher
|
|
|
-func (p *TriePrefetcher) Close() {
|
|
|
- if p.quitCh != nil {
|
|
|
- close(p.quitCh)
|
|
|
- p.quitCh = nil
|
|
|
+// copy creates a deep-but-inactive copy of the trie prefetcher. Any trie data
|
|
|
+// already loaded will be copied over, but no goroutines will be started. This
|
|
|
+// is mostly used in the miner which creates a copy of it's actively mutated
|
|
|
+// state to be sealed while it may further mutate the state.
|
|
|
+func (p *triePrefetcher) copy() *triePrefetcher {
|
|
|
+ copy := &triePrefetcher{
|
|
|
+ db: p.db,
|
|
|
+ root: p.root,
|
|
|
+ fetches: make(map[common.Hash]Trie), // Active prefetchers use the fetches map
|
|
|
+
|
|
|
+ deliveryMissMeter: p.deliveryMissMeter,
|
|
|
+ accountLoadMeter: p.accountLoadMeter,
|
|
|
+ accountDupMeter: p.accountDupMeter,
|
|
|
+ accountSkipMeter: p.accountSkipMeter,
|
|
|
+ accountWasteMeter: p.accountWasteMeter,
|
|
|
+ storageLoadMeter: p.storageLoadMeter,
|
|
|
+ storageDupMeter: p.storageDupMeter,
|
|
|
+ storageSkipMeter: p.storageSkipMeter,
|
|
|
+ storageWasteMeter: p.storageWasteMeter,
|
|
|
+ }
|
|
|
+ // If the prefetcher is already a copy, duplicate the data
|
|
|
+ if p.fetches != nil {
|
|
|
+ for root, fetch := range p.fetches {
|
|
|
+ copy.fetches[root] = p.db.CopyTrie(fetch)
|
|
|
+ }
|
|
|
+ return copy
|
|
|
+ }
|
|
|
+ // Otherwise we're copying an active fetcher, retrieve the current states
|
|
|
+ for root, fetcher := range p.fetchers {
|
|
|
+ copy.fetches[root] = fetcher.peek()
|
|
|
}
|
|
|
+ return copy
|
|
|
}
|
|
|
|
|
|
-// Resume causes the prefetcher to clear out old data, and get ready to
|
|
|
-// fetch data concerning the new root
|
|
|
-func (p *TriePrefetcher) Resume(root common.Hash) {
|
|
|
- p.paused = false
|
|
|
- p.cmdCh <- &cmd{
|
|
|
- root: root,
|
|
|
+// prefetch schedules a batch of trie items to prefetch.
|
|
|
+func (p *triePrefetcher) prefetch(root common.Hash, keys [][]byte) {
|
|
|
+ // If the prefetcher is an inactive one, bail out
|
|
|
+ if p.fetches != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // Active fetcher, schedule the retrievals
|
|
|
+ fetcher := p.fetchers[root]
|
|
|
+ if fetcher == nil {
|
|
|
+ fetcher = newSubfetcher(p.db, root)
|
|
|
+ p.fetchers[root] = fetcher
|
|
|
}
|
|
|
- // Wait for it
|
|
|
- <-p.deliveryCh
|
|
|
+ fetcher.schedule(keys)
|
|
|
}
|
|
|
|
|
|
-// Pause causes the prefetcher to pause prefetching, and make tries
|
|
|
-// accessible to callers via GetTrie
|
|
|
-func (p *TriePrefetcher) Pause() {
|
|
|
- if p.paused {
|
|
|
- return
|
|
|
+// trie returns the trie matching the root hash, or nil if the prefetcher doesn't
|
|
|
+// have it.
|
|
|
+func (p *triePrefetcher) trie(root common.Hash) Trie {
|
|
|
+ // If the prefetcher is inactive, return from existing deep copies
|
|
|
+ if p.fetches != nil {
|
|
|
+ trie := p.fetches[root]
|
|
|
+ if trie == nil {
|
|
|
+ p.deliveryMissMeter.Mark(1)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ return p.db.CopyTrie(trie)
|
|
|
}
|
|
|
- p.paused = true
|
|
|
- p.cmdCh <- &cmd{
|
|
|
- root: common.Hash{},
|
|
|
+ // Otherwise the prefetcher is active, bail if no trie was prefetched for this root
|
|
|
+ fetcher := p.fetchers[root]
|
|
|
+ if fetcher == nil {
|
|
|
+ p.deliveryMissMeter.Mark(1)
|
|
|
+ return nil
|
|
|
}
|
|
|
- // Wait for it
|
|
|
- <-p.deliveryCh
|
|
|
+ // Interrupt the prefetcher if it's by any chance still running and return
|
|
|
+ // a copy of any pre-loaded trie.
|
|
|
+ fetcher.abort() // safe to do multiple times
|
|
|
+
|
|
|
+ trie := fetcher.peek()
|
|
|
+ if trie == nil {
|
|
|
+ p.deliveryMissMeter.Mark(1)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ return trie
|
|
|
}
|
|
|
|
|
|
-// PrefetchAddresses adds an address for prefetching
|
|
|
-func (p *TriePrefetcher) PrefetchAddresses(addresses []common.Address) {
|
|
|
- cmd := fetchRequest{
|
|
|
- addresses: addresses,
|
|
|
+// used marks a batch of state items used to allow creating statistics as to
|
|
|
+// how useful or wasteful the prefetcher is.
|
|
|
+func (p *triePrefetcher) used(root common.Hash, used [][]byte) {
|
|
|
+ if fetcher := p.fetchers[root]; fetcher != nil {
|
|
|
+ fetcher.used = used
|
|
|
}
|
|
|
- // We do an async send here, to not cause the caller to block
|
|
|
- //p.requestCh <- cmd
|
|
|
+}
|
|
|
+
|
|
|
+// subfetcher is a trie fetcher goroutine responsible for pulling entries for a
|
|
|
+// single trie. It is spawned when a new root is encountered and lives until the
|
|
|
+// main prefetcher is paused and either all requested items are processed or if
|
|
|
+// the trie being worked on is retrieved from the prefetcher.
|
|
|
+type subfetcher struct {
|
|
|
+ db Database // Database to load trie nodes through
|
|
|
+ root common.Hash // Root hash of the trie to prefetch
|
|
|
+ trie Trie // Trie being populated with nodes
|
|
|
+
|
|
|
+ tasks [][]byte // Items queued up 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
|
|
|
+ term chan struct{} // Channel to signal iterruption
|
|
|
+ copy chan chan Trie // Channel to request a copy of the current trie
|
|
|
+
|
|
|
+ seen map[string]struct{} // Tracks the entries already loaded
|
|
|
+ dups int // Number of duplicate preload tasks
|
|
|
+ used [][]byte // Tracks the entries used in the end
|
|
|
+}
|
|
|
+
|
|
|
+// newSubfetcher creates a goroutine to prefetch state items belonging to a
|
|
|
+// particular root hash.
|
|
|
+func newSubfetcher(db Database, root common.Hash) *subfetcher {
|
|
|
+ sf := &subfetcher{
|
|
|
+ db: db,
|
|
|
+ root: root,
|
|
|
+ wake: make(chan struct{}, 1),
|
|
|
+ stop: make(chan struct{}),
|
|
|
+ term: make(chan struct{}),
|
|
|
+ copy: make(chan chan Trie),
|
|
|
+ seen: make(map[string]struct{}),
|
|
|
+ }
|
|
|
+ go sf.loop()
|
|
|
+ return sf
|
|
|
+}
|
|
|
+
|
|
|
+// schedule adds a batch of trie keys to the queue to prefetch.
|
|
|
+func (sf *subfetcher) schedule(keys [][]byte) {
|
|
|
+ // Append the tasks to the current queue
|
|
|
+ sf.lock.Lock()
|
|
|
+ sf.tasks = append(sf.tasks, keys...)
|
|
|
+ sf.lock.Unlock()
|
|
|
+
|
|
|
+ // Notify the prefetcher, it's fine if it's already terminated
|
|
|
select {
|
|
|
- case p.requestCh <- cmd:
|
|
|
+ case sf.wake <- struct{}{}:
|
|
|
default:
|
|
|
- triePrefetchDropMeter.Mark(int64(len(addresses)))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// PrefetchStorage adds a storage root and a set of keys for prefetching
|
|
|
-func (p *TriePrefetcher) PrefetchStorage(root common.Hash, slots []common.Hash) {
|
|
|
- cmd := fetchRequest{
|
|
|
- storageRoot: &root,
|
|
|
- slots: slots,
|
|
|
+// peek tries to retrieve a deep copy of the fetcher's trie in whatever form it
|
|
|
+// is currently.
|
|
|
+func (sf *subfetcher) peek() Trie {
|
|
|
+ ch := make(chan Trie)
|
|
|
+ select {
|
|
|
+ case sf.copy <- ch:
|
|
|
+ // Subfetcher still alive, return copy from it
|
|
|
+ return <-ch
|
|
|
+
|
|
|
+ case <-sf.term:
|
|
|
+ // Subfetcher already terminated, return a copy directly
|
|
|
+ if sf.trie == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ return sf.db.CopyTrie(sf.trie)
|
|
|
}
|
|
|
- // We do an async send here, to not cause the caller to block
|
|
|
- //p.requestCh <- cmd
|
|
|
+}
|
|
|
+
|
|
|
+// abort interrupts the subfetcher immediately. It is safe to call abort multiple
|
|
|
+// times but it is not thread safe.
|
|
|
+func (sf *subfetcher) abort() {
|
|
|
select {
|
|
|
- case p.requestCh <- cmd:
|
|
|
+ case <-sf.stop:
|
|
|
default:
|
|
|
- triePrefetchDropMeter.Mark(int64(len(slots)))
|
|
|
+ close(sf.stop)
|
|
|
}
|
|
|
+ <-sf.term
|
|
|
}
|
|
|
|
|
|
-// GetTrie returns the trie matching the root hash, or nil if the prefetcher
|
|
|
-// doesn't have it.
|
|
|
-func (p *TriePrefetcher) GetTrie(root common.Hash) Trie {
|
|
|
- if root == p.accountTrieRoot {
|
|
|
- return p.accountTrie
|
|
|
- }
|
|
|
- if storageTrie, ok := p.storageTries[root]; ok {
|
|
|
- // Two accounts may well have the same storage root, but we cannot allow
|
|
|
- // them both to make updates to the same trie instance. Therefore,
|
|
|
- // we need to either delete the trie now, or deliver a copy of the trie.
|
|
|
- delete(p.storageTries, root)
|
|
|
- return storageTrie
|
|
|
- }
|
|
|
- trieDeliveryMissMeter.Mark(1)
|
|
|
- return nil
|
|
|
+// loop waits for new tasks to be scheduled and keeps loading them until it runs
|
|
|
+// out of tasks or its underlying trie is retrieved for committing.
|
|
|
+func (sf *subfetcher) loop() {
|
|
|
+ // No matter how the loop stops, signal anyone waiting that it's terminated
|
|
|
+ defer close(sf.term)
|
|
|
+
|
|
|
+ // Start by opening the trie and stop processing if it fails
|
|
|
+ trie, err := sf.db.OpenTrie(sf.root)
|
|
|
+ if err != nil {
|
|
|
+ log.Warn("Trie prefetcher failed opening trie", "root", sf.root, "err", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ sf.trie = trie
|
|
|
+
|
|
|
+ // Trie opened successfully, keep prefetching items
|
|
|
+ for {
|
|
|
+ select {
|
|
|
+ case <-sf.wake:
|
|
|
+ // Subfetcher was woken up, retrieve any tasks to avoid spinning the lock
|
|
|
+ sf.lock.Lock()
|
|
|
+ tasks := sf.tasks
|
|
|
+ sf.tasks = nil
|
|
|
+ sf.lock.Unlock()
|
|
|
+
|
|
|
+ // Prefetch any tasks until the loop is interrupted
|
|
|
+ for i, task := range tasks {
|
|
|
+ select {
|
|
|
+ case <-sf.stop:
|
|
|
+ // If termination is requested, add any leftover back and return
|
|
|
+ sf.lock.Lock()
|
|
|
+ sf.tasks = append(sf.tasks, tasks[i:]...)
|
|
|
+ sf.lock.Unlock()
|
|
|
+ return
|
|
|
+
|
|
|
+ case ch := <-sf.copy:
|
|
|
+ // Somebody wants a copy of the current trie, grant them
|
|
|
+ ch <- sf.db.CopyTrie(sf.trie)
|
|
|
+
|
|
|
+ default:
|
|
|
+ // No termination request yet, prefetch the next entry
|
|
|
+ taskid := string(task)
|
|
|
+ if _, ok := sf.seen[taskid]; ok {
|
|
|
+ sf.dups++
|
|
|
+ } else {
|
|
|
+ sf.trie.TryGet(task)
|
|
|
+ sf.seen[taskid] = struct{}{}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ case ch := <-sf.copy:
|
|
|
+ // Somebody wants a copy of the current trie, grant them
|
|
|
+ ch <- sf.db.CopyTrie(sf.trie)
|
|
|
+
|
|
|
+ case <-sf.stop:
|
|
|
+ // Termination is requested, abort and leave remaining tasks
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|