httpdl/cli: drop --auto-split; classify errors by type, not message text
Two Pike cleanups. --auto-split was a got-only third connection-count policy (beside -x and -s) that invented rather than matched aria2. 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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user