feat(agent): auto-resume interrupted downloads after a daemon restart
A daemon restart used to abandon in-flight downloads: the in-memory queue was lost and the web doesn't re-dispatch a stuck task, so the user had to retry manually. The bytes already persisted (mmap + anacrolix's piece-completion DB keyed by info_hash; debrid via Range; usenet via its tracker) — the daemon just didn't re-attempt the work. ActiveTaskStore persists each in-flight download's agent.Task payload to active-tasks.json; the daemon re-submits them on startup so the downloaders resume the partial data. manager.Submit now dedups (the startup re-submit and a later web re-dispatch can't both run), and recordFinished removes a task from the store only on a genuine terminal — shuttingDown (set before Shutdown cancels the task contexts) keeps shutdown-interrupted tasks so they resume next start. Stream/seed/upgrade tasks aren't persisted; ForceStart is cleared on resume.
This commit is contained in:
parent
b708bb8ab2
commit
445da233c0
6 changed files with 399 additions and 9 deletions
|
|
@ -316,6 +316,11 @@ func runDaemonStart() error {
|
|||
},
|
||||
}, reporter, torrentDl, debridDl, usenetDl)
|
||||
|
||||
// Resume store: persist in-flight downloads so a daemon restart can re-submit
|
||||
// them (the downloaders resume the partial data). Wire it before any Submit.
|
||||
taskStore := agent.NewActiveTaskStore()
|
||||
manager.SetTaskStore(taskStore)
|
||||
|
||||
// Create persistent stream server
|
||||
streamSrv := engine.NewStreamServer(cfg.Download.StreamPort)
|
||||
streamSrv.SetUPnPEnabled(cfg.Download.EnableUPnP)
|
||||
|
|
@ -426,6 +431,20 @@ func runDaemonStart() error {
|
|||
}
|
||||
}
|
||||
|
||||
// Resume downloads interrupted by the previous shutdown/crash. Re-submit
|
||||
// each persisted task; its downloader picks up the partial data (torrent via
|
||||
// the piece-completion DB, debrid via Range, usenet via its tracker). Done
|
||||
// before the sync loop starts; a later web re-dispatch of the same id is
|
||||
// deduped by the manager.
|
||||
if resume := taskStore.Load(); len(resume) > 0 {
|
||||
log.Printf("[resume] re-submitting %d interrupted download(s)", len(resume))
|
||||
for _, t := range resume {
|
||||
t.ForceStart = false // respect MaxConcurrent on bulk auto-resume
|
||||
log.Printf("[resume] %s — %s", agent.ShortID(t.ID), t.Title)
|
||||
manager.Submit(ctx, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Wire: sync receives control signals → act on manager
|
||||
d.OnControlAction = func(action, taskID string, deleteFiles bool) {
|
||||
switch action {
|
||||
|
|
@ -847,13 +866,16 @@ func runDaemonStart() error {
|
|||
cancelStreamContexts()
|
||||
cancelAllPlayerSessions()
|
||||
streamSrv.Shutdown(context.Background())
|
||||
cancel()
|
||||
|
||||
// Give active downloads 30s to finish
|
||||
// Drain active downloads BEFORE cancelling the daemon context. Shutdown
|
||||
// sets shuttingDown + cancels each task context itself, so interrupted
|
||||
// downloads keep their resume-store entry. Cancelling the shared ctx first
|
||||
// would make them look like genuine failures and wipe the entry → no resume.
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer shutdownCancel()
|
||||
manager.Shutdown(shutdownCtx)
|
||||
|
||||
cancel()
|
||||
d.Deregister()
|
||||
fmt.Println(" Daemon stopped.")
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue