Retry/stall: игнорировать last_activity из будущего (клэмп)

Наблюдение из ревью кластера B: если qBittorrent отдаёт last_activity
впереди now (перекос часов или sentinel «никогда не был активен»),
stallDuration уходил в минус и реально застрявший торрент никогда не
помечался stuck. Теперь значение из будущего трактуется как непригодное
и простой считается от базиса добавления (addedBasis), как при
отсутствующем last_activity. Поведение спеки не меняется — оборонительная
деталь реализации.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-08 17:35:33 +03:00
co-authored by Claude Opus 4.8
parent 4cc4de4269
commit 78c61605fd
2 changed files with 29 additions and 4 deletions
+21
View File
@@ -239,3 +239,24 @@ func TestStallMeasuredFromLastActivity(t *testing.T) {
t.Errorf("простой 2ч > stuck_after 1ч должен дать stuck, got %q", got)
}
}
// Оборонительный клэмп: last_activity из будущего (перекос часов или sentinel
// «никогда не был активен») трактуется как непригодное значение. Иначе простой
// вышел бы отрицательным и реально застрявший торрент никогда не пометился бы
// stuck. Значение игнорируется, простой считается от базиса добавления (5ч).
func TestStallIgnoresFutureLastActivity(t *testing.T) {
const ih = "541adcff3b6dd5dba7088ea83317d9d6fac331d6"
lastActivityFuture := time.Date(2026, 6, 14, 11, 0, 0, 0, time.UTC).Unix() // 1ч в будущем
st := oneDownloading(ih, timeOld)
qb := &fakeQbt{torrents: []qbt.Torrent{{
Hash: ih, State: "stalledDL",
AddedOn: addedLongAgo, LastActivity: lastActivityFuture,
}}}
w := newTestWorker(st, qb)
if err := w.Poll(context.Background()); err != nil {
t.Fatal(err)
}
if got := st.downloads["1"].State; got != store.StateStuck {
t.Errorf("last_activity из будущего игнорируется (фолбэк на возраст 5ч > stuck_after 1ч → stuck), got %q", got)
}
}