github.com/letterj/go-ethereum@v1.8.22-0.20190204142846-520024dfd689/swarm/network/fetcher.go (about)

     1  // Copyright 2018 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package network
    18  
    19  import (
    20  	"context"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/ethereum/go-ethereum/log"
    25  	"github.com/ethereum/go-ethereum/p2p/enode"
    26  	"github.com/ethereum/go-ethereum/swarm/storage"
    27  )
    28  
    29  const (
    30  	defaultSearchTimeout = 1 * time.Second
    31  	// maximum number of forwarded requests (hops), to make sure requests are not
    32  	// forwarded forever in peer loops
    33  	maxHopCount uint8 = 20
    34  )
    35  
    36  // Time to consider peer to be skipped.
    37  // Also used in stream delivery.
    38  var RequestTimeout = 10 * time.Second
    39  
    40  type RequestFunc func(context.Context, *Request) (*enode.ID, chan struct{}, error)
    41  
    42  // Fetcher is created when a chunk is not found locally. It starts a request handler loop once and
    43  // keeps it alive until all active requests are completed. This can happen:
    44  //     1. either because the chunk is delivered
    45  //     2. or because the requester cancelled/timed out
    46  // Fetcher self destroys itself after it is completed.
    47  // TODO: cancel all forward requests after termination
    48  type Fetcher struct {
    49  	protoRequestFunc RequestFunc     // request function fetcher calls to issue retrieve request for a chunk
    50  	addr             storage.Address // the address of the chunk to be fetched
    51  	offerC           chan *enode.ID  // channel of sources (peer node id strings)
    52  	requestC         chan uint8      // channel for incoming requests (with the hopCount value in it)
    53  	searchTimeout    time.Duration
    54  	skipCheck        bool
    55  }
    56  
    57  type Request struct {
    58  	Addr        storage.Address // chunk address
    59  	Source      *enode.ID       // nodeID of peer to request from (can be nil)
    60  	SkipCheck   bool            // whether to offer the chunk first or deliver directly
    61  	peersToSkip *sync.Map       // peers not to request chunk from (only makes sense if source is nil)
    62  	HopCount    uint8           // number of forwarded requests (hops)
    63  }
    64  
    65  // NewRequest returns a new instance of Request based on chunk address skip check and
    66  // a map of peers to skip.
    67  func NewRequest(addr storage.Address, skipCheck bool, peersToSkip *sync.Map) *Request {
    68  	return &Request{
    69  		Addr:        addr,
    70  		SkipCheck:   skipCheck,
    71  		peersToSkip: peersToSkip,
    72  	}
    73  }
    74  
    75  // SkipPeer returns if the peer with nodeID should not be requested to deliver a chunk.
    76  // Peers to skip are kept per Request and for a time period of RequestTimeout.
    77  // This function is used in stream package in Delivery.RequestFromPeers to optimize
    78  // requests for chunks.
    79  func (r *Request) SkipPeer(nodeID string) bool {
    80  	val, ok := r.peersToSkip.Load(nodeID)
    81  	if !ok {
    82  		return false
    83  	}
    84  	t, ok := val.(time.Time)
    85  	if ok && time.Now().After(t.Add(RequestTimeout)) {
    86  		// deadline expired
    87  		r.peersToSkip.Delete(nodeID)
    88  		return false
    89  	}
    90  	return true
    91  }
    92  
    93  // FetcherFactory is initialised with a request function and can create fetchers
    94  type FetcherFactory struct {
    95  	request   RequestFunc
    96  	skipCheck bool
    97  }
    98  
    99  // NewFetcherFactory takes a request function and skip check parameter and creates a FetcherFactory
   100  func NewFetcherFactory(request RequestFunc, skipCheck bool) *FetcherFactory {
   101  	return &FetcherFactory{
   102  		request:   request,
   103  		skipCheck: skipCheck,
   104  	}
   105  }
   106  
   107  // New constructs a new Fetcher, for the given chunk. All peers in peersToSkip
   108  // are not requested to deliver the given chunk. peersToSkip should always
   109  // contain the peers which are actively requesting this chunk, to make sure we
   110  // don't request back the chunks from them.
   111  // The created Fetcher is started and returned.
   112  func (f *FetcherFactory) New(ctx context.Context, source storage.Address, peersToSkip *sync.Map) storage.NetFetcher {
   113  	fetcher := NewFetcher(source, f.request, f.skipCheck)
   114  	go fetcher.run(ctx, peersToSkip)
   115  	return fetcher
   116  }
   117  
   118  // NewFetcher creates a new Fetcher for the given chunk address using the given request function.
   119  func NewFetcher(addr storage.Address, rf RequestFunc, skipCheck bool) *Fetcher {
   120  	return &Fetcher{
   121  		addr:             addr,
   122  		protoRequestFunc: rf,
   123  		offerC:           make(chan *enode.ID),
   124  		requestC:         make(chan uint8),
   125  		searchTimeout:    defaultSearchTimeout,
   126  		skipCheck:        skipCheck,
   127  	}
   128  }
   129  
   130  // Offer is called when an upstream peer offers the chunk via syncing as part of `OfferedHashesMsg` and the node does not have the chunk locally.
   131  func (f *Fetcher) Offer(ctx context.Context, source *enode.ID) {
   132  	// First we need to have this select to make sure that we return if context is done
   133  	select {
   134  	case <-ctx.Done():
   135  		return
   136  	default:
   137  	}
   138  
   139  	// This select alone would not guarantee that we return of context is done, it could potentially
   140  	// push to offerC instead if offerC is available (see number 2 in https://golang.org/ref/spec#Select_statements)
   141  	select {
   142  	case f.offerC <- source:
   143  	case <-ctx.Done():
   144  	}
   145  }
   146  
   147  // Request is called when an upstream peer request the chunk as part of `RetrieveRequestMsg`, or from a local request through FileStore, and the node does not have the chunk locally.
   148  func (f *Fetcher) Request(ctx context.Context, hopCount uint8) {
   149  	// First we need to have this select to make sure that we return if context is done
   150  	select {
   151  	case <-ctx.Done():
   152  		return
   153  	default:
   154  	}
   155  
   156  	if hopCount >= maxHopCount {
   157  		log.Debug("fetcher request hop count limit reached", "hops", hopCount)
   158  		return
   159  	}
   160  
   161  	// This select alone would not guarantee that we return of context is done, it could potentially
   162  	// push to offerC instead if offerC is available (see number 2 in https://golang.org/ref/spec#Select_statements)
   163  	select {
   164  	case f.requestC <- hopCount + 1:
   165  	case <-ctx.Done():
   166  	}
   167  }
   168  
   169  // start prepares the Fetcher
   170  // it keeps the Fetcher alive within the lifecycle of the passed context
   171  func (f *Fetcher) run(ctx context.Context, peers *sync.Map) {
   172  	var (
   173  		doRequest bool             // determines if retrieval is initiated in the current iteration
   174  		wait      *time.Timer      // timer for search timeout
   175  		waitC     <-chan time.Time // timer channel
   176  		sources   []*enode.ID      // known sources, ie. peers that offered the chunk
   177  		requested bool             // true if the chunk was actually requested
   178  		hopCount  uint8
   179  	)
   180  	gone := make(chan *enode.ID) // channel to signal that a peer we requested from disconnected
   181  
   182  	// loop that keeps the fetching process alive
   183  	// after every request a timer is set. If this goes off we request again from another peer
   184  	// note that the previous request is still alive and has the chance to deliver, so
   185  	// requesting again extends the search. ie.,
   186  	// if a peer we requested from is gone we issue a new request, so the number of active
   187  	// requests never decreases
   188  	for {
   189  		select {
   190  
   191  		// incoming offer
   192  		case source := <-f.offerC:
   193  			log.Trace("new source", "peer addr", source, "request addr", f.addr)
   194  			// 1) the chunk is offered by a syncing peer
   195  			// add to known sources
   196  			sources = append(sources, source)
   197  			// launch a request to the source iff the chunk was requested (not just expected because its offered by a syncing peer)
   198  			doRequest = requested
   199  
   200  		// incoming request
   201  		case hopCount = <-f.requestC:
   202  			log.Trace("new request", "request addr", f.addr)
   203  			// 2) chunk is requested, set requested flag
   204  			// launch a request iff none been launched yet
   205  			doRequest = !requested
   206  			requested = true
   207  
   208  			// peer we requested from is gone. fall back to another
   209  			// and remove the peer from the peers map
   210  		case id := <-gone:
   211  			log.Trace("peer gone", "peer id", id.String(), "request addr", f.addr)
   212  			peers.Delete(id.String())
   213  			doRequest = requested
   214  
   215  		// search timeout: too much time passed since the last request,
   216  		// extend the search to a new peer if we can find one
   217  		case <-waitC:
   218  			log.Trace("search timed out: requesting", "request addr", f.addr)
   219  			doRequest = requested
   220  
   221  			// all Fetcher context closed, can quit
   222  		case <-ctx.Done():
   223  			log.Trace("terminate fetcher", "request addr", f.addr)
   224  			// TODO: send cancellations to all peers left over in peers map (i.e., those we requested from)
   225  			return
   226  		}
   227  
   228  		// need to issue a new request
   229  		if doRequest {
   230  			var err error
   231  			sources, err = f.doRequest(ctx, gone, peers, sources, hopCount)
   232  			if err != nil {
   233  				log.Info("unable to request", "request addr", f.addr, "err", err)
   234  			}
   235  		}
   236  
   237  		// if wait channel is not set, set it to a timer
   238  		if requested {
   239  			if wait == nil {
   240  				wait = time.NewTimer(f.searchTimeout)
   241  				defer wait.Stop()
   242  				waitC = wait.C
   243  			} else {
   244  				// stop the timer and drain the channel if it was not drained earlier
   245  				if !wait.Stop() {
   246  					select {
   247  					case <-wait.C:
   248  					default:
   249  					}
   250  				}
   251  				// reset the timer to go off after defaultSearchTimeout
   252  				wait.Reset(f.searchTimeout)
   253  			}
   254  		}
   255  		doRequest = false
   256  	}
   257  }
   258  
   259  // doRequest attempts at finding a peer to request the chunk from
   260  // * first it tries to request explicitly from peers that are known to have offered the chunk
   261  // * if there are no such peers (available) it tries to request it from a peer closest to the chunk address
   262  //   excluding those in the peersToSkip map
   263  // * if no such peer is found an error is returned
   264  //
   265  // if a request is successful,
   266  // * the peer's address is added to the set of peers to skip
   267  // * the peer's address is removed from prospective sources, and
   268  // * a go routine is started that reports on the gone channel if the peer is disconnected (or terminated their streamer)
   269  func (f *Fetcher) doRequest(ctx context.Context, gone chan *enode.ID, peersToSkip *sync.Map, sources []*enode.ID, hopCount uint8) ([]*enode.ID, error) {
   270  	var i int
   271  	var sourceID *enode.ID
   272  	var quit chan struct{}
   273  
   274  	req := &Request{
   275  		Addr:        f.addr,
   276  		SkipCheck:   f.skipCheck,
   277  		peersToSkip: peersToSkip,
   278  		HopCount:    hopCount,
   279  	}
   280  
   281  	foundSource := false
   282  	// iterate over known sources
   283  	for i = 0; i < len(sources); i++ {
   284  		req.Source = sources[i]
   285  		var err error
   286  		sourceID, quit, err = f.protoRequestFunc(ctx, req)
   287  		if err == nil {
   288  			// remove the peer from known sources
   289  			// Note: we can modify the source although we are looping on it, because we break from the loop immediately
   290  			sources = append(sources[:i], sources[i+1:]...)
   291  			foundSource = true
   292  			break
   293  		}
   294  	}
   295  
   296  	// if there are no known sources, or none available, we try request from a closest node
   297  	if !foundSource {
   298  		req.Source = nil
   299  		var err error
   300  		sourceID, quit, err = f.protoRequestFunc(ctx, req)
   301  		if err != nil {
   302  			// if no peers found to request from
   303  			return sources, err
   304  		}
   305  	}
   306  	// add peer to the set of peers to skip from now
   307  	peersToSkip.Store(sourceID.String(), time.Now())
   308  
   309  	// if the quit channel is closed, it indicates that the source peer we requested from
   310  	// disconnected or terminated its streamer
   311  	// here start a go routine that watches this channel and reports the source peer on the gone channel
   312  	// this go routine quits if the fetcher global context is done to prevent process leak
   313  	go func() {
   314  		select {
   315  		case <-quit:
   316  			gone <- sourceID
   317  		case <-ctx.Done():
   318  		}
   319  	}()
   320  	return sources, nil
   321  }