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