test: enforce async extension API contract (#1452)

This commit is contained in:
Levi authored and GitHub committed 2026-08-27 12:51:37 +08:00
1 parent cfdf87c3b8
commit 59f3aa7028
13 files changed
+121 -77

No files matched your search

+37 -14
View File
@@ -612,14 +612,18 @@ func (f *Fetcher) doStart() error {
return nil
}
// If retrying after error, reset connection states for retry
if state == stateError {
// Drain any pending error from doneCh before retry
// Each Start/Wait cycle must observe only its own completion result.
// A paused or failed cycle may have completed concurrently with Pause and
// left a result buffered before its download loop fully stopped.
if state == statePaused || state == stateError {
select {
case <-f.doneCh:
default:
}
}
// If retrying after error, reset connection states for retry
if state == stateError {
f.connMu.Lock()
for _, conn := range f.connections {
// Reset connections that can be retried
@@ -695,6 +699,8 @@ func (f *Fetcher) doStart() error {
}
func (f *Fetcher) downloadLoop() {
ctx := f.ctx
defer func() {
// Update file last modified time before closing
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
@@ -713,24 +719,30 @@ func (f *Fetcher) downloadLoop() {
if !isResume {
// Fresh start: begin with resolve connection
f.startResolveDownload()
// Non-range downloads wait for their only connection in
// startResolveDownload. Falling through would consume the connection's
// expansion signal and publish the same completion a second time.
if !f.meta.Res.Range || f.meta.Res.Size == 0 || f.getState() == stateDone {
return
}
} else {
// Resume: restart existing connections
f.resumeConnections()
f.waitForCompletion()
f.waitForCompletion(ctx)
return
}
// Slow start loop
for {
select {
case <-f.ctx.Done():
case <-ctx.Done():
// Paused or cancelled
return
case <-f.slowStart.expansionCh:
// Batch completed, try to expand
if f.checkCompletion() {
// All work is done, wait for connections to finish
f.waitForCompletion()
f.waitForCompletion(ctx)
return
}
f.expandConnections()
@@ -738,7 +750,7 @@ func (f *Fetcher) downloadLoop() {
// Check if we've reached steady state (max connections)
if f.getState() == stateSteady {
// Wait for all connections to complete
f.waitForCompletion()
f.waitForCompletion(ctx)
return
}
}
@@ -764,7 +776,7 @@ func (f *Fetcher) startResolveDownload() {
// For non-range downloads, wait for completion directly in this goroutine
// Don't create another goroutine to avoid WaitGroup reuse issues
f.waitForCompletion()
f.waitForCompletion(f.ctx)
return
}
@@ -881,7 +893,6 @@ func (f *Fetcher) expandConnections() {
if len(newConns) == 0 {
// No new connections could be created, stop expansion
f.setState(stateSteady)
go f.waitForCompletion()
return
}
@@ -1181,9 +1192,13 @@ func (f *Fetcher) runConnectionWithResolveResp(conn *connection) {
n, err := reader.Read(buf)
if n > 0 {
f.connMu.Lock()
writeOffset := conn.Chunk.Downloaded
f.connMu.Unlock()
f.fileMu.Lock()
if f.file != nil {
_, writeErr := f.file.WriteAt(buf[:n], conn.Chunk.Downloaded)
_, writeErr := f.file.WriteAt(buf[:n], writeOffset)
if writeErr != nil {
f.fileMu.Unlock()
f.connMu.Lock()
@@ -1198,8 +1213,10 @@ func (f *Fetcher) runConnectionWithResolveResp(conn *connection) {
}
f.fileMu.Unlock()
f.connMu.Lock()
conn.Chunk.Downloaded += int64(n)
conn.Downloaded += int64(n)
f.connMu.Unlock()
}
if err != nil {
if err == io.EOF {
@@ -1283,9 +1300,13 @@ func (f *Fetcher) runConnectionFallback(conn *connection) {
n, err := reader.Read(buf)
if n > 0 {
f.connMu.Lock()
writeOffset := conn.Chunk.Downloaded
f.connMu.Unlock()
f.fileMu.Lock()
if f.file != nil {
_, writeErr := f.file.WriteAt(buf[:n], conn.Chunk.Downloaded)
_, writeErr := f.file.WriteAt(buf[:n], writeOffset)
if writeErr != nil {
f.fileMu.Unlock()
return writeErr
@@ -1293,8 +1314,10 @@ func (f *Fetcher) runConnectionFallback(conn *connection) {
}
f.fileMu.Unlock()
f.connMu.Lock()
conn.Chunk.Downloaded += int64(n)
conn.Downloaded += int64(n)
f.connMu.Unlock()
}
if err != nil {
if err == io.EOF {
@@ -1480,10 +1503,10 @@ func (f *Fetcher) resumeConnections() {
}
}
func (f *Fetcher) waitForCompletion() {
func (f *Fetcher) waitForCompletion(ctx context.Context) {
f.wg.Wait()
// Only trigger completion if not cancelled/paused
if f.ctx != nil && f.ctx.Err() == nil {
// Only trigger completion for the Start cycle that launched this waiter.
if ctx != nil && ctx.Err() == nil {
f.onDownloadComplete()
}
}