Восстановление зависших загрузок и уведомления о падении (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:
@@ -65,6 +65,7 @@ type fakeReviewer struct {
|
||||
typed map[int64]string
|
||||
deferred []int64
|
||||
canceled []int64
|
||||
retried []int64
|
||||
}
|
||||
|
||||
func (f *fakeReviewer) ReviewData(context.Context, int64) (*worker.ReviewData, error) {
|
||||
@@ -96,6 +97,10 @@ func (f *fakeReviewer) Cancel(_ context.Context, id int64) error {
|
||||
f.canceled = append(f.canceled, id)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) Retry(_ context.Context, id int64) error {
|
||||
f.retried = append(f.retried, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func reviewData(state store.State) *worker.ReviewData {
|
||||
s, e := 2, 1
|
||||
@@ -252,6 +257,29 @@ func TestBot_NotifyDone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBot_NotifyFailed(t *testing.T) {
|
||||
b, api, _, rev := newTestBot(t, []int64{7})
|
||||
rev.data = reviewData(store.StateFailed)
|
||||
b.Notify(context.Background(), 5, worker.EventFailed)
|
||||
|
||||
if len(api.sent) != 1 || !strings.Contains(api.sent[0].text, "не удалась") {
|
||||
t.Errorf("sent = %+v", api.sent)
|
||||
}
|
||||
if !api.sent[0].hasKB { // кнопка повтора
|
||||
t.Error("уведомление о падении без клавиатуры повтора")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBot_CallbackRetry(t *testing.T) {
|
||||
b, _, _, rev := newTestBot(t, []int64{7})
|
||||
rev.data = reviewData(store.StateFailed)
|
||||
b.handleCallback(context.Background(), cbFrom(7, "retry:5"))
|
||||
|
||||
if len(rev.retried) != 1 || rev.retried[0] != 5 {
|
||||
t.Errorf("retried = %v", rev.retried)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCallback(t *testing.T) {
|
||||
a, id, v := parseCallback("type:5:series")
|
||||
if a != "type" || id != 5 || v != "series" {
|
||||
|
||||
Reference in New Issue
Block a user