feat: add unified worker pool for recursive downloads
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"codeberg.org/petrbalvin/goget/internal/core"
|
||||
"codeberg.org/petrbalvin/goget/internal/output"
|
||||
"codeberg.org/petrbalvin/goget/internal/pool"
|
||||
"codeberg.org/petrbalvin/goget/internal/protocol"
|
||||
"codeberg.org/petrbalvin/goget/internal/transport"
|
||||
)
|
||||
@@ -982,9 +983,18 @@ func (p *Protocol) downloadRecursiveSequential(ctx context.Context, req *core.Do
|
||||
}, nil
|
||||
}
|
||||
|
||||
// webdavTask represents a unit of work for the concurrent recursive
|
||||
// download: a file or subdirectory entry with its pre-built request.
|
||||
type webdavTask struct {
|
||||
entry WebDAVEntry
|
||||
subReq *core.DownloadRequest
|
||||
outPath string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// downloadRecursiveParallel is the worker-pool implementation used when
|
||||
// req.RecursiveParallel > 1. Top-level entries are processed concurrently,
|
||||
// bounded by a semaphore. Subdirectory recursion is delegated back to
|
||||
// bounded by the pool. Subdirectory recursion is delegated back to
|
||||
// downloadRecursive, so the same RecursiveParallel limit applies at every
|
||||
// level of the tree. Stats are aggregated under a mutex.
|
||||
func (p *Protocol) downloadRecursiveParallel(ctx context.Context, req *core.DownloadRequest) (*core.DownloadResult, error) {
|
||||
@@ -1021,24 +1031,52 @@ func (p *Protocol) downloadRecursiveParallel(ctx context.Context, req *core.Down
|
||||
// these counters, so they reflect the entire subtree processed by
|
||||
// this call.
|
||||
var (
|
||||
statsMu sync.Mutex
|
||||
totalBytes int64
|
||||
totalChunks int
|
||||
processedDirs int
|
||||
statsMu sync.Mutex
|
||||
totalBytes int64
|
||||
totalChunks int
|
||||
)
|
||||
|
||||
processedDirs++
|
||||
processedDirs := 1
|
||||
|
||||
// Pre-build sub-requests and output paths so the goroutine body has
|
||||
// no per-entry allocation cost.
|
||||
type task struct {
|
||||
entry WebDAVEntry
|
||||
subReq *core.DownloadRequest
|
||||
outPath string
|
||||
isDir bool
|
||||
}
|
||||
wp := pool.New(ctx, req.RecursiveParallel, func(taskCtx context.Context, t webdavTask) {
|
||||
if taskCtx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if t.isDir {
|
||||
subResult, err := p.downloadRecursive(taskCtx, t.subReq)
|
||||
if err != nil {
|
||||
if req.Verbose {
|
||||
fmt.Fprintf(os.Stderr, "[webdav] Warning: failed to %s sub-directory %s: %v\n", dryRunVerb(req.DryRun), t.entry.URL.String(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
statsMu.Lock()
|
||||
totalBytes += subResult.BytesDownloaded
|
||||
totalChunks += subResult.ChunksCount
|
||||
statsMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
subResult, err := p.downloadFile(taskCtx, t.subReq, t.entry.Size, t.entry.LastModified)
|
||||
if err != nil {
|
||||
if req.Verbose {
|
||||
fmt.Fprintf(os.Stderr, "[webdav] Warning: failed to download file %s: %v\n", t.entry.URL.String(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Apply keep-timestamps if requested.
|
||||
if req.KeepTimestamps {
|
||||
setFileTimestamp(t.outPath, t.entry.LastModified)
|
||||
}
|
||||
|
||||
statsMu.Lock()
|
||||
totalBytes += subResult.BytesDownloaded
|
||||
totalChunks++
|
||||
statsMu.Unlock()
|
||||
})
|
||||
|
||||
tasks := make([]task, 0, len(filteredEntries))
|
||||
for _, entry := range filteredEntries {
|
||||
// Respect MaxDepth to avoid infinite recursion.
|
||||
if entry.IsDir && req.MaxDepth == 0 {
|
||||
@@ -1065,7 +1103,7 @@ func (p *Protocol) downloadRecursiveParallel(ctx context.Context, req *core.Down
|
||||
subReq.MaxDepth = req.MaxDepth - 1
|
||||
subReq.RecursiveParallel = req.RecursiveParallel
|
||||
subReq.DryRun = true
|
||||
tasks = append(tasks, task{entry: entry, subReq: &subReq, outPath: itemOutputPath, isDir: true})
|
||||
wp.Submit(webdavTask{entry: entry, subReq: &subReq, outPath: itemOutputPath, isDir: true})
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[webdav] [dry-run] would download: %s (%d bytes)\n", entry.URL.String(), entry.Size)
|
||||
statsMu.Lock()
|
||||
@@ -1106,7 +1144,7 @@ func (p *Protocol) downloadRecursiveParallel(ctx context.Context, req *core.Down
|
||||
RejectPatterns: req.RejectPatterns,
|
||||
Ctx: req.Ctx,
|
||||
}
|
||||
tasks = append(tasks, task{entry: entry, subReq: subReq, outPath: itemOutputPath, isDir: true})
|
||||
wp.Submit(webdavTask{entry: entry, subReq: subReq, outPath: itemOutputPath, isDir: true})
|
||||
} else {
|
||||
if req.Verbose {
|
||||
fmt.Fprintf(os.Stderr, "[webdav] Downloading file %s -> %s (depth=%d)\n", entry.URL.String(), itemOutputPath, req.MaxDepth)
|
||||
@@ -1126,71 +1164,11 @@ func (p *Protocol) downloadRecursiveParallel(ctx context.Context, req *core.Down
|
||||
MaxDepth: req.MaxDepth - 1,
|
||||
Ctx: req.Ctx,
|
||||
}
|
||||
tasks = append(tasks, task{entry: entry, subReq: subReq, outPath: itemOutputPath, isDir: false})
|
||||
wp.Submit(webdavTask{entry: entry, subReq: subReq, outPath: itemOutputPath, isDir: false})
|
||||
}
|
||||
}
|
||||
|
||||
// Worker pool: bounded by req.RecursiveParallel. We use a semaphore
|
||||
// channel + WaitGroup so total in-flight workers never exceed the
|
||||
// requested count, regardless of how many tasks are enqueued.
|
||||
sem := make(chan struct{}, req.RecursiveParallel)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, t := range tasks {
|
||||
// Honour context cancellation before launching a new task.
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
|
||||
go func(t task) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
|
||||
// If the context was cancelled while we were waiting for
|
||||
// the semaphore, just exit cleanly.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if t.isDir {
|
||||
subResult, err := p.downloadRecursive(ctx, t.subReq)
|
||||
if err != nil {
|
||||
if req.Verbose {
|
||||
fmt.Fprintf(os.Stderr, "[webdav] Warning: failed to %s sub-directory %s: %v\n", dryRunVerb(req.DryRun), t.entry.URL.String(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
statsMu.Lock()
|
||||
totalBytes += subResult.BytesDownloaded
|
||||
totalChunks += subResult.ChunksCount
|
||||
statsMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
subResult, err := p.downloadFile(ctx, t.subReq, t.entry.Size, t.entry.LastModified)
|
||||
if err != nil {
|
||||
if req.Verbose {
|
||||
fmt.Fprintf(os.Stderr, "[webdav] Warning: failed to download file %s: %v\n", t.entry.URL.String(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Apply keep-timestamps if requested.
|
||||
if req.KeepTimestamps {
|
||||
setFileTimestamp(t.outPath, t.entry.LastModified)
|
||||
}
|
||||
|
||||
statsMu.Lock()
|
||||
totalBytes += subResult.BytesDownloaded
|
||||
totalChunks++
|
||||
statsMu.Unlock()
|
||||
}(t)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
wp.Wait()
|
||||
|
||||
_ = processedDirs // reserved for future per-directory accounting
|
||||
|
||||
|
||||
Reference in New Issue
Block a user