package torrent
import (
+ "cmp"
"io"
"os"
"testing"
cfg.Seed = true
// Some test instances don't like this being on, even when there's no cache involved.
cfg.DropMutuallyCompletePeers = false
- if ps.SeederUploadRateLimiter != nil {
- cfg.UploadRateLimiter = ps.SeederUploadRateLimiter
- }
+ cfg.UploadRateLimiter = cmp.Or(ps.SeederUploadRateLimiter, cfg.UploadRateLimiter)
cfg.DataDir = greetingTempDir
if ps.ConfigureSeeder.Config != nil {
ps.ConfigureSeeder.Config(cfg)
c.peerImpl = c
c.setPeerLoggers(cl.logger, cl.slogger)
c.setRW(connStatsReadWriter{nc, c})
- c.r = &rateLimitedReader{
- l: cl.config.DownloadRateLimiter,
- r: c.r,
- }
+ c.r = cl.newDownloadRateLimitedReader(c.r)
c.logger.Levelf(
log.Debug,
"inited with remoteAddr %v network %v outgoing %t",
return
}
+func (cl *Client) newDownloadRateLimitedReader(r io.Reader) io.Reader {
+ if cl.config.DownloadRateLimiter == nil {
+ return r
+ }
+ // Why if the limit is Inf? Because it can be dynamically adjusted.
+ return rateLimitedReader{
+ l: cl.config.DownloadRateLimiter,
+ r: r,
+ }
+}
+
func (cl *Client) onDHTAnnouncePeer(ih metainfo.Hash, ip net.IP, port int, portOk bool) {
cl.lock()
defer cl.unlock()
// Check for bad arrangements. This is a candidate for an error state check method.
func (cl *Client) checkConfig() error {
- if cl.config.DownloadRateLimiter.Limit() == 0 {
+ if EffectiveDownloadRateLimit(cl.config.DownloadRateLimiter) == 0 {
if len(cl.dialers) != 0 {
return errors.New("download rate limit is zero, but dialers are set")
}
// rate-limiting io.Reader minus one. This is likely to be the larger of the main read loop
// buffer (~4096), and the requested chunk size (~16KiB, see TorrentSpec.ChunkSize). If limit is
// not Inf, and burst is left at 0, the implementation will choose a suitable burst.
+ //
+ // If the field is nil, no rate limiting is applied. And it can't be adjusted dynamically.
DownloadRateLimiter *rate.Limiter
// Maximum unverified bytes across all torrents. Not used if zero.
MaxUnverifiedBytes int64
MaxAllocPeerRequestDataPerConn: 1 << 20,
ListenHost: func(string) string { return "" },
UploadRateLimiter: unlimited,
- DownloadRateLimiter: unlimited,
DisableAcceptRateLimiting: true,
DropMutuallyCompletePeers: true,
HeaderObfuscationPolicy: HeaderObfuscationPolicy{
func (cfg *ClientConfig) setRateLimiterBursts() {
// What about chunk size?
- setRateLimiterBurstIfZero(cfg.UploadRateLimiter, cfg.MaxAllocPeerRequestDataPerConn)
- setRateLimiterBurstIfZero(
- cfg.DownloadRateLimiter,
- min(
- int(cfg.DownloadRateLimiter.Limit()),
- defaultDownloadRateLimiterBurst))
+ if cfg.UploadRateLimiter.Burst() == 0 {
+ cfg.UploadRateLimiter.SetBurst(cfg.MaxAllocPeerRequestDataPerConn)
+ }
+ setDefaultDownloadRateLimiterBurstIfZero(cfg.DownloadRateLimiter)
+}
+
+// Returns the download rate.Limit handling the special nil case.
+func EffectiveDownloadRateLimit(l *rate.Limiter) rate.Limit {
+ if l == nil {
+ return rate.Inf
+ }
+ return l.Limit()
}
func BenchmarkConnectionMainReadLoop(b *testing.B) {
var cl Client
- cl.init(&ClientConfig{
- DownloadRateLimiter: unlimited,
- })
+ cl.init(&ClientConfig{})
cl.initLogger()
ts := &torrentStorage{}
t := cl.newTorrentForTesting()
package torrent
import (
+ "math"
+
"golang.org/x/time/rate"
)
-// 64 KiB used to be a rough default buffer for sockets on Windows. I'm sure it's bigger
-// these days. What about the read buffer size mentioned elsewhere? Note this is also used for
-// webseeding since that shares the download rate limiter by default.
-const defaultDownloadRateLimiterBurst = 1 << 16
+// 64 KiB used to be a rough default buffer for sockets on Windows. I'm sure it's bigger these days.
+// What about the read buffer size mentioned elsewhere? Note this is also used for webseeding since
+// that shares the download rate limiter by default. 1 MiB is the default max read frame size for
+// HTTP/2,
+const defaultMinDownloadRateLimiterBurst = 1 << 20
// Sets rate limiter burst if it's set to zero which is used to request the default by our API.
func setRateLimiterBurstIfZero(l *rate.Limiter, def int) {
- if l.Burst() == 0 && l.Limit() != rate.Inf {
+ // Set it to something reasonable if the limit is Inf, in case the limit is dynamically adjusted
+ // and the user doesn't know what value to use. Assume the original limit is in a reasonable
+ // ballpark.
+ if l != nil && l.Burst() == 0 {
// What if the limit is greater than what can be represented by int?
l.SetBurst(def)
}
}
+
+// Sets rate limiter burst if it's set to zero which is used to request the default by our API.
+func setDefaultDownloadRateLimiterBurstIfZero(l *rate.Limiter) {
+ setRateLimiterBurstIfZero(l, int(
+ max(
+ min(EffectiveDownloadRateLimit(l), math.MaxInt),
+ defaultMinDownloadRateLimiterBurst)))
+}
"golang.org/x/time/rate"
)
+func newRateLimitedReader(r io.Reader, l *rate.Limiter) io.Reader {
+ if l == nil {
+ // Avoids taking Limiter lock to check limit, and allows type assertions to bypass Read.
+ return r
+ }
+ return rateLimitedReader{
+ l: l,
+ r: r,
+ }
+}
+
type rateLimitedReader struct {
l *rate.Limiter
r io.Reader
for _, opt := range opts {
opt(&ws.client)
}
- setRateLimiterBurstIfZero(ws.client.ResponseBodyRateLimiter, defaultDownloadRateLimiterBurst)
+ setDefaultDownloadRateLimiterBurstIfZero(ws.client.ResponseBodyRateLimiter)
ws.client.ResponseBodyWrapper = func(r io.Reader) io.Reader {
- return &rateLimitedReader{
- l: ws.client.ResponseBodyRateLimiter,
- r: r,
- }
+ return newRateLimitedReader(r, ws.client.ResponseBodyRateLimiter)
}
g.MakeMapWithCap(&ws.activeRequests, ws.client.MaxRequests)
ws.locker = t.cl.locker()