first commit
This commit is contained in:
69
download/download.go
Normal file
69
download/download.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Package download defines the core abstraction of the program: a Download is
|
||||
// a thing you can Run and ask for a Stat snapshot. Where a single-threaded
|
||||
// Command/event-poll reactor would express this, we use one
|
||||
// goroutine per Download blocking on real I/O. Following Rob Pike, the data
|
||||
// (Stat) and the interface are kept small; the algorithms fall out of them.
|
||||
package download
|
||||
|
||||
import "context"
|
||||
|
||||
// Status is the coarse lifecycle state of a download, using the
|
||||
// active/waiting/complete/error vocabulary.
|
||||
type Status int
|
||||
|
||||
const (
|
||||
Waiting Status = iota
|
||||
Active
|
||||
Seeding
|
||||
Complete
|
||||
Errored
|
||||
)
|
||||
|
||||
func (s Status) String() string {
|
||||
switch s {
|
||||
case Waiting:
|
||||
return "waiting"
|
||||
case Active:
|
||||
return "active"
|
||||
case Seeding:
|
||||
return "seeding"
|
||||
case Complete:
|
||||
return "complete"
|
||||
case Errored:
|
||||
return "error"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Stat is a snapshot of a download's progress. Every field is a plain value so
|
||||
// a renderer running in another goroutine can copy it without sharing memory.
|
||||
// Completed and Uploaded are cumulative byte counters; the renderer derives
|
||||
// speeds from successive snapshots rather than each download tracking its own.
|
||||
type Stat struct {
|
||||
Name string
|
||||
// ID is a process-unique stable identity for this download. Name can
|
||||
// collide (two pre-metadata magnets both show their source string), so the
|
||||
// renderer keys its per-download speed samples on ID, not Name.
|
||||
ID string
|
||||
IsBT bool // true for BitTorrent; progress always shows CN and shows SD for torrents
|
||||
Status Status
|
||||
Total int64 // total bytes, or -1 if not yet known
|
||||
Completed int64 // bytes downloaded so far
|
||||
Uploaded int64 // bytes uploaded (BitTorrent), 0 otherwise
|
||||
Conns int // active connections / peers
|
||||
Seeders int // connected seeders (BitTorrent only)
|
||||
}
|
||||
|
||||
// Done reports whether the download has reached a terminal state.
|
||||
func (s Stat) Done() bool { return s.Status == Complete || s.Status == Errored }
|
||||
|
||||
// A Download is one logical job: a URL, a torrent, a magnet. Run blocks in its
|
||||
// own goroutine until the work finishes, fails, or ctx is cancelled. Stat may
|
||||
// be called concurrently at any time and must not block; implementations back
|
||||
// it with atomics or a briefly-held lock.
|
||||
type Download interface {
|
||||
Name() string
|
||||
Run(ctx context.Context) error
|
||||
Stat() Stat
|
||||
}
|
||||
103
download/engine.go
Normal file
103
download/engine.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Result pairs a finished download with its outcome and a final stat snapshot.
|
||||
type Result struct {
|
||||
Name string
|
||||
Index int // position in the slice passed to Run, so callers can restore input order
|
||||
Err error
|
||||
Stat Stat
|
||||
}
|
||||
|
||||
// Engine runs a set of downloads, at most maxConcurrent at a time, and exposes
|
||||
// a live snapshot of the running ones for the progress renderer. A buffered
|
||||
// channel of slots enforces the concurrency limit, and one goroutine per
|
||||
// download replaces an explicit scheduler tick.
|
||||
type Engine struct {
|
||||
maxConcurrent int
|
||||
|
||||
mu sync.Mutex
|
||||
active []Download
|
||||
}
|
||||
|
||||
// NewEngine returns an engine that runs at most maxConcurrent downloads at once.
|
||||
func NewEngine(maxConcurrent int) *Engine {
|
||||
if maxConcurrent < 1 {
|
||||
maxConcurrent = 1
|
||||
}
|
||||
return &Engine{maxConcurrent: maxConcurrent}
|
||||
}
|
||||
|
||||
// Run starts every download, honouring the concurrency limit, and returns once
|
||||
// all of them have finished or ctx is cancelled. There is one Result per
|
||||
// download; results arrive in completion order, but each Result carries its
|
||||
// input Index so callers can sort back to the original slice order.
|
||||
func (e *Engine) Run(ctx context.Context, downloads []Download) []Result {
|
||||
slots := make(chan struct{}, e.maxConcurrent)
|
||||
results := make(chan Result, len(downloads))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i, d := range downloads {
|
||||
wg.Add(1)
|
||||
go func(i int, d Download) {
|
||||
defer wg.Done()
|
||||
|
||||
// Acquire a concurrency slot, or bail if we're shutting down
|
||||
// before this download ever started.
|
||||
select {
|
||||
case slots <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
results <- Result{Name: d.Name(), Index: i, Err: ctx.Err(), Stat: d.Stat()}
|
||||
return
|
||||
}
|
||||
defer func() { <-slots }()
|
||||
|
||||
e.track(d)
|
||||
defer e.untrack(d)
|
||||
|
||||
err := d.Run(ctx)
|
||||
results <- Result{Name: d.Name(), Index: i, Err: err, Stat: d.Stat()}
|
||||
}(i, d)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
out := make([]Result, 0, len(downloads))
|
||||
for r := range results {
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Snapshot returns a Stat for every currently running download.
|
||||
func (e *Engine) Snapshot() []Stat {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
out := make([]Stat, 0, len(e.active))
|
||||
for _, d := range e.active {
|
||||
out = append(out, d.Stat())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *Engine) track(d Download) {
|
||||
e.mu.Lock()
|
||||
e.active = append(e.active, d)
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
func (e *Engine) untrack(d Download) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
for i, x := range e.active {
|
||||
if x == d {
|
||||
e.active = append(e.active[:i], e.active[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
18
download/limit.go
Normal file
18
download/limit.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package download
|
||||
|
||||
// LimiterBurst returns the token-bucket burst size for a bytes/sec rate limit,
|
||||
// as used with golang.org/x/time/rate. The burst is the rate itself, but
|
||||
// floored at 256 KiB: with burst==rate a tiny limit (say a few KiB/s) would
|
||||
// hand out only a few bytes per refill, so a single read large enough to fill a
|
||||
// network buffer could never proceed and throughput would stall well below the
|
||||
// limit. The floor guarantees every limit still permits one usefully sized read
|
||||
// while the long-run average stays bounded by the rate.
|
||||
//
|
||||
// This is the single source of truth for the burst calculation; httpdl, bt, and
|
||||
// main call it rather than each keeping a private copy.
|
||||
func LimiterBurst(bps int64) int {
|
||||
if bps < 256*1024 {
|
||||
return 256 * 1024
|
||||
}
|
||||
return int(bps)
|
||||
}
|
||||
Reference in New Issue
Block a user