httpdl/cli: drop --auto-split; classify errors by type, not message text

Two Pike cleanups.

--auto-split was a third connection-count policy (beside -x and -s) that added
an extra knob rather than keeping the model minimal. Removing it also retires the
now-dead autoConns/maxAutoConns and the per-host transport cap that existed only
to scale for it; min(split, M*-x) is the sole policy again.

main's exit-code mapping fell back to substring-matching third-party error text
(strings.Contains "connection refused"/"timeout"/...), which rots when a
dependency rewords a message. The typed checks (errors.Is on the syscall errno,
*net.DNSError, net.Error.Timeout, context.DeadlineExceeded) already cover the
real stdlib errors -- verified end to end: refused -> 6, bad host -> 19. The two
cases that genuinely needed the text match are our own errors, now typed
sentinels: httpdl.ErrTimeout (idle timeout) and the bt metadata timeout wrapping
context.DeadlineExceeded.
This commit is contained in:
2026-06-20 23:55:39 +09:00
parent ebc630b231
commit 508708b039
8 changed files with 27 additions and 108 deletions

View File

@@ -57,8 +57,7 @@ type Config struct {
Split int // --split: total connections across all mirrors
MaxConnPerServer int // --max-connection-per-server: per-host connection cap
MinSplit int64
AutoSplit bool // --auto-split (got-only): pick the connection count from file size
Tries int // 0 = unlimited
Tries int // 0 = unlimited
Timeout time.Duration
FileAlloc string // none | prealloc | trunc | falloc
Continue bool
@@ -120,7 +119,7 @@ type Config struct {
// connections.
type Download struct {
uris []string // mirror list; uris[0] is the primary (naming + resume key)
maxConns int // manual segment-worker cap = min(split, len(uris)*max-connection-per-server); --auto-split derives its own count per file in segmented()
maxConns int // segment-worker cap = min(split, len(uris)*max-connection-per-server)
cfg Config
client *http.Client
limit []*rate.Limiter
@@ -216,17 +215,10 @@ func New(uris []string, cfg Config) *Download {
if perHost < 1 {
perHost = 1
}
// The transport must allow as many concurrent connections per host as we may
// actually open. --auto-split scales up to maxAutoConns per host once the file
// size is known (in segmented), so raise the cap to that ceiling when it is on.
connCap := perHost
if cfg.AutoSplit {
connCap = maxAutoConns
}
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxConnsPerHost: connCap,
MaxIdleConnsPerHost: connCap,
MaxConnsPerHost: perHost,
MaxIdleConnsPerHost: perHost,
ResponseHeaderTimeout: cfg.Timeout,
TLSHandshakeTimeout: connectTimeout,
DialContext: dialContext,
@@ -671,13 +663,7 @@ func (d *Download) probe(ctx context.Context, uri string) (probeResult, error) {
// segmented downloads total bytes over d.maxConns workers, with per-segment
// resume from the control file.
func (d *Download) segmented(ctx context.Context, out string, total int64, etag, lastmod string) error {
// --auto-split picks the connection count from the file size now that total is
// known; otherwise use the manual cap fixed at construction.
conns := d.maxConns
if d.cfg.AutoSplit {
conns = autoConns(total, d.cfg.MinSplit)
}
segs := makeSegments(total, d.cfg.MinSplit, conns)
segs := makeSegments(total, d.cfg.MinSplit, d.maxConns)
if c := d.resumeControl(out, etag, lastmod); c != nil {
segs = segsFromControl(c)
} else if d.cfg.Continue && !fileExists(controlPath(out)) {
@@ -809,10 +795,11 @@ func (d *Download) fetchOnce(ctx context.Context, f *os.File, s *seg, uri string
return d.pump(ctx, f, s, body)
}
// errIdleTimeout marks a transfer that stalled past the idle window, so callers
// can report a distinct "timed out" instead of the bare "context canceled" that
// the cancelled request would otherwise surface (item [39]).
var errIdleTimeout = errors.New("idle timeout: no data received")
// ErrTimeout marks a transfer that stalled past the idle window (--timeout): a
// distinct "timed out" instead of the bare "context canceled" the cancelled
// request would otherwise surface (item [39]). main maps it to exit code 2 via
// errors.Is, so it must stay on the error chain (CONTRACT).
var ErrTimeout = errors.New("idle timeout: no data received")
// idleReader records the time of the last byte received; a background watcher
// (idleGuard) cancels the request once a full idle window passes with no
@@ -834,7 +821,7 @@ func (ir *idleReader) Read(p []byte) (int, error) {
if err != nil && ir.timedOut.Load() {
// The cancellation came from our watcher, not the caller's ctx, so
// translate the read error into the distinct idle-timeout sentinel.
return n, fmt.Errorf("%w (after %s)", errIdleTimeout, ir.idle)
return n, fmt.Errorf("%w (after %s)", ErrTimeout, ir.idle)
}
return n, err
}

View File

@@ -55,27 +55,3 @@ func makeSegments(total, minSplit int64, conns int) []seg {
}
return segs
}
// maxAutoConns is the ceiling --auto-split scales to. aria2 caps
// max-connection-per-server (-x) at 16, so a got-chosen count honours the same
// per-server limit rather than inventing a looser one.
const maxAutoConns = 16
// autoConns picks a connection count from the file size, used only when
// --auto-split is set: one connection per min-split-size of content, at least 1
// and at most maxAutoConns. It is the got-only stand-in for hand-tuning -x/-s,
// and because the count never exceeds total/minSplit, makeSegments yields exactly
// that many balanced segments.
func autoConns(total, minSplit int64) int {
if minSplit < 1 {
minSplit = 1
}
n := total / minSplit
if n < 1 {
n = 1
}
if n > maxAutoConns {
n = maxAutoConns
}
return int(n)
}

View File

@@ -47,31 +47,6 @@ func TestMakeSegments(t *testing.T) {
}
}
func TestAutoConns(t *testing.T) {
const m = int64(20 << 20) // 20 MiB min-split, aria2's default
tests := []struct {
name string
total int64
minSplit int64
want int
}{
{"under one min-split is a single connection", 5 << 20, m, 1},
{"exactly one min-split", m, m, 1},
{"five min-splits", 100 << 20, m, 5},
{"caps at maxAutoConns", 10 << 30, m, maxAutoConns},
{"rounds down to whole pieces", m*3 + 1, m, 3},
{"tiny min-split still capped", 1 << 30, 1 << 20, maxAutoConns},
{"zero total is one connection", 0, m, 1},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := autoConns(tc.total, tc.minSplit); got != tc.want {
t.Errorf("autoConns(%d, %d) = %d, want %d", tc.total, tc.minSplit, got, tc.want)
}
})
}
}
func TestSegProgress(t *testing.T) {
s := seg{start: 100, end: 199} // length 100
if s.length() != 100 {