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 }