github.com/anacrolix/torrent@v1.61.0/webseed-peer.go (about) 1 package torrent 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "log/slog" 9 "math/rand" 10 "net" 11 "runtime/pprof" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/RoaringBitmap/roaring" 17 g "github.com/anacrolix/generics" 18 "github.com/anacrolix/missinggo/v2/panicif" 19 "golang.org/x/net/http2" 20 21 "github.com/anacrolix/torrent/metainfo" 22 pp "github.com/anacrolix/torrent/peer_protocol" 23 "github.com/anacrolix/torrent/webseed" 24 ) 25 26 type webseedPeer struct { 27 // First field for stats alignment. 28 peer Peer 29 logger *slog.Logger 30 client webseed.Client 31 activeRequests map[*webseedRequest]struct{} 32 locker sync.Locker 33 hostKey webseedHostKeyHandle 34 // We need this to look ourselves up in the Client.activeWebseedRequests map. 35 url webseedUrlKey 36 37 // When requests are allowed to resume. If Zero, then anytime. 38 penanceComplete time.Time 39 lastCrime error 40 } 41 42 func (me *webseedPeer) suspended() bool { 43 return me.lastCrime != nil && time.Now().Before(me.penanceComplete) 44 } 45 46 func (me *webseedPeer) convict(err error, term time.Duration) { 47 if me.suspended() { 48 return 49 } 50 me.lastCrime = err 51 me.penanceComplete = time.Now().Add(term) 52 } 53 54 func (*webseedPeer) allConnStatsImplField(stats *AllConnStats) *ConnStats { 55 return &stats.WebSeeds 56 } 57 58 func (me *webseedPeer) cancelAllRequests() { 59 // Is there any point to this? Won't we fail to receive a chunk and cancel anyway? Should we 60 // Close requests instead? 61 for req := range me.activeRequests { 62 req.Cancel("all requests cancelled", me.peer.t) 63 } 64 } 65 66 func (me *webseedPeer) peerImplWriteStatus(w io.Writer) {} 67 68 func (me *webseedPeer) isLowOnRequests() bool { 69 // Updates globally instead. 70 return false 71 } 72 73 // Webseed requests are issued globally so per-connection reasons or handling make no sense. 74 func (me *webseedPeer) onNeedUpdateRequests(reason updateRequestReason) { 75 // Too many reasons here: Can't predictably determine when we need to rerun updates. 76 // TODO: Can trigger this when we have Client-level active-requests map. 77 //me.peer.cl.scheduleImmediateWebseedRequestUpdate(reason) 78 } 79 80 func (me *webseedPeer) expectingChunks() bool { 81 return len(me.activeRequests) > 0 82 } 83 84 func (me *webseedPeer) checkReceivedChunk(RequestIndex, *pp.Message, Request) (bool, error) { 85 return true, nil 86 } 87 88 func (me *webseedPeer) lastWriteUploadRate() float64 { 89 // We never upload to webseeds. 90 return 0 91 } 92 93 var _ legacyPeerImpl = (*webseedPeer)(nil) 94 95 func (me *webseedPeer) peerImplStatusLines() []string { 96 lines := []string{ 97 me.client.Url, 98 } 99 if me.lastCrime != nil { 100 lines = append(lines, fmt.Sprintf("last crime: %v", me.lastCrime)) 101 } 102 if me.suspended() { 103 lines = append(lines, fmt.Sprintf("suspended for %v more", time.Until(me.penanceComplete))) 104 } 105 if len(me.activeRequests) > 0 { 106 elems := make([]string, 0, len(me.activeRequests)) 107 for wr := range me.activeRequests { 108 elems = append(elems, fmt.Sprintf("%v of [%v-%v)", wr.next, wr.begin, wr.end)) 109 } 110 lines = append(lines, "active requests: "+strings.Join(elems, ", ")) 111 } 112 return lines 113 } 114 115 func (ws *webseedPeer) String() string { 116 return fmt.Sprintf("webseed peer for %q", ws.client.Url) 117 } 118 119 func (ws *webseedPeer) onGotInfo(info *metainfo.Info) { 120 ws.client.SetInfo(info, ws.peer.t.fileSegmentsIndex.UnwrapPtr()) 121 // There should be probably be a callback in Client instead, so it can remove pieces at its whim 122 // too. 123 ws.client.Pieces.Iterate(func(x uint32) bool { 124 ws.peer.t.incPieceAvailability(pieceIndex(x)) 125 return true 126 }) 127 } 128 129 // Webseeds check the next request is wanted before reading it. 130 func (ws *webseedPeer) handleCancel(RequestIndex) {} 131 132 func (ws *webseedPeer) requestIndexTorrentOffset(r RequestIndex) int64 { 133 return ws.peer.t.requestIndexBegin(r) 134 } 135 136 func (ws *webseedPeer) intoSpec(begin, end RequestIndex) webseed.RequestSpec { 137 t := ws.peer.t 138 start := t.requestIndexBegin(begin) 139 endOff := t.requestIndexEnd(end - 1) 140 return webseed.RequestSpec{start, endOff - start} 141 } 142 143 func (ws *webseedPeer) spawnRequest(begin, end RequestIndex, logger *slog.Logger) { 144 extWsReq := ws.client.StartNewRequest(ws.peer.closedCtx, ws.intoSpec(begin, end), logger) 145 wsReq := webseedRequest{ 146 logger: logger, 147 request: extWsReq, 148 begin: begin, 149 next: begin, 150 end: end, 151 } 152 if ws.hasOverlappingRequests(begin, end) { 153 if webseed.PrintDebug { 154 logger.Warn( 155 "webseedPeer.spawnRequest: request overlaps existing", 156 "new", &wsReq, 157 "torrent", ws.peer.t) 158 } 159 ws.peer.t.cl.dumpCurrentWebseedRequests() 160 } 161 t := ws.peer.t 162 cl := t.cl 163 ws.activeRequests[&wsReq] = struct{}{} 164 t.deferUpdateRegularTrackerAnnouncing() 165 g.MakeMapIfNil(&cl.activeWebseedRequests) 166 g.MapMustAssignNew(cl.activeWebseedRequests, ws.getRequestKey(&wsReq), &wsReq) 167 ws.peer.updateExpectingChunks() 168 panicif.Zero(ws.hostKey) 169 ws.peer.t.cl.numWebSeedRequests[ws.hostKey]++ 170 ws.slogger().Debug( 171 "starting webseed request", 172 "begin", begin, 173 "end", end, 174 "len", end-begin, 175 ) 176 go ws.sliceProcessor(&wsReq) 177 } 178 179 func (me *webseedPeer) getRequestKey(wr *webseedRequest) webseedUniqueRequestKey { 180 // This is used to find the request in the Client's active requests map. 181 return webseedUniqueRequestKey{ 182 url: me.url, 183 t: me.peer.t, 184 sliceIndex: me.peer.t.requestIndexToWebseedSliceIndex(wr.begin), 185 } 186 } 187 188 func (me *webseedPeer) hasOverlappingRequests(begin, end RequestIndex) bool { 189 for req := range me.activeRequests { 190 if req.cancelled.Load() { 191 continue 192 } 193 if begin < req.end && end > req.begin { 194 return true 195 } 196 } 197 return false 198 } 199 200 func (ws *webseedPeer) readChunksErrorLevel(err error, req *webseedRequest) slog.Level { 201 if req.cancelled.Load() { 202 return slog.LevelDebug 203 } 204 if ws.peer.closedCtx.Err() != nil { 205 return slog.LevelDebug 206 } 207 var h2e http2.GoAwayError 208 if errors.As(err, &h2e) { 209 if h2e.ErrCode == http2.ErrCodeEnhanceYourCalm { 210 // It's fine, we'll sleep for a bit. But it's still interesting. 211 return slog.LevelInfo 212 } 213 } 214 var ne net.Error 215 if errors.As(err, &ne) && ne.Timeout() { 216 return slog.LevelInfo 217 } 218 if errors.Is(err, webseed.ErrStatusOkForRangeRequest{}) { 219 // This can happen for uncached results, and we get passed directly to origin. We should be 220 // coming back later and then only warning if it happens for a long time. 221 return slog.LevelDebug 222 } 223 // Error if we aren't also using and/or have peers...? 224 return slog.LevelWarn 225 } 226 227 // Reads chunks from the responses for the webseed slice. 228 func (ws *webseedPeer) sliceProcessor(webseedRequest *webseedRequest) { 229 // Detach cost association from webseed update requests routine. 230 pprof.SetGoroutineLabels(context.Background()) 231 locker := ws.locker 232 err := ws.readChunks(webseedRequest) 233 if webseed.PrintDebug && webseedRequest.next < webseedRequest.end { 234 fmt.Printf("webseed peer request %v in %v stopped reading chunks early: %v\n", webseedRequest, ws.peer.t.name(), err) 235 if err == nil { 236 ws.peer.t.cl.dumpCurrentWebseedRequests() 237 } 238 } 239 // Ensure the body reader and response are closed. 240 webseedRequest.Close() 241 if err != nil { 242 level := ws.readChunksErrorLevel(err, webseedRequest) 243 ws.slogger().Log(context.TODO(), level, "webseed request error", "err", err) 244 torrent.Add("webseed request error count", 1) 245 // This used to occur only on webseed.ErrTooFast but I think it makes sense to slow down any 246 // kind of error. Pausing here will starve the available requester slots which slows things 247 // down. TODO: Use the Retry-After implementation from Erigon. 248 select { 249 case <-ws.peer.closed.Done(): 250 case <-time.After(time.Duration(rand.Int63n(int64(10 * time.Second)))): 251 } 252 } 253 ws.slogger().Debug("webseed request ended") 254 locker.Lock() 255 // Delete this entry after waiting above on an error, to prevent more requests. 256 ws.deleteActiveRequest(webseedRequest) 257 cl := ws.peer.cl 258 if err == nil && cl.numWebSeedRequests[ws.hostKey] == webseedHostRequestConcurrency/2 { 259 cl.updateWebseedRequestsWithReason("webseedPeer.runRequest low water") 260 } else if cl.numWebSeedRequests[ws.hostKey] == 0 { 261 cl.updateWebseedRequestsWithReason("webseedPeer.runRequest zero requests") 262 } 263 locker.Unlock() 264 } 265 266 func (ws *webseedPeer) deleteActiveRequest(wr *webseedRequest) { 267 g.MustDelete(ws.activeRequests, wr) 268 if len(ws.activeRequests) == 0 { 269 ws.peer.t.deferUpdateRegularTrackerAnnouncing() 270 } 271 cl := ws.peer.cl 272 cl.numWebSeedRequests[ws.hostKey]-- 273 g.MustDelete(cl.activeWebseedRequests, ws.getRequestKey(wr)) 274 ws.peer.updateExpectingChunks() 275 } 276 277 func (ws *webseedPeer) connectionFlags() string { 278 return "WS" 279 } 280 281 // Maybe this should drop all existing connections, or something like that. 282 func (ws *webseedPeer) drop() {} 283 284 func (cn *webseedPeer) providedBadData() { 285 cn.convict(errors.New("provided bad data"), time.Minute) 286 } 287 288 func (ws *webseedPeer) onClose() { 289 ws.peer.t.iterPeers(func(p *Peer) { 290 if p.isLowOnRequests() { 291 p.onNeedUpdateRequests("webseedPeer.onClose") 292 } 293 }) 294 } 295 296 // Do we want a chunk, assuming it's valid etc. 297 func (ws *webseedPeer) wantChunk(ri RequestIndex) bool { 298 return ws.peer.t.wantReceiveChunk(ri) 299 } 300 301 func (ws *webseedPeer) maxChunkDiscard() RequestIndex { 302 return RequestIndex(int(intCeilDiv(webseed.MaxDiscardBytes, ws.peer.t.chunkSize))) 303 } 304 305 func (ws *webseedPeer) wantedChunksInDiscardWindow(wr *webseedRequest) bool { 306 // Shouldn't call this if request is at the end already. 307 panicif.GreaterThanOrEqual(wr.next, wr.end) 308 windowEnd := wr.next + ws.maxChunkDiscard() 309 panicif.LessThan(windowEnd, wr.next) 310 for ri := wr.next; ri < wr.end && ri <= wr.next+ws.maxChunkDiscard(); ri++ { 311 if ws.wantChunk(ri) { 312 return true 313 } 314 } 315 return false 316 } 317 318 func (ws *webseedPeer) readChunks(wr *webseedRequest) (err error) { 319 t := ws.peer.t 320 buf := t.getChunkBuffer() 321 defer t.putChunkBuffer(buf) 322 msg := pp.Message{ 323 Type: pp.Piece, 324 } 325 for { 326 reqSpec := t.requestIndexToRequest(wr.next) 327 chunkLen := reqSpec.Length.Int() 328 buf = buf[:chunkLen] 329 var n int 330 n, err = io.ReadFull(wr.request.Body, buf) 331 ws.peer.readBytes(int64(n)) 332 reqCtxErr := context.Cause(wr.request.Context()) 333 if errors.Is(err, reqCtxErr) { 334 err = reqCtxErr 335 } 336 if webseed.PrintDebug && wr.cancelled.Load() { 337 fmt.Printf("webseed read %v after cancellation: %v\n", n, err) 338 } 339 // We need this early for the convict call. 340 ws.peer.locker().Lock() 341 if err != nil { 342 // TODO: Pick out missing files or associate error with file. See also 343 // webseed.ReadRequestPartError. 344 if !wr.cancelled.Load() { 345 ws.convict(err, time.Minute) 346 } 347 ws.peer.locker().Unlock() 348 err = fmt.Errorf("reading chunk: %w", err) 349 return 350 } 351 // TODO: This happens outside Client lock, and stats can be written out of sync with each 352 // other. Why even bother with atomics? This needs to happen after the err check above. 353 ws.peer.doChunkReadStats(int64(n)) 354 // TODO: Clean up the parameters for receiveChunk. 355 msg.Piece = buf 356 msg.Index = reqSpec.Index 357 msg.Begin = reqSpec.Begin 358 359 // Ensure the request is pointing to the next chunk before receiving the current one. If 360 // webseed requests are triggered, we want to ensure our existing request is up to date. 361 wr.next++ 362 err = ws.peer.receiveChunk(&msg) 363 stop := err != nil || wr.next >= wr.end 364 if !stop { 365 if !ws.wantedChunksInDiscardWindow(wr) { 366 // This cancels the stream, but we don't stop su--reading to make the most of the 367 // buffered body. 368 wr.Cancel("no wanted chunks in discard window", ws.peer.t) 369 } 370 } 371 ws.peer.locker().Unlock() 372 373 if err != nil { 374 err = fmt.Errorf("processing chunk: %w", err) 375 } 376 if stop { 377 return 378 } 379 } 380 } 381 382 func (me *webseedPeer) peerPieces() *roaring.Bitmap { 383 return &me.client.Pieces 384 } 385 386 func (cn *webseedPeer) peerHasAllPieces() (all, known bool) { 387 if !cn.peer.t.haveInfo() { 388 return true, false 389 } 390 return cn.client.Pieces.GetCardinality() == uint64(cn.peer.t.numPieces()), true 391 } 392 393 func (me *webseedPeer) slogger() *slog.Logger { 394 return me.peer.slogger 395 }