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
|
|
@ -68,7 +68,7 @@ desde la web. Diseño + set de opciones en el estado abajo.
|
|||
|
||||
### Huecos medios ⬜
|
||||
- ~~Sin gestión de espacio en disco (`Statfs`)~~ ✅ **Pre-flight de espacio (2026-05-31)** — `CheckDiskSpace` antes de cada descarga (torrent/usenet/debrid) con reserva configurable `downloads.min_free_disk_mb` (default 2048); manager NO hace fallback en disco lleno; aviso web 507 `INSUFFICIENT_DISK` al despachar (torrentclaw). Monitoreo mid-download diferido. Ver estado abajo.
|
||||
- Resume de torrent NO persiste reinicio del daemon (usenet sí).
|
||||
- ~~Resume de torrent NO persiste reinicio del daemon~~ ✅ **Auto-resume tras reinicio (2026-05-31)** — `agent.ActiveTaskStore` persiste los `agent.Task` de descargas en vuelo (`active-tasks.json`); el daemon los re-somete al arrancar → los downloaders reanudan los bytes (torrent vía completion DB de anacrolix, debrid vía Range, usenet vía tracker). Dedup en `manager.Submit` (restore + re-despacho web no duplican). `shuttingDown` preserva el entry en apagado limpio (solo terminal genuino lo borra). Ver estado abajo.
|
||||
- Sin seeding/ratio lifecycle (flags existen, nadie los aplica).
|
||||
- Reproducir-mientras-baja: readahead estático 5MB, sin playhead→prioridad dinámica.
|
||||
- HDR→SDR sin tonemap (zscale/zimg) → HDR desaturado.
|
||||
|
|
@ -95,10 +95,19 @@ WireGuard endpoint sin pin · sesión única (1 viewer).
|
|||
- **Secreto de stream en claro en DB** (baja): `agent_registration.stream_secret` es una clave HMAC viva (por arranque) en la DB central; quien lea la DB puede mintear tokens HLS de cualquier agente. Inherente al diseño (el web debe mintear HLS). Mitigado por regeneración por arranque. Excluir esta columna de cualquier JSON admin/usuario.
|
||||
- **Refrescar/limpiar streamUrl al re-registrar** (baja): tras reinicio del daemon el secreto cambia; URLs `?t=` ya guardadas en `download_task.streamUrl` quedan stale hasta re-stream. Es auto-curativo, pero el web podría limpiar streamUrl en el re-register del agente.
|
||||
- **gofmt preexistente** en `internal/agent/types.go` (StreamSession) y `hls.go`/`torrent.go`/`stream_source.go` (no introducido por este trabajo) — chore aparte.
|
||||
- **Data race preexistente manager↔reporter (baja)**: bajo `-race`, `Task.ToStatusUpdate()` (leído por `ProgressReporter.flushBatch`) corre sin lock contra la escritura de campos del task en `processTask` (`manager.go:371`). No introducido por el resume; expuesto al correr la suite con `-race` (la suite normal corre sin `-race`). Fix: proteger los campos de estado/progreso del `Task` con su `mu` en ToStatusUpdate + processTask. Chore aparte. Múltiples `task.ID[:8]` en `progress.go`/`torrent.go` paniquean con ids <8 chars (irreal: el web manda UUIDs) — limpiar a `ShortID` de paso.
|
||||
- **Funnel = SPOF CloudFlare** (ya en huecos medios): el funnel sigue siendo trycloudflare; relay propio pendiente.
|
||||
- ~~**Rutas localizadas unarr 404 (media)**~~ ✅ **ARREGLADO (2026-05-31)**: bajo `NEXT_PUBLIC_BRAND=unarr` el allowlist `UNARR_PAGE_PREFIXES` (paths EN) no reconocía los localizados de next-intl (`/es/biblioteca`, `/es/descargas`, `/es/perfil`) → 404. Fix (web): `enFirstSegmentByLocalized` (mapa localizado→EN derivado de `routing.pathnames`) + `toCanonicalPath()` en `branding/routes.ts` traduce el 1er segmento antes del match. Assertion anti-colisión en el build del mapa (fail-fast si una ruta futura reusa un segmento → no puede colar una ruta denegada). Verificado: 175 entradas, cero crossover; denegadas siguen denegadas.
|
||||
- **Thumbnails — sprites/trickplay (media)**: cerrado solo el camino bajo demanda (N frames en vivo). El scrubber pregenerado (sprite/BIF de toda la timeline, preview al pasar el ratón por la barra) queda como hueco propio: reaprovecharía `/thumbnail` + cacheo en disco del agente. Decidido alcance "solo bajo demanda" con el usuario (2026-05-31).
|
||||
|
||||
### Hueco medio — Auto-resume de descargas tras reinicio ✅ CERRADO (2026-05-31)
|
||||
Antes: tras reiniciar el daemon, una descarga en vuelo quedaba abandonada (cola in-memory perdida, el web no re-despacha una tarea "downloading" atascada) hasta reintento manual. Los BYTES ya persistían (mmap + completion DB BoltDB de anacrolix, keyed por info_hash; debrid Range; usenet tracker) — faltaba que el daemon se re-sometiera solo.
|
||||
- **`agent.ActiveTaskStore`** (`active-tasks.json`, atómico tmp+rename): persiste el payload `agent.Task` re-submittable de descargas en vuelo. Add al arrancar la descarga, Remove en terminal genuino.
|
||||
- **Manager**: interfaz `taskPersister` (inyectable/testeable) + `SetTaskStore`. `Submit` ahora DEDUPLICA (mismo id del restore + re-despacho web no lanzan 2 goroutines) y persiste descargas (no stream/seed/upgrade-ReplacePath). `recordFinished` borra del store SALVO `shuttingDown` (atomic) → un apagado limpio preserva el entry; terminal genuino (completado/fallo/cancel-usuario) lo borra. ForceStart se limpia en el re-submit (respeta MaxConcurrent).
|
||||
- **Daemon**: construye el store, `SetTaskStore`, y al arrancar re-somete `Load()` antes del sync loop.
|
||||
- **/critico**: 1 revisor → **bug CRÍTICO (conf 98)**: el daemon hacía `cancel()` (ctx padre) ANTES de `manager.Shutdown()` → contextos de tarea cancelados antes de marcar `shuttingDown` → recordFinished con shuttingDown=false → borraba el entry → NO resume (guard era código muerto). FIX: `Manager.Shutdown` cancela los contextos él mismo ANTES de `wg.Wait` (con shuttingDown ya puesto) + el daemon llama `Shutdown` antes de `cancel()`. + ForceStart-strip + excluir upgrade. Tests: store round-trip, dedup, persist/remove-terminal, keep-on-shutdown, stream-no-persiste.
|
||||
- **Smoke**: cubierto por unit tests (incl. shutdown-keeps). El e2e real (descarga → kill daemon → restart → resume) no se ejecutó para no reiniciar el agente dev en uso por el usuario.
|
||||
|
||||
### Hueco medio — Gestión de espacio en disco (pre-flight) ✅ CERRADO (2026-05-31)
|
||||
Una descarga ya no llena el disco a 0 a mitad (corrompía el fichero parcial).
|
||||
- **CLI**: `internal/engine/diskspace.go` — `CheckDiskSpace(dir, need, reserve)` usa `agent.DiskInfo` (Statfs/GetDiskFreeSpaceEx, ya abstraído) y devuelve `*InsufficientDiskError` si `free-need < reserve`; best-effort (need≤0 o stat falla → nil, ENOSPC sigue de backstop). Cableado antes de escribir en los 3 downloaders (torrent: DataDir+totalBytes; debrid: outputDir+restantes; usenet: outputDir+totalBytes solo en fresh). Reserva por `SetMinFreeBytes` desde `downloads.min_free_disk_mb` (default 2048 MiB). `manager` falla sin fallback en disco lleno (otra fuente llena el mismo disco). Fix latente: `formatBytes` paniqueaba ≥1PB (array hasta TB) → +PB/EB+clamp.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue