github.com/susy-go/susy-graviton@v0.0.0-20190614130430-36cddae42305/swarm/network/fetcher.go (about)

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