fix(http): re-probe range support after sequential retry (#1464)

This commit is contained in:
ggg authored and GitHub committed 2026-09-02 14:29:48 +08:00
1 parent c5c8bb2bf5
commit f49e2510f5
5 files changed
+1245 -41

No files matched your search

+84 -4
View File
@@ -14,8 +14,15 @@ import (
// ============================================================================
type fetcherData struct {
Connections []*connection
RedirectURL string // Saved redirect URL for resume
Connections []*connection
RedirectURL string // Saved redirect URL for resume
IfRange string // Strong ETag or Last-Modified validator for safe resume
RangeReprobeEligible bool // Origin advertised Range before falling back to sequential mode
RangeValidatorPinned bool // Recovered Range mode must keep the same If-Range validator
SequentialSizeUnknown bool // Interrupted chunked restart must retry from byte zero
Range *bool // Authoritative Range mode; nil for records saved by older versions
ResourceSize *int64 // Authoritative resource size from the same snapshot as Connections
FileSize *int64 // Authoritative single-file size; nil for older records
}
// ============================================================================
@@ -75,12 +82,69 @@ func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
_f.redirectLock.Lock()
redirectURL := _f.redirectURL
_f.redirectLock.Unlock()
// Build an immutable snapshot while holding the same lock used by download
// workers to update Range mode and connection progress. The storage layer
// marshals this value after Store returns, so returning live pointers would
// otherwise race with the active download and could persist a mixed state.
_f.connMu.Lock()
connections := snapshotConnectionsLocked(_f.connections)
ifRange := _f.ifRange
rangeReprobeEligible := _f.rangeReprobeEligible
rangeValidatorPinned := _f.rangeValidatorPinned
sequentialSizeUnknown := _f.sequentialSizeUnknown
var rangeMode *bool
var resourceSize *int64
var fileSize *int64
if _f.meta != nil && _f.meta.Res != nil {
value := _f.meta.Res.Range
rangeMode = &value
size := _f.meta.Res.Size
resourceSize = &size
if len(_f.meta.Res.Files) > 0 && _f.meta.Res.Files[0] != nil {
size := _f.meta.Res.Files[0].Size
fileSize = &size
}
}
_f.connMu.Unlock()
return &fetcherData{
Connections: _f.connections,
RedirectURL: redirectURL,
Connections: connections,
RedirectURL: redirectURL,
IfRange: ifRange,
RangeReprobeEligible: rangeReprobeEligible,
RangeValidatorPinned: rangeValidatorPinned,
SequentialSizeUnknown: sequentialSizeUnknown,
Range: rangeMode,
ResourceSize: resourceSize,
FileSize: fileSize,
}, nil
}
// snapshotConnectionsLocked copies only the serialized connection state and
// deep-copies chunks. The caller must hold the fetcher's connMu.
func snapshotConnectionsLocked(connections []*connection) []*connection {
snapshot := make([]*connection, len(connections))
for i, conn := range connections {
if conn == nil {
continue
}
copyConn := &connection{
ID: conn.ID,
Role: conn.Role,
State: conn.State,
Downloaded: conn.Downloaded,
Completed: conn.Completed,
}
if conn.Chunk != nil {
copyChunk := *conn.Chunk
copyConn.Chunk = &copyChunk
}
snapshot[i] = copyConn
}
return snapshot
}
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
fd := v.(*fetcherData)
@@ -96,6 +160,22 @@ func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
if fd.RedirectURL != "" {
fetcher.redirectURL = fd.RedirectURL
}
fetcher.ifRange = fd.IfRange
fetcher.rangeReprobeEligible = fd.RangeReprobeEligible
fetcher.rangeValidatorPinned = fd.RangeValidatorPinned
fetcher.sequentialSizeUnknown = fd.SequentialSizeUnknown
if fd.Range != nil && fetcher.meta.Res != nil {
// Recovery-critical resource fields live in the same persisted snapshot
// as Connections, making them authoritative over task metadata that may
// have been saved just before or after this record.
fetcher.meta.Res.Range = *fd.Range
}
if fd.ResourceSize != nil && fetcher.meta.Res != nil {
fetcher.meta.Res.Size = *fd.ResourceSize
}
if fd.FileSize != nil && fetcher.meta.Res != nil && len(fetcher.meta.Res.Files) > 0 && fetcher.meta.Res.Files[0] != nil {
fetcher.meta.Res.Files[0].Size = *fd.FileSize
}
return fetcher
}
}