Восстановление зависших загрузок и уведомления о падении (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:
@@ -40,10 +40,13 @@ const (
|
||||
// ТОЛЬКО сюда — иначе семантика «активности» разъедется (idempotency_key
|
||||
// снимается по IsTerminal, а активность считалась бы по другому списку).
|
||||
//
|
||||
// Состояния рассинхрона (target_missing/orphaned/deleted) — терминальны по
|
||||
// тем же причинам, что reverted/cancelled: дальше двигает либо человек
|
||||
// (relink из target_missing), либо фоновая сверка (healing/прогрессия),
|
||||
// напрямую через SetDownloadState; ключ идемпотентности при этом не нужен.
|
||||
// Состояния рассинхрона (target_missing/orphaned/deleted), а также
|
||||
// failed/stuck — терминальны для idempotency_key, но не «мертвы»: дальше
|
||||
// двигает либо человек (relink из target_missing, retry из failed/stuck), либо
|
||||
// фоновая сверка (healing/прогрессия desync; авто-восстановление failed/stuck
|
||||
// при оживлении источника, см. state-reconciliation) — напрямую через
|
||||
// SetDownloadState, который восстановит ключ для нетерминального целевого
|
||||
// состояния.
|
||||
var terminalStates = []State{
|
||||
StateDone, StateCancelled, StateFailed, StateReverted,
|
||||
StateTargetMissing, StateOrphaned, StateDeleted,
|
||||
@@ -157,6 +160,30 @@ func (s *Store) ListDownloadsByState(ctx context.Context, states ...State) ([]Do
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListRecoverable возвращает задачи в failed/stuck с одним из переданных
|
||||
// error_code — кандидатов на авто-восстановление (см. state-reconciliation).
|
||||
// Фильтр по коду в SQL, чтобы не вычитывать на каждом тике поллинга все
|
||||
// накопленные провалы (qbit_error и пр.), которые восстановлению не подлежат.
|
||||
func (s *Store) ListRecoverable(ctx context.Context, codes ...string) ([]Download, error) {
|
||||
if len(codes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
ph := make([]string, len(codes))
|
||||
args := make([]any, 0, len(codes)+2)
|
||||
for i, c := range codes {
|
||||
ph[i] = "?"
|
||||
args = append(args, c)
|
||||
}
|
||||
args = append(args, string(StateFailed), string(StateStuck))
|
||||
q := `SELECT * FROM download WHERE error_code IN (` + strings.Join(ph, ",") +
|
||||
`) AND state IN (?, ?) ORDER BY id DESC`
|
||||
var out []Download
|
||||
if err := s.DB.SelectContext(ctx, &out, q, args...); err != nil {
|
||||
return nil, fmt.Errorf("list recoverable: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// FindActiveByInfohash возвращает незавершённую задачу для infohash либо
|
||||
// (nil, nil), если её нет. Основа идемпотентного приёма.
|
||||
func (s *Store) FindActiveByInfohash(ctx context.Context, infohash string) (*Download, error) {
|
||||
|
||||
Reference in New Issue
Block a user