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  }