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:
@@ -239,3 +239,24 @@ func TestStallMeasuredFromLastActivity(t *testing.T) {
|
|||||||
t.Errorf("простой 2ч > stuck_after 1ч должен дать stuck, got %q", got)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -626,12 +626,16 @@ func (w *Worker) torrentAge(d store.Download, t qbt.Torrent) (time.Duration, boo
|
|||||||
|
|
||||||
// stallDuration — длительность простоя торрента для stuck_after: от
|
// stallDuration — длительность простоя торрента для stuck_after: от
|
||||||
// last_activity qBittorrent (момент последнего движения данных), с фолбэком на
|
// last_activity qBittorrent (момент последнего движения данных), с фолбэком на
|
||||||
// базис добавления, если qBit не отдал last_activity. Базис приподнят до
|
// базис добавления, если qBit не отдал пригодного last_activity. Базис приподнят
|
||||||
// retried_at (ручной retry даёт свежее окно). ok=false — базис неизвестен.
|
// до retried_at (ручной retry даёт свежее окно). ok=false — базис неизвестен.
|
||||||
|
//
|
||||||
|
// last_activity в будущем (перекос часов, sentinel «никогда не был активен»)
|
||||||
|
// трактуем как непригодное значение и падаем на addedBasis: иначе простой вышел
|
||||||
|
// бы отрицательным и реально застрявший торрент никогда бы не пометился stuck.
|
||||||
func (w *Worker) stallDuration(d store.Download, t qbt.Torrent) (time.Duration, bool) {
|
func (w *Worker) stallDuration(d store.Download, t qbt.Torrent) (time.Duration, bool) {
|
||||||
var basis time.Time
|
var basis time.Time
|
||||||
if t.LastActivity > 0 {
|
if la := time.Unix(t.LastActivity, 0).UTC(); t.LastActivity > 0 && !la.After(w.now()) {
|
||||||
basis = time.Unix(t.LastActivity, 0).UTC()
|
basis = la
|
||||||
} else {
|
} else {
|
||||||
var ok bool
|
var ok bool
|
||||||
if basis, ok = w.addedBasis(d, t); !ok {
|
if basis, ok = w.addedBasis(d, t); !ok {
|
||||||
|
|||||||
Reference in New Issue
Block a user