@ -60,6 +60,10 @@ type (
GetTaskStatusTried int ` json:"get_task_status_tried,omitempty" `
GetTaskStatusTried int ` json:"get_task_status_tried,omitempty" `
Transferred map [ int ] interface { } ` json:"transferred,omitempty" `
Transferred map [ int ] interface { } ` json:"transferred,omitempty" `
Failed int ` json:"failed,omitempty" `
Failed int ` json:"failed,omitempty" `
// ResumeMonitorAfterTransfer marks an early transfer batch taken while
// the download is still running: after it completes, return to Monitor
// instead of awaiting seeding.
ResumeMonitorAfterTransfer bool ` json:"resume_monitor_after_transfer,omitempty" `
}
}
)
)
@ -389,6 +393,13 @@ func (m *RemoteDownloadTask) monitor(ctx context.Context, dep dependency.Dep) (t
m . l . Info ( "Download task seeding completed" )
m . l . Info ( "Download task seeding completed" )
return task . StatusCompleted , nil
return task . StatusCompleted , nil
case downloader . StatusDownloading :
case downloader . StatusDownloading :
if m . hasEarlyTransferCandidates ( status ) {
m . l . Info ( "Some files already completed, starting early transfer." )
m . state . Phase = RemoteDownloadTaskPhaseTransfer
m . state . ResumeMonitorAfterTransfer = true
m . ResumeAfter ( 0 )
return task . StatusSuspending , nil
}
m . ResumeAfter ( resumeAfter )
m . ResumeAfter ( resumeAfter )
return task . StatusSuspending , nil
return task . StatusSuspending , nil
case downloader . StatusUnknown , downloader . StatusError :
case downloader . StatusUnknown , downloader . StatusError :
@ -399,6 +410,19 @@ func (m *RemoteDownloadTask) monitor(ctx context.Context, dep dependency.Dep) (t
return task . StatusSuspending , nil
return task . StatusSuspending , nil
}
}
// hasEarlyTransferCandidates reports whether any selected file finished
// downloading but has not been transferred to the user's storage yet.
func ( m * RemoteDownloadTask ) hasEarlyTransferCandidates ( status * downloader . TaskStatus ) bool {
for _ , f := range status . Files {
if f . Selected && f . Progress >= 1 {
if _ , ok := m . state . Transferred [ f . Index ] ; ! ok {
return true
}
}
}
return false
}
func ( m * RemoteDownloadTask ) slaveTransfer ( ctx context . Context , dep dependency . Dep ) ( task . Status , error ) {
func ( m * RemoteDownloadTask ) slaveTransfer ( ctx context . Context , dep dependency . Dep ) ( task . Status , error ) {
u := inventory . UserFromContext ( ctx )
u := inventory . UserFromContext ( ctx )
if m . state . Transferred == nil {
if m . state . Transferred == nil {
@ -429,6 +453,11 @@ func (m *RemoteDownloadTask) slaveTransfer(ctx context.Context, dep dependency.D
continue
continue
}
}
// During early transfer batches only completed files are picked.
if m . state . ResumeMonitorAfterTransfer && f . Progress < 1 {
continue
}
dst := dstUri . JoinRaw ( sanitizeFileName ( f . Name ) )
dst := dstUri . JoinRaw ( sanitizeFileName ( f . Name ) )
src := path . Join ( m . state . Status . SavePath , f . Name )
src := path . Join ( m . state . Status . SavePath , f . Name )
payload . Files = append ( payload . Files , SlaveUploadEntity {
payload . Files = append ( payload . Files , SlaveUploadEntity {
@ -480,12 +509,30 @@ func (m *RemoteDownloadTask) slaveTransfer(ctx context.Context, dep dependency.D
}
}
m . l . Warning ( "Slave task %d failed to transfer %d files, retrying..." , slaveTaskId , len ( m . state . SlaveUploadState . Files ) - len ( m . state . SlaveUploadState . Transferred ) )
m . l . Warning ( "Slave task %d failed to transfer %d files, retrying..." , slaveTaskId , len ( m . state . SlaveUploadState . Files ) - len ( m . state . SlaveUploadState . Transferred ) )
if m . state . ResumeMonitorAfterTransfer {
// Early transfer batch failed while the download continues -
// return to monitoring; the final transfer pass retries them.
m . state . ResumeMonitorAfterTransfer = false
m . state . Phase = RemoteDownloadTaskPhaseMonitor
m . ResumeAfter ( 0 )
return task . StatusSuspending , nil
}
return task . StatusError , fmt . Errorf (
return task . StatusError , fmt . Errorf (
"slave task failed to transfer %d files, first 5 errors: %s" ,
"slave task failed to transfer %d files, first 5 errors: %s" ,
len ( m . state . SlaveUploadState . Files ) - len ( m . state . SlaveUploadState . Transferred ) ,
len ( m . state . SlaveUploadState . Files ) - len ( m . state . SlaveUploadState . Transferred ) ,
m . state . SlaveUploadState . First5TransferErrors ,
m . state . SlaveUploadState . First5TransferErrors ,
)
)
} else {
} else {
if m . state . ResumeMonitorAfterTransfer {
for i := range m . state . SlaveUploadState . Transferred {
m . state . Transferred [ m . state . SlaveUploadState . Files [ i ] . Index ] = struct { } { }
}
m . state . SlaveUploadTaskID = 0
m . state . ResumeMonitorAfterTransfer = false
m . state . Phase = RemoteDownloadTaskPhaseMonitor
m . ResumeAfter ( 0 )
return task . StatusSuspending , nil
}
m . state . Phase = RemoteDownloadTaskPhaseAwaitSeeding
m . state . Phase = RemoteDownloadTaskPhaseAwaitSeeding
m . ResumeAfter ( 0 )
m . ResumeAfter ( 0 )
return task . StatusSuspending , nil
return task . StatusSuspending , nil
@ -519,6 +566,11 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency.
allFiles := make ( [ ] downloader . TaskFile , 0 , len ( m . state . Status . Files ) )
allFiles := make ( [ ] downloader . TaskFile , 0 , len ( m . state . Status . Files ) )
for _ , f := range m . state . Status . Files {
for _ , f := range m . state . Status . Files {
if f . Selected {
if f . Selected {
// Early transfer batches only pick completed files; files still
// downloading are left for the final transfer pass.
if m . state . ResumeMonitorAfterTransfer && f . Progress < 1 {
continue
}
allFiles = append ( allFiles , f )
allFiles = append ( allFiles , f )
totalSize += f . Size
totalSize += f . Size
totalCount ++
totalCount ++
@ -622,12 +674,27 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency.
wg . Wait ( )
wg . Wait ( )
if failed > 0 {
if failed > 0 {
if m . state . ResumeMonitorAfterTransfer {
// Early batch failed while the download continues - return to
// monitoring; the final transfer pass retries the failed files.
m . l . Warning ( "Early transfer batch failed for %d file(s), will retry after download completes." , failed )
m . state . ResumeMonitorAfterTransfer = false
m . state . Phase = RemoteDownloadTaskPhaseMonitor
m . ResumeAfter ( 0 )
return task . StatusSuspending , nil
}
m . state . Failed = int ( failed )
m . state . Failed = int ( failed )
m . l . Error ( "Failed to transfer %d file(s)." , failed )
m . l . Error ( "Failed to transfer %d file(s)." , failed )
return task . StatusError , fmt . Errorf ( "failed to transfer %d file(s), first 5 errors: %s" , failed , ae . FormatFirstN ( 5 ) )
return task . StatusError , fmt . Errorf ( "failed to transfer %d file(s), first 5 errors: %s" , failed , ae . FormatFirstN ( 5 ) )
}
}
m . l . Info ( "All files transferred." )
m . l . Info ( "All files transferred." )
if m . state . ResumeMonitorAfterTransfer {
m . state . ResumeMonitorAfterTransfer = false
m . state . Phase = RemoteDownloadTaskPhaseMonitor
m . ResumeAfter ( 0 )
return task . StatusSuspending , nil
}
m . state . Phase = RemoteDownloadTaskPhaseAwaitSeeding
m . state . Phase = RemoteDownloadTaskPhaseAwaitSeeding
return task . StatusSuspending , nil
return task . StatusSuspending , nil
}
}