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 := ®ularTrackerAnnounceDispatcher{ 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 }