Восстановление зависших загрузок и уведомления о падении (state-reconciliation)
Долгий metaDL больше не убивается агрессивным таймаутом: дефолт magnet_timeout 30m → 24h (страховочный предохранитель), базис отсчёта — added_on из qBittorrent, а не created_at (переживает retry/усыновление). Авто-восстановление: фоновая сверка возвращает в поток задачи, упавшие по нашей нетерпеливости (magnet_timeout/stalled), когда источник ожил и продвинулся за условие падения (downloading/completed по статусу торрента); qbit_error не воскрешается. Конфликт idempotency (infohash занят другой активной задачей) — оставляем в failed. Уведомления: любой переход в failed/stuck пингует автора (включая приёмный qbit_add через ingest), с дебаунсом против спама при флаппинге stalled. Ручной retry добавлен в веб-UI и Telegram; Retry перецепляется к живому торренту вместо слепого Add. Дельта state-reconciliation влита в живые спеки; обновлён workflow.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,10 @@ import (
|
||||
// capIngest — стадия приёма для поля capability в логах.
|
||||
const capIngest = "ingest"
|
||||
|
||||
// errCodeQbitAdd — error_code задачи, упавшей на добавлении источника в
|
||||
// qBittorrent (раздачи в qBittorrent нет, восстановлению не подлежит).
|
||||
const errCodeQbitAdd = "qbit_add"
|
||||
|
||||
// Store — нужная ingest часть хранилища.
|
||||
type Store interface {
|
||||
FindActiveByInfohash(ctx context.Context, infohash string) (*store.Download, error)
|
||||
@@ -49,6 +53,12 @@ type Service struct {
|
||||
namer Namer
|
||||
cfg Config
|
||||
log *slog.Logger
|
||||
|
||||
// notifyFailed — опц. пинг автору о падении приёма (добавление в qBittorrent
|
||||
// не удалось). Closure, а не worker.Notifier: приёмное падение в qBit не
|
||||
// попадает в поллинг-цикл worker (раздачи нет), поэтому уведомляет ingest
|
||||
// сам; closure избавляет ядро приёма от зависимости на пакет worker.
|
||||
notifyFailed func(downloadID int64)
|
||||
}
|
||||
|
||||
// New собирает сервис приёма. namer опционален (nil → отображаемое имя не
|
||||
@@ -57,6 +67,9 @@ func New(st Store, qb QBittorrent, namer Namer, cfg Config, log *slog.Logger) *S
|
||||
return &Service{store: st, qbt: qb, namer: namer, cfg: cfg, log: log}
|
||||
}
|
||||
|
||||
// SetFailureNotifier подключает пинг о падении приёма (до начала работы).
|
||||
func (s *Service) SetFailureNotifier(fn func(downloadID int64)) { s.notifyFailed = fn }
|
||||
|
||||
// Request — входной запрос приёма.
|
||||
type Request struct {
|
||||
Source string // пока — magnet-ссылка
|
||||
@@ -135,8 +148,12 @@ func (s *Service) Ingest(ctx context.Context, req Request) (Result, error) {
|
||||
// это разные факты, не дубль.
|
||||
log.Error("download accept failed", "error", addErr)
|
||||
// Задача уже в БД — помечаем failed, чтобы worker её не подхватил.
|
||||
if setErr := s.store.SetDownloadState(ctx, id, store.StateFailed, "qbit_add", addErr.Error()); setErr != nil {
|
||||
if setErr := s.store.SetDownloadState(ctx, id, store.StateFailed, errCodeQbitAdd, addErr.Error()); setErr != nil {
|
||||
log.Error("mark download failed after qbit error failed", "error", setErr)
|
||||
} else if s.notifyFailed != nil {
|
||||
// Это падение минует worker.transition (раздачи в qBit нет) — уведомляем
|
||||
// сами, чтобы приёмные провалы тоже доходили до автора.
|
||||
go s.notifyFailed(id)
|
||||
}
|
||||
return Result{DownloadID: id, Infohash: info.Infohash, State: store.StateFailed},
|
||||
fmt.Errorf("ingest: add to qbittorrent: %w", addErr)
|
||||
|
||||
Reference in New Issue
Block a user