github.com/anacrolix/torrent@v1.61.0/client-tracker-announcer.go (about)

     1  package torrent
     2  
     3  import (
     4  	"cmp"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"log/slog"
    10  	"net/url"
    11  	"time"
    12  	"weak"
    13  
    14  	g "github.com/anacrolix/generics"
    15  	analog "github.com/anacrolix/log"
    16  	"github.com/anacrolix/missinggo/v2/panicif"
    17  	"github.com/anacrolix/torrent/internal/extracmp"
    18  	"github.com/anacrolix/torrent/internal/indexed"
    19  	"github.com/anacrolix/torrent/internal/mytimer"
    20  	"github.com/anacrolix/torrent/tracker"
    21  	trHttp "github.com/anacrolix/torrent/tracker/http"
    22  )
    23  
    24  // Designed in a way to allow switching to an event model if required. If multiple slots are allowed
    25  // per tracker it would be handled here. Currently, handles only regular trackers but let's see if
    26  // we can get websocket trackers to use this too.
    27  type regularTrackerAnnounceDispatcher struct {
    28  	torrentClient *Client
    29  	logger        *slog.Logger
    30  
    31  	trackerClients map[trackerAnnouncerKey]*trackerClientsValue
    32  	announceStates map[torrentTrackerAnnouncerKey]*announceState
    33  	// Save torrents so we can fetch announce request fields even when the torrent Client has
    34  	// dropped it. We should just prefer to remember the fields we need. Ideally this would map all
    35  	// short infohash forms to the same value. We're using weak.Pointer because we need to clean it
    36  	// up at some point, if this crashes I know to fix it.
    37  	torrentForAnnounceRequests map[shortInfohash]weak.Pointer[Torrent]
    38  
    39  	// Raw announce data keyed by announcer and short infohash.
    40  	announceData indexed.Map[torrentTrackerAnnouncerKey, nextAnnounceInput]
    41  	// Announcing sorted by url then priority.
    42  	announceIndex indexed.Index[nextAnnounceRecord]
    43  	overdueIndex  indexed.Index[nextAnnounceRecord]
    44  
    45  	trackerAnnounceHead indexed.Table[trackerAnnounceHeadRecord]
    46  	nextAnnounce        indexed.Index[trackerAnnounceHeadRecord]
    47  
    48  	infohashAnnouncing indexed.Map[shortInfohash, infohashConcurrency]
    49  	trackerAnnouncing  indexed.Map[trackerAnnouncerKey, int]
    50  	timer              mytimer.Timer
    51  }
    52  
    53  type trackerAnnounceHeadRecord struct {
    54  	trackerRequests int // Count of active concurrent requests to a given tracker.
    55  	nextAnnounceRecord
    56  }
    57  
    58  type trackerClientsValue struct {
    59  	client tracker.Client
    60  	//active int
    61  }
    62  
    63  // According to compareNextAnnounce, which is universal, and we only need to handle the non-zero
    64  // value fields.
    65  func nextAnnounceMinRecord() (ret nextAnnounceRecord) {
    66  	ret.nextAnnounceInput = nextAnnounceInputMin()
    67  	return
    68  }
    69  
    70  func nextAnnounceInputMin() (ret nextAnnounceInput) {
    71  	ret.overdue = true
    72  	ret.torrent.Ok = true
    73  	ret.torrent.Value.NeedData = true
    74  	ret.torrent.Value.WantPeers = true
    75  	ret.AnnounceEvent = tracker.Started
    76  	return
    77  }
    78  
    79  func (me *regularTrackerAnnounceDispatcher) init(client *Client) {
    80  	me.torrentClient = client
    81  	me.logger = client.slogger
    82  	me.announceData.Init(torrentTrackerAnnouncerKey.Compare)
    83  	me.announceData.SetMinRecord(torrentTrackerAnnouncerKey{})
    84  	// This is super pedantic, we're checking distinct root tables are synced with each other. In
    85  	// this case there's a trigger in infohashAnnouncing to update all the corresponding infohashes
    86  	// in announceData. Anytime announceData is changed, we check it's still up to date with
    87  	// infohashAnnouncing.
    88  	me.announceData.OnChange(func(old, new g.Option[indexed.Pair[torrentTrackerAnnouncerKey, nextAnnounceInput]]) {
    89  		if !new.Ok {
    90  			return
    91  		}
    92  		// Due to trigger chains that result in announceData being updated *for unrelated fields*,
    93  		// the check occurred prematurely while updating announceData. The fix is to update all
    94  		// indexes, then to do triggers. This is massive overkill for this project right now.
    95  		actual := new.Value.Right.infohashActive
    96  		key := new.Value.Left
    97  		expected := g.OptionFromTuple(me.infohashAnnouncing.Get(key.ShortInfohash)).Value.count
    98  		if actual != expected {
    99  			me.logger.Debug(
   100  				"announceData.infohashActive != infohashAnnouncing.count",
   101  				"key", key,
   102  				"actual", actual,
   103  				"expected", expected)
   104  		}
   105  	})
   106  	me.announceIndex = indexed.NewFullMappedIndex(
   107  		&me.announceData,
   108  		announceIndexCompare,
   109  		nextAnnounceRecordFromPair,
   110  		nextAnnounceMinRecord(),
   111  	)
   112  	me.overdueIndex = indexed.NewFullMappedIndex(
   113  		&me.announceData,
   114  		overdueIndexCompare,
   115  		nextAnnounceRecordFromPair,
   116  		func() (ret nextAnnounceRecord) {
   117  			ret.overdue = true
   118  			return
   119  		}(),
   120  	)
   121  	me.trackerAnnounceHead.Init(func(a, b trackerAnnounceHeadRecord) int {
   122  		return cmp.Compare(a.url, b.url)
   123  	})
   124  	// Just empty url.
   125  	me.trackerAnnounceHead.SetMinRecord(trackerAnnounceHeadRecord{})
   126  	me.nextAnnounce = indexed.NewFullIndex(
   127  		&me.trackerAnnounceHead,
   128  		func(a, b trackerAnnounceHeadRecord) int {
   129  			return cmp.Or(
   130  				cmp.Compare(a.trackerRequests, b.trackerRequests),
   131  				compareNextAnnounce(a.nextAnnounceInput, b.nextAnnounceInput),
   132  				a.torrentTrackerAnnouncerKey.Compare(b.torrentTrackerAnnouncerKey),
   133  			)
   134  		},
   135  		func() (ret trackerAnnounceHeadRecord) {
   136  			ret.nextAnnounceInput = nextAnnounceInputMin()
   137  			return
   138  		}(),
   139  	)
   140  	// After announce index changes (we need the ordering), update the next announce for each
   141  	// tracker url.
   142  	me.announceData.OnChange(func(old, new g.Option[indexed.Pair[torrentTrackerAnnouncerKey, nextAnnounceInput]]) {
   143  		if old.Ok {
   144  			me.updateTrackerAnnounceHead(old.Value.Left.url)
   145  		}
   146  		if new.Ok {
   147  			me.updateTrackerAnnounceHead(new.Value.Left.url)
   148  		}
   149  	})
   150  	me.infohashAnnouncing.Init(shortInfohash.Compare)
   151  	me.infohashAnnouncing.OnValueChange(func(shortIh shortInfohash, old, new g.Option[infohashConcurrency]) {
   152  		start := me.announceData.MinRecord()
   153  		start.Left.ShortInfohash = shortIh
   154  		keys := make([]torrentTrackerAnnouncerKey, 0, len(me.trackerClients))
   155  		var expectedCount g.Option[int]
   156  		for r := range indexed.IterClusteredWhere(
   157  			me.announceData,
   158  			start,
   159  			func(p indexed.Pair[torrentTrackerAnnouncerKey, nextAnnounceInput]) bool {
   160  				return p.Left.ShortInfohash == shortIh
   161  			},
   162  		) {
   163  			if expectedCount.Ok {
   164  				panicif.NotEq(r.Right.infohashActive, expectedCount.Value)
   165  			} else {
   166  				expectedCount.Set(r.Right.infohashActive)
   167  			}
   168  			if r.Right.infohashActive != new.Value.count {
   169  				keys = append(keys, r.Left)
   170  			}
   171  		}
   172  		for _, key := range keys {
   173  			panicif.False(me.announceData.Update(
   174  				key,
   175  				func(input nextAnnounceInput) nextAnnounceInput {
   176  					input.infohashActive = new.Value.count
   177  					return input
   178  				},
   179  			).Exists)
   180  		}
   181  	})
   182  	me.trackerAnnouncing.Init(cmp.Compare)
   183  	me.trackerAnnouncing.OnValueChange(func(key trackerAnnouncerKey, old, new g.Option[int]) {
   184  		panicif.GreaterThan(new.Value, maxConcurrentAnnouncesPerTracker)
   185  		me.updateTrackerAnnounceHead(key)
   186  		// This could be modified to use "instead of" triggers, or alter the new value in a before
   187  		// or something.
   188  		if new.Value == 0 {
   189  			me.trackerAnnouncing.Delete(key)
   190  		}
   191  	})
   192  	me.timer.Init(time.Now(), me.timerFunc)
   193  }
   194  
   195  // Updates the derived tracker announce head table.
   196  func (me *regularTrackerAnnounceDispatcher) updateTrackerAnnounceHead(url trackerAnnouncerKey) {
   197  	new := me.getTrackerNextAnnounce(url)
   198  	if new.Ok {
   199  		tr := g.OptionFromTuple(me.trackerAnnouncing.Get(url)).Value
   200  		//fmt.Printf("tracker %v has %v announces\n", url, tr)
   201  		me.trackerAnnounceHead.CreateOrReplace(trackerAnnounceHeadRecord{
   202  			trackerRequests:    tr,
   203  			nextAnnounceRecord: new.Unwrap(),
   204  		})
   205  	} else {
   206  		//fmt.Println("looking up", url, "got nothing")
   207  		key := me.trackerAnnounceHead.MinRecord()
   208  		key.url = url
   209  		me.trackerAnnounceHead.Delete(key)
   210  	}
   211  	panicif.NotEq(me.trackerAnnounceHead.Len(), me.nextAnnounce.Len())
   212  	panicif.GreaterThan(me.trackerAnnounceHead.Len(), len(me.trackerClients))
   213  	me.updateTimer()
   214  }
   215  
   216  func nextAnnounceRecordFromParts(key torrentTrackerAnnouncerKey, input nextAnnounceInput) nextAnnounceRecord {
   217  	return nextAnnounceRecord{
   218  		torrentTrackerAnnouncerKey: key,
   219  		nextAnnounceInput:          input,
   220  	}
   221  }
   222  
   223  func nextAnnounceRecordFromPair(from indexed.Pair[torrentTrackerAnnouncerKey, nextAnnounceInput]) nextAnnounceRecord {
   224  	return nextAnnounceRecordFromParts(from.Left, from.Right)
   225  }
   226  
   227  func announceIndexCompare(a, b nextAnnounceRecord) int {
   228  	return cmp.Or(
   229  		cmp.Compare(a.url, b.url),
   230  		compareNextAnnounce(a.nextAnnounceInput, b.nextAnnounceInput),
   231  		a.ShortInfohash.Compare(b.ShortInfohash),
   232  	)
   233  }
   234  
   235  type infohashConcurrency struct {
   236  	count int
   237  }
   238  
   239  // Picks the best announce for a given tracker, and applies filters from announce concurrency limits.
   240  func (me *regularTrackerAnnounceDispatcher) getTrackerNextAnnounce(key trackerAnnouncerKey) (_ g.Option[nextAnnounceRecord]) {
   241  	panicif.NotEq(me.announceIndex.Len(), me.announceData.Len())
   242  	gte := me.announceIndex.MinRecord()
   243  	gte.url = key
   244  	return indexed.IterClusteredWhere(me.announceIndex, gte, func(r nextAnnounceRecord) bool {
   245  		return r.url == key
   246  	}).First()
   247  }
   248  
   249  var nextAnnounceRecordCols = []any{
   250  	"Tracker",
   251  	"ShortInfohash",
   252  	"active",
   253  	"Overdue",
   254  	"UntilWhen",
   255  	"|ih|",
   256  	"WantPeers",
   257  	"NeedData",
   258  	"Progress",
   259  	"Webseeds",
   260  	"Event",
   261  	"status line",
   262  }
   263  
   264  func (me *regularTrackerAnnounceDispatcher) printNextAnnounceRecordTable(
   265  	sw statusWriter,
   266  	table indexed.Index[nextAnnounceRecord],
   267  ) {
   268  	tab := sw.tab()
   269  	tab.cols(nextAnnounceRecordCols...)
   270  	tab.row()
   271  	for r := range table.Iter {
   272  		me.putNextAnnounceRecordCols(tab, r)
   273  		tab.row()
   274  	}
   275  	tab.end()
   276  }
   277  
   278  func (me *regularTrackerAnnounceDispatcher) printNextAnnounceTable(
   279  	sw statusWriter,
   280  	table indexed.Index[trackerAnnounceHeadRecord],
   281  ) {
   282  	tab := sw.tab()
   283  	tab.cols("#tr")
   284  	tab.cols(nextAnnounceRecordCols...)
   285  	tab.row()
   286  	for r := range table.Iter {
   287  		tab.cols(r.trackerRequests)
   288  		me.putNextAnnounceRecordCols(tab, r.nextAnnounceRecord)
   289  		tab.row()
   290  	}
   291  	tab.end()
   292  }
   293  
   294  func (me *regularTrackerAnnounceDispatcher) putNextAnnounceRecordCols(
   295  	tab *tableWriter,
   296  	r nextAnnounceRecord,
   297  ) {
   298  	t := me.torrentFromShortInfohash(r.ShortInfohash)
   299  	progress := "dropped"
   300  	if t != nil {
   301  		progress = fmt.Sprintf("%d%%", int(100*t.progressUnitFloat()))
   302  	}
   303  	tab.cols(
   304  		r.url,
   305  		r.ShortInfohash,
   306  		r.active,
   307  		r.overdue,
   308  		time.Until(r.When),
   309  		r.infohashActive,
   310  		r.torrent.Value.WantPeers,
   311  		r.torrent.Value.NeedData,
   312  		progress,
   313  		r.torrent.Value.HasActiveWebseedRequests,
   314  		r.AnnounceEvent,
   315  		regularTrackerScraperStatusLine(*me.announceStates[r.torrentTrackerAnnouncerKey]),
   316  	)
   317  }
   318  
   319  func (me *regularTrackerAnnounceDispatcher) writeStatus(w io.Writer) {
   320  	sw := statusWriter{w: w}
   321  	// TODO: Print active announces
   322  	sw.f("timer next: %v\n", time.Until(me.timer.When()))
   323  	sw.f("Next announces:\n")
   324  	for sw := range indented(sw) {
   325  		me.printNextAnnounceRecordTable(sw, me.announceIndex)
   326  	}
   327  	fmt.Fprintln(sw, "Next announces")
   328  	for sw := range sw.indented() {
   329  		me.printNextAnnounceTable(sw, me.nextAnnounce)
   330  	}
   331  }
   332  
   333  // This moves values that have When that have passed, so we compete on other parts of the priority
   334  // if there is more than one pending. This can be done with another index, and have values move back
   335  // the other way to simplify things.
   336  func (me *regularTrackerAnnounceDispatcher) updateOverdue() {
   337  	now := time.Now()
   338  	start := me.overdueIndex.MinRecord()
   339  	start.When = now.Add(1)
   340  	end := me.overdueIndex.MinRecord()
   341  	end.overdue = false
   342  	end.When = now.Add(1)
   343  
   344  	// This stops recursive thrashing while we pivot on a fixed now.
   345  	var updateKeys []torrentTrackerAnnouncerKey
   346  	for r := range indexed.IterRange(me.overdueIndex, start, end) {
   347  		updateKeys = append(updateKeys, r.torrentTrackerAnnouncerKey)
   348  	}
   349  	for _, key := range updateKeys {
   350  		// There's no guarantee we actually change anything, the overdue might remain the same due
   351  		// to timing.
   352  		panicif.False(me.announceData.Update(
   353  			key,
   354  			func(value nextAnnounceInput) nextAnnounceInput {
   355  				// For recursive updates, we make sure to monotonically progress state. (Now always
   356  				// forward, so we are always agreeing with other instances of updateOverdue).
   357  				value.overdue = value.When.Compare(time.Now()) <= 0
   358  				return value
   359  			},
   360  		).Exists)
   361  	}
   362  }
   363  
   364  func (me *regularTrackerAnnounceDispatcher) timerFunc() mytimer.TimeValue {
   365  	me.torrentClient.lock()
   366  	ret := me.step()
   367  	me.torrentClient.unlock()
   368  	return ret
   369  }
   370  
   371  // The progress method, called by the timer.
   372  func (me *regularTrackerAnnounceDispatcher) step() mytimer.TimeValue {
   373  	me.dispatchAnnounces()
   374  	// We *are* the Sen... Timer.
   375  	return me.nextTimerDelay()
   376  }
   377  
   378  func (me *regularTrackerAnnounceDispatcher) addKey(key torrentTrackerAnnouncerKey) {
   379  	if me.announceData.ContainsKey(key) {
   380  		return
   381  	}
   382  	t := me.torrentFromShortInfohash(key.ShortInfohash)
   383  	if t == nil {
   384  		// Crude, but the torrent was already dropped. We probably called AddTrackers late.
   385  		return
   386  	}
   387  	g.MakeMapIfNil(&me.torrentForAnnounceRequests)
   388  	// This can be duplicated when there's multiple trackers for a short infohash. That's fine.
   389  	me.torrentForAnnounceRequests[key.ShortInfohash] = weak.Make(t)
   390  	if !g.MapContains(me.announceStates, key) {
   391  		g.MakeMapIfNil(&me.announceStates)
   392  		g.MapMustAssignNew(me.announceStates, key, g.PtrTo(announceState{}))
   393  	}
   394  	t.regularTrackerAnnounceState[key] = g.MapMustGet(me.announceStates, key)
   395  	me.announceData.Create(key, nextAnnounceInput{
   396  		torrent:                me.makeTorrentInput(t),
   397  		nextAnnounceStateInput: me.makeAnnounceStateInput(key),
   398  		infohashActive:         g.OptionFromTuple(me.infohashAnnouncing.Get(key.ShortInfohash)).Value.count,
   399  	})
   400  	me.updateTimer()
   401  }
   402  
   403  // Returns nil if the torrent was dropped.
   404  func (me *regularTrackerAnnounceDispatcher) torrentFromShortInfohash(short shortInfohash) *Torrent {
   405  	return me.torrentClient.torrentsByShortHash[short]
   406  }
   407  
   408  const maxConcurrentAnnouncesPerTracker = 2
   409  
   410  // Returns true if an announce was dispatched and should be tried again.
   411  func (me *regularTrackerAnnounceDispatcher) dispatchAnnounces() {
   412  	for {
   413  		next := me.getNextAnnounce()
   414  		if !next.Ok {
   415  			break
   416  		}
   417  		t := me.torrentFromShortInfohash(next.Value.ShortInfohash)
   418  		// Check that torrent input synchronization is working. At this point, running in the
   419  		// dispatcher role, everything should be synced. Other state in the announce data index is
   420  		// now the original.
   421  		{
   422  			actual := next.Value.torrent
   423  			expected := me.makeTorrentInput(t)
   424  			if actual != expected {
   425  				me.logger.Warn("announce dispatcher torrent input is not synced",
   426  					"expected", fmt.Sprintf("%#v", expected),
   427  					"actual", fmt.Sprintf("%#v", actual))
   428  			}
   429  		}
   430  		if !next.Value.overdue {
   431  			break
   432  		}
   433  		panicif.True(next.Value.When.After(time.Now()))
   434  		panicif.True(next.Value.active)
   435  		me.startAnnounce(next.Value.torrentTrackerAnnouncerKey)
   436  	}
   437  }
   438  
   439  func (me *regularTrackerAnnounceDispatcher) startAnnounce(key torrentTrackerAnnouncerKey) {
   440  	next, ok := me.announceData.Get(key)
   441  	panicif.False(ok)
   442  	panicif.False(me.announceData.Update(key, func(r nextAnnounceInput) nextAnnounceInput {
   443  		panicif.True(r.active)
   444  		r.active = true
   445  		return r
   446  	}).Exists)
   447  	me.alterInfohashConcurrency(key.ShortInfohash, func(existing int) int {
   448  		return existing + 1
   449  	})
   450  	me.trackerAnnouncing.UpdateOrCreate(key.url, func(i int) int {
   451  		return i + 1
   452  	})
   453  	me.updateTrackerAnnounceHead(key.url)
   454  	go me.singleAnnounceAttempter(key, next.AnnounceEvent)
   455  }
   456  
   457  func (me *regularTrackerAnnounceDispatcher) alterInfohashConcurrency(ih shortInfohash, update func(existing int) int) {
   458  	me.infohashAnnouncing.Alter(
   459  		ih,
   460  		func(ic infohashConcurrency, b bool) (infohashConcurrency, bool) {
   461  			ic.count = update(ic.count)
   462  			panicif.LessThan(ic.count, 0)
   463  			return ic, ic.count > 0
   464  		})
   465  }
   466  
   467  func (me *regularTrackerAnnounceDispatcher) finishedAnnounce(key torrentTrackerAnnouncerKey) {
   468  	me.alterInfohashConcurrency(key.ShortInfohash, func(existing int) int { return existing - 1 })
   469  	me.announceData.Update(key, func(r nextAnnounceInput) nextAnnounceInput {
   470  		panicif.False(r.active)
   471  		r.active = false
   472  		// Should this be from the updateTorrentInput method?
   473  		r.torrent = me.makeTorrentInput(me.torrentFromShortInfohash(key.ShortInfohash))
   474  		return r
   475  	})
   476  	me.trackerAnnouncing.Update(key.url, func(i int) int {
   477  		return i - 1
   478  	})
   479  	me.updateTimer()
   480  }
   481  
   482  func (me *regularTrackerAnnounceDispatcher) syncAnnounceState(key torrentTrackerAnnouncerKey) {
   483  	input := me.makeAnnounceStateInput(key)
   484  	me.announceData.UpdateOrCreate(key, func(old nextAnnounceInput) nextAnnounceInput {
   485  		old.nextAnnounceStateInput = input
   486  		return old
   487  	})
   488  }
   489  
   490  func (me *regularTrackerAnnounceDispatcher) updateTorrentInput(t *Torrent) {
   491  	input := me.makeTorrentInput(t)
   492  	changed := false
   493  	for key := range t.regularTrackerAnnounceState {
   494  		panicif.Zero(key.url)
   495  		panicif.Zero(key.ShortInfohash)
   496  		// Avoid clobbering derived and unrelated values (overdue and active).
   497  		res := me.announceData.Update(
   498  			key,
   499  			func(av nextAnnounceInput) nextAnnounceInput {
   500  				av.torrent = input
   501  				// Because completion event
   502  				av.nextAnnounceStateInput = me.makeAnnounceStateInput(key)
   503  				return av
   504  			},
   505  		)
   506  		panicif.False(res.Exists)
   507  		changed = changed || res.Changed
   508  	}
   509  	// 'Twould be better to have a change trigger on nextAnnounce, but I'm in a hurry.
   510  	if changed {
   511  		me.updateTimer()
   512  	}
   513  }
   514  
   515  func (me *regularTrackerAnnounceDispatcher) nextTimerDelay() mytimer.TimeValue {
   516  	next := me.getNextAnnounce()
   517  	return next.Value.When
   518  }
   519  
   520  func (me *regularTrackerAnnounceDispatcher) updateTimer() {
   521  	me.timer.Update(me.nextTimerDelay())
   522  }
   523  
   524  func (me *regularTrackerAnnounceDispatcher) singleAnnounceAttempter(key torrentTrackerAnnouncerKey, event tracker.AnnounceEvent) {
   525  	me.torrentClient.lock()
   526  	defer me.torrentClient.unlock()
   527  	defer me.finishedAnnounce(key)
   528  	ih := key.ShortInfohash
   529  	logger := me.logger.With(
   530  		"short infohash", ih,
   531  		"url", key.url,
   532  	)
   533  	t := me.getTorrentForAnnounceRequest(key.ShortInfohash)
   534  	if t == nil {
   535  		logger.Debug("skipping announce for GCed torrent")
   536  		me.updateAnnounceState(key, func(state *announceState) {
   537  			state.Err = errors.New("announce skipped: Torrent GCed")
   538  			state.lastAttemptCompleted = time.Now()
   539  		})
   540  	} else {
   541  		me.singleAnnounce(key, event, logger, t)
   542  	}
   543  }
   544  
   545  // Actually do an announce. We know *Torrent is accessible.
   546  func (me *regularTrackerAnnounceDispatcher) singleAnnounce(
   547  	key torrentTrackerAnnouncerKey,
   548  	event tracker.AnnounceEvent,
   549  	logger *slog.Logger,
   550  	t *Torrent,
   551  ) {
   552  	// A logger that includes the nice torrent group so we know what the announce is for.
   553  	logger = logger.With(t.slogGroup())
   554  	req := t.announceRequest(event, key.ShortInfohash)
   555  	me.torrentClient.unlock()
   556  	ctx, cancel := context.WithTimeout(context.TODO(), tracker.DefaultTrackerAnnounceTimeout)
   557  	defer cancel()
   558  	logger.Debug("announcing", "req", req)
   559  	resp, err := me.trackerClients[key.url].client.Announce(ctx, req, me.getAnnounceOpts())
   560  	now := time.Now()
   561  	{
   562  		level := slog.LevelDebug
   563  		if err != nil {
   564  			level = analog.SlogErrorLevel(err).UnwrapOr(level)
   565  		}
   566  		// numPeers is (.resp.Peers | length) with jq...
   567  		logger.Log(context.Background(), level, "announced", "resp", resp, "err", err)
   568  	}
   569  
   570  	me.torrentClient.lock()
   571  	me.updateAnnounceState(key, func(state *announceState) {
   572  		state.Err = err
   573  		state.lastAttemptCompleted = now
   574  		if err == nil {
   575  			state.lastOk = lastAnnounceOk{
   576  				AnnouncedEvent: req.Event,
   577  				Interval:       time.Duration(resp.Interval) * time.Second,
   578  				NumPeers:       len(resp.Peers),
   579  				Completed:      now,
   580  			}
   581  			if req.Event == tracker.Completed {
   582  				state.sentCompleted = true
   583  			}
   584  		}
   585  	})
   586  	t.addPeers(peerInfos(nil).AppendFromTracker(resp.Peers))
   587  }
   588  
   589  // Updates the announce state, shared by regularTrackerAnnounceDispatcher and Torrent, but it lives in Torrent
   590  // for now.
   591  func (me *regularTrackerAnnounceDispatcher) updateAnnounceState(
   592  	key torrentTrackerAnnouncerKey,
   593  	update func(state *announceState),
   594  ) {
   595  	// It should always be inserted before an update could occur. It should only be removed by the
   596  	// dispatcher. So it should never be nil here.
   597  	as := me.announceStates[key]
   598  	update(as)
   599  	me.syncAnnounceState(key)
   600  }
   601  
   602  func (me *regularTrackerAnnounceDispatcher) getAnnounceOpts() trHttp.AnnounceOpt {
   603  	cfg := me.torrentClient.config
   604  	return trHttp.AnnounceOpt{
   605  		UserAgent: cfg.HTTPUserAgent,
   606  		// TODO: Bring this back.
   607  		//HostHeader:          me.urlHost,
   608  		ClientIp4:           cfg.PublicIp4,
   609  		ClientIp6:           cfg.PublicIp6,
   610  		HttpRequestDirector: cfg.HttpRequestDirector,
   611  	}
   612  }
   613  
   614  // Picks the most eligible announce then filters it if it's not allowed.
   615  func (me *regularTrackerAnnounceDispatcher) getNextAnnounce() (_ g.Option[nextAnnounceRecord]) {
   616  	me.updateOverdue()
   617  	v, ok := me.nextAnnounce.GetFirst()
   618  	ok = ok && !v.active && v.trackerRequests < maxConcurrentAnnouncesPerTracker
   619  	return g.OptionFromTuple(v.nextAnnounceRecord, ok)
   620  }
   621  
   622  func (me *regularTrackerAnnounceDispatcher) makeAnnounceStateInput(key torrentTrackerAnnouncerKey) nextAnnounceStateInput {
   623  	panicif.Zero(me.torrentClient)
   624  	state := me.announceStates[key]
   625  	event, when := me.nextAnnounceEvent(key)
   626  	return nextAnnounceStateInput{
   627  		AnnounceEvent:      event,
   628  		When:               when,
   629  		LastAnnounceFailed: state.Err != nil,
   630  	}
   631  }
   632  
   633  func (me *regularTrackerAnnounceDispatcher) makeTorrentInput(t *Torrent) (_ g.Option[nextAnnounceTorrentInput]) {
   634  	// No torrent means the client has lost interest and the dispatcher just does followup actions.
   635  	// If we drop a torrent, we still end up here but with a torrent that should result in None, so
   636  	// check for that.
   637  	if t == nil || t.isDropped() {
   638  		return
   639  	}
   640  	return g.Some(nextAnnounceTorrentInput{
   641  		NeedData:                 t.needData(),
   642  		WantPeers:                t.wantPeers(),
   643  		HasActiveWebseedRequests: t.hasActiveWebseedRequests(),
   644  	})
   645  }
   646  
   647  // Make zero/default unhandled AnnounceEvent sort last.
   648  var eventOrdering = map[tracker.AnnounceEvent]int{
   649  	tracker.Started: -4, // Get peers ASAP
   650  	tracker.Stopped: -3, // Stop unwanted peers ASAP
   651  	// Maybe prevent seeders from connecting to us. We want to send this before Stopped, but also we
   652  	// don't want people connecting to us if we are stopped and can only get out a single message.
   653  	// Really we should send this before Stopped...
   654  	tracker.Completed: -2,
   655  	tracker.None:      -1, // Regular maintenance
   656  }
   657  
   658  func overdueIndexCompare(a, b nextAnnounceRecord) int {
   659  	return cmp.Or(
   660  		compareOverdue(a.nextAnnounceInput, b.nextAnnounceInput),
   661  		a.torrentTrackerAnnouncerKey.Compare(b.torrentTrackerAnnouncerKey),
   662  	)
   663  }
   664  
   665  func compareOverdue(a, b nextAnnounceInput) int {
   666  	return cmp.Or(
   667  		-extracmp.CompareBool(a.overdue, b.overdue),
   668  		a.When.Compare(b.When),
   669  	)
   670  }
   671  
   672  func compareNextAnnounce(ar, br nextAnnounceInput) (ret int) {
   673  	// What about pushing back based on last announce failure? Some infohashes aren't liked by
   674  	// trackers.
   675  
   676  	ret = cmp.Or(
   677  		extracmp.CompareBool(ar.active, br.active),
   678  		-extracmp.CompareBool(ar.overdue, br.overdue),
   679  	)
   680  	if ret != 0 {
   681  		return
   682  	}
   683  	panicif.NotEq(ar.overdue, br.overdue)
   684  	overdue := ar.overdue
   685  	whenCmp := ar.When.Compare(br.When)
   686  	if !overdue {
   687  		ret = whenCmp
   688  		if ret != 0 {
   689  			return
   690  		}
   691  	}
   692  	return cmp.Or(
   693  		cmp.Compare(ar.infohashActive, br.infohashActive),
   694  		-extracmp.CompareBool(ar.torrent.Ok, br.torrent.Ok),
   695  		-extracmp.CompareBool(ar.torrent.Value.WantPeers, br.torrent.Value.WantPeers),
   696  		-extracmp.CompareBool(ar.torrent.Value.NeedData, br.torrent.Value.NeedData),
   697  		extracmp.CompareBool(ar.torrent.Value.HasActiveWebseedRequests, br.torrent.Value.HasActiveWebseedRequests),
   698  		cmp.Compare(eventOrdering[ar.AnnounceEvent], eventOrdering[br.AnnounceEvent]),
   699  		// Sort on when again, to order amongst announces with the same priorities. Not sure if we
   700  		// want this. Might be masking or fixing a bug in overdue handling.
   701  		whenCmp,
   702  	)
   703  }
   704  
   705  type nextAnnounceRecord struct {
   706  	torrentTrackerAnnouncerKey
   707  	nextAnnounceInput
   708  }
   709  
   710  type nextAnnounceInput struct {
   711  	torrent g.Option[nextAnnounceTorrentInput]
   712  	nextAnnounceStateInput
   713  	infohashActive int
   714  	overdue        bool
   715  	active         bool
   716  }
   717  
   718  type nextAnnounceStateInput struct {
   719  	AnnounceEvent      tracker.AnnounceEvent
   720  	When               time.Time
   721  	LastAnnounceFailed bool
   722  }
   723  
   724  type nextAnnounceTorrentInput struct {
   725  	NeedData                 bool
   726  	WantPeers                bool
   727  	HasActiveWebseedRequests bool
   728  }
   729  
   730  // when.IsZero if there's nothing to do and the data can be forgotten.
   731  func (me *regularTrackerAnnounceDispatcher) nextAnnounceEvent(key torrentTrackerAnnouncerKey) (event tracker.AnnounceEvent, when time.Time) {
   732  	state := g.MapMustGet(me.announceStates, key)
   733  	lastOk := state.lastOk
   734  	t := me.torrentFromShortInfohash(key.ShortInfohash)
   735  	if t == nil {
   736  		// Our lastOk attempt was an error.
   737  		if state.Err != nil {
   738  			return
   739  		}
   740  		// We've never announced
   741  		if lastOk.Completed.IsZero() {
   742  			return
   743  		}
   744  		// We already left
   745  		if lastOk.AnnouncedEvent == tracker.Stopped {
   746  			return
   747  		}
   748  		return tracker.Stopped, time.Now()
   749  	}
   750  	// Extend `when` if there was an error on the lastOk attempt. Not required for Stopped because
   751  	// that gives up on error anyway.
   752  	defer func() {
   753  		if state.Err == nil || when.IsZero() {
   754  			return
   755  		}
   756  		minWhen := state.lastAttemptCompleted.Add(time.Minute)
   757  		if when.Before(minWhen) {
   758  			when = minWhen
   759  		}
   760  	}()
   761  	if !state.sentCompleted && t.sawInitiallyIncompleteData && t.haveAllPieces() {
   762  		return tracker.Completed, time.Now()
   763  	}
   764  	if lastOk.Completed.IsZero() {
   765  		// Returning now should be fine as sorting should occur on "overdue" derived value.
   766  		return tracker.Started, time.Now()
   767  	}
   768  	// TODO: Shorten and modify intervals here. Check for completion/stopped etc.
   769  	return tracker.None, lastOk.Completed.Add(lastOk.Interval)
   770  }
   771  
   772  type lastAnnounceOk struct {
   773  	AnnouncedEvent tracker.AnnounceEvent
   774  	Interval       time.Duration
   775  	Completed      time.Time
   776  	NumPeers       int
   777  }
   778  
   779  type announceState struct {
   780  	lastOk               lastAnnounceOk
   781  	Err                  error
   782  	lastAttemptCompleted time.Time
   783  	// Has ever sent completed event. Should only be sent once.
   784  	sentCompleted bool
   785  }
   786  
   787  func (cl *Client) startTrackerAnnouncer(u *url.URL, urlStr trackerAnnouncerKey) {
   788  	cl.regularTrackerAnnounceDispatcher.initTrackerClient(u, urlStr, cl.config, cl.logger)
   789  }
   790  
   791  func (me *regularTrackerAnnounceDispatcher) initTrackerClient(
   792  	u *url.URL,
   793  	urlStr trackerAnnouncerKey,
   794  	config *ClientConfig,
   795  	logger analog.Logger,
   796  ) {
   797  	panicif.NotEq(u.String(), string(urlStr))
   798  	if g.MapContains(me.trackerClients, urlStr) {
   799  		return
   800  	}
   801  	// Parts of the old Announce code, here for reference, to help with mapping configuration to the
   802  	// new global client tracker implementation.
   803  	/*
   804  		res, err := tracker.Announce{
   805  			Context:             ctx,
   806  			HttpProxy:           me.t.cl.config.HTTPProxy,
   807  			HttpRequestDirector: me.t.cl.config.HttpRequestDirector,
   808  			DialContext:         me.t.cl.config.TrackerDialContext,
   809  			ListenPacket:        me.t.cl.config.TrackerListenPacket,
   810  			UserAgent:           me.t.cl.config.HTTPUserAgent,
   811  			TrackerUrl:          me.trackerUrl(ip),
   812  			Request:             req,
   813  			HostHeader:          me.u.Host,
   814  			ServerName:          me.u.Hostname(),
   815  			UdpNetwork:          me.u.Scheme,
   816  			ClientIp4:           krpc.NodeAddr{IP: me.t.cl.config.PublicIp4},
   817  			ClientIp6:           krpc.NodeAddr{IP: me.t.cl.config.PublicIp6},
   818  			Logger:              me.t.logger,
   819  		}.Do()
   820  
   821  		cl, err := NewClient(me.TrackerUrl, NewClientOpts{
   822  			Http: trHttp.NewClientOpts{
   823  				Proxy:       me.HttpProxy,
   824  				DialContext: me.DialContext,
   825  				ServerName:  me.ServerName,
   826  			},
   827  			UdpNetwork:   me.UdpNetwork,
   828  			Logger:       me.Logger.WithContextValue(fmt.Sprintf("tracker client for %q", me.TrackerUrl)),
   829  			ListenPacket: me.ListenPacket,
   830  		})
   831  		if err != nil {
   832  			return
   833  		}
   834  		defer cl.Close()
   835  		if me.Context == nil {
   836  			// This is just to maintain the old behaviour that should be a timeout of 15s. Users can
   837  			// override it by providing their own Context. See comments elsewhere about longer timeouts
   838  			// acting as rate limiting overloaded trackers.
   839  			ctx, cancel := context.WithTimeout(context.Background(), DefaultTrackerAnnounceTimeout)
   840  			defer cancel()
   841  			me.Context = ctx
   842  		}
   843  		return cl.Announce(me.Context, me.Request, trHttp.AnnounceOpt{
   844  			UserAgent:           me.UserAgent,
   845  			HostHeader:          me.HostHeader,
   846  			ClientIp4:           me.ClientIp4.IP,
   847  			ClientIp6:           me.ClientIp6.IP,
   848  			HttpRequestDirector: me.HttpRequestDirector,
   849  		})
   850  	*/
   851  	tc, err := tracker.NewClient(string(urlStr), tracker.NewClientOpts{
   852  		Http: trHttp.NewClientOpts{
   853  			Proxy:       config.HTTPProxy,
   854  			DialContext: config.TrackerDialContext,
   855  			ServerName:  u.Hostname(),
   856  		},
   857  		UdpNetwork:   u.Scheme,
   858  		Logger:       logger.WithContextValue(fmt.Sprintf("tracker client for %q", urlStr)),
   859  		ListenPacket: config.TrackerListenPacket,
   860  	})
   861  	panicif.Err(err)
   862  	// Need deep copy
   863  	panicif.NotNil(u.User)
   864  	//ta := &regularTrackerAnnounceDispatcher{
   865  	//	trackerClient: tc,
   866  	//	torrentClient: cl,
   867  	//	urlStr:        urlStr,
   868  	//	urlHost:       u.Host,
   869  	//	logger:        cl.slogger.With("tracker", u.String()),
   870  	//}
   871  	value := trackerClientsValue{
   872  		client: tc,
   873  	}
   874  
   875  	g.MakeMapIfNil(&me.trackerClients)
   876  	// TODO: Put the urlHost from here.
   877  	g.MapMustAssignNew(me.trackerClients, urlStr, &value)
   878  }
   879  
   880  // Returns nil if the Torrent has been GCd. Use this lazily as a way to stop caring about announcing
   881  // something, if we don't get to sending Completed or error in time.
   882  func (me *regularTrackerAnnounceDispatcher) getTorrentForAnnounceRequest(ih shortInfohash) *Torrent {
   883  	return g.MapMustGet(me.torrentForAnnounceRequests, ih).Value()
   884  }