fix(#3393): capped по сканированному окну — interrupted-строка не глушит лестницу навсегда; контроль zero-сторожа
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
This commit is contained in:
parent
5f94bf0eeb
commit
ba4fb03a05
3 changed files with 86 additions and 4 deletions
|
|
@ -463,9 +463,17 @@ def _alert_if_consecutive_failures(db: Session, source: str) -> None:
|
|||
# #3393: оборванного деплоем прогона в популяции стрика нет. getattr —
|
||||
# функция целиком под `except Exception: pass`, и строка без колонки
|
||||
# выключила бы сторож молча (тот же класс, что #2703).
|
||||
scanned = len(rows)
|
||||
rows = [r for r in rows if not _was_interrupted(getattr(r, "counters", None))]
|
||||
streak = _leading_streak(rows, lambda r: r.status in ("failed", "banned"))
|
||||
capped = streak >= STREAK_SCAN_LIMIT
|
||||
# Потолок — по СКАНИРОВАННОМУ окну, а не по отфильтрованному: по второму
|
||||
# одна interrupted-строка внутри 500 опускает измеримый максимум до 499, и
|
||||
# `capped` становится недостижим по построению. Источник, сломанный наглухо,
|
||||
# замолкал бы после 384-й неудачи навсегда (следующая веха 768 больше окна),
|
||||
# а новую interrupted-строку в окно подкладывает каждый деплой — ровно
|
||||
# анти-спам «один раз навсегда» из #2670/#2703. Условие читается так: окно
|
||||
# было полным И стрик покрывает всё, что мы вообще могли судить.
|
||||
capped = scanned >= STREAK_SCAN_LIMIT and streak >= len(rows)
|
||||
if not capped and not _streak_alert_due(streak, n):
|
||||
return
|
||||
|
||||
|
|
@ -526,6 +534,7 @@ def _alert_if_consecutive_zero_results(db: Session, source: str) -> None:
|
|||
|
||||
# #3393: та же популяция, что у сторожа неудач — оборванный деплоем прогон
|
||||
# не судим (в т.ч. не он решает, «измерен» ли свежайший результат ниже).
|
||||
scanned = len(rows)
|
||||
rows = [r for r in rows if not _was_interrupted(getattr(r, "counters", None))]
|
||||
if not rows:
|
||||
return
|
||||
|
|
@ -546,7 +555,10 @@ def _alert_if_consecutive_zero_results(db: Session, source: str) -> None:
|
|||
return
|
||||
|
||||
streak = _leading_streak(rows, _is_zero_done)
|
||||
capped = streak >= STREAK_SCAN_LIMIT
|
||||
# Потолок — по сканированному окну, как у _alert_if_consecutive_failures:
|
||||
# по отфильтрованному списку одна interrupted-строка делает `capped`
|
||||
# недостижимым, и лестница за последней вехой в пределах окна молчит навсегда.
|
||||
capped = scanned >= STREAK_SCAN_LIMIT and streak >= len(rows)
|
||||
if not capped and not _streak_alert_due(streak, n):
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,64 @@ def test_streaks_unchanged_without_the_mark(name: str) -> None:
|
|||
_run_failure_watchdog(_MODULES[name], rows).capture_message.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(_MODULES))
|
||||
def test_zero_streak_unchanged_without_the_mark(name: str) -> None:
|
||||
"""Тот же контроль для zero-result-сторожа: 4 измеренных нуля + 'done' с
|
||||
результатом, без метки → стрик 4, между вехами, молчание (прежнее поведение)."""
|
||||
rows = [_row("done", lots_fetched=0) for _ in range(4)] + [_row("done", lots_fetched=42)]
|
||||
_run_zero_watchdog(_MODULES[name], rows).capture_message.assert_not_called()
|
||||
|
||||
|
||||
# ── (б2) потолок окна: одна interrupted-строка не глушит лестницу навсегда ────
|
||||
|
||||
|
||||
def _full_window(mod: Any, row: SimpleNamespace, interrupted: SimpleNamespace | None) -> list[Any]:
|
||||
"""Полное окно сканирования из одинаковых строк; `interrupted` (если дан) — внутри."""
|
||||
rows = [row for _ in range(mod.STREAK_SCAN_LIMIT)]
|
||||
if interrupted is not None:
|
||||
rows[100] = interrupted
|
||||
return rows
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(_MODULES))
|
||||
def test_full_failure_window_alerts_despite_one_interrupted(name: str) -> None:
|
||||
"""500 отказов, одна строка с меткой внутри окна → алерт по потолку, не тишина.
|
||||
|
||||
Красный на main: `capped` считался по УЖЕ отфильтрованному списку, где максимум
|
||||
499 — то есть недостижим по построению, как только в окно попадает хоть одна
|
||||
interrupted-строка (а её подкладывает каждый деплой). Стрик 499 мимо вехи
|
||||
(последняя в пределах окна — 384, следующая 768), поэтому источник, сломанный
|
||||
наглухо, замолкал бы навсегда: ровно анти-спам «один раз навсегда» (#2670/#2703).
|
||||
"""
|
||||
mod = _MODULES[name]
|
||||
limit = mod.STREAK_SCAN_LIMIT
|
||||
marked = _row("failed", attempted=10, failed=2, interrupted=1)
|
||||
sentry = _run_failure_watchdog(mod, _full_window(mod, _row("failed"), marked))
|
||||
sentry.capture_message.assert_called_once()
|
||||
assert f"{limit - 1} consecutive" in sentry.capture_message.call_args[0][0]
|
||||
|
||||
# Контроль: то же окно без метки алертило и до правки — краснеет метка, а не длина.
|
||||
control = _run_failure_watchdog(mod, _full_window(mod, _row("failed"), None))
|
||||
control.capture_message.assert_called_once()
|
||||
assert f"{limit} consecutive" in control.capture_message.call_args[0][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(_MODULES))
|
||||
def test_full_zero_window_alerts_despite_one_interrupted(name: str) -> None:
|
||||
"""Тот же потолок у второй лестницы — обе копии правятся одинаково."""
|
||||
mod = _MODULES[name]
|
||||
limit = mod.STREAK_SCAN_LIMIT
|
||||
zero = _row("done", lots_fetched=0)
|
||||
marked = _row("done", lots_fetched=0, interrupted=1)
|
||||
sentry = _run_zero_watchdog(mod, _full_window(mod, zero, marked))
|
||||
sentry.capture_message.assert_called_once()
|
||||
assert f"{limit - 1} consecutive" in sentry.capture_message.call_args[0][0]
|
||||
|
||||
control = _run_zero_watchdog(mod, _full_window(mod, zero, None))
|
||||
control.capture_message.assert_called_once()
|
||||
assert f"{limit} consecutive" in control.capture_message.call_args[0][0]
|
||||
|
||||
|
||||
# ── (в) honest-status-гейт не судит частичные counters ───────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -462,9 +462,17 @@ def _alert_if_consecutive_failures(db: Session, source: str) -> None:
|
|||
# #3393: оборванного деплоем прогона в популяции стрика нет. getattr —
|
||||
# функция целиком под `except Exception: pass`, и строка без колонки
|
||||
# выключила бы сторож молча (тот же класс, что #2703).
|
||||
scanned = len(rows)
|
||||
rows = [r for r in rows if not _was_interrupted(getattr(r, "counters", None))]
|
||||
streak = _leading_streak(rows, lambda r: r.status in ("failed", "banned"))
|
||||
capped = streak >= STREAK_SCAN_LIMIT
|
||||
# Потолок — по СКАНИРОВАННОМУ окну, а не по отфильтрованному: по второму
|
||||
# одна interrupted-строка внутри 500 опускает измеримый максимум до 499, и
|
||||
# `capped` становится недостижим по построению. Источник, сломанный наглухо,
|
||||
# замолкал бы после 384-й неудачи навсегда (следующая веха 768 больше окна),
|
||||
# а новую interrupted-строку в окно подкладывает каждый деплой — ровно
|
||||
# анти-спам «один раз навсегда» из #2670/#2703. Условие читается так: окно
|
||||
# было полным И стрик покрывает всё, что мы вообще могли судить.
|
||||
capped = scanned >= STREAK_SCAN_LIMIT and streak >= len(rows)
|
||||
if not capped and not _streak_alert_due(streak, n):
|
||||
return
|
||||
|
||||
|
|
@ -527,6 +535,7 @@ def _alert_if_consecutive_zero_results(db: Session, source: str) -> None:
|
|||
|
||||
# #3393: та же популяция, что у сторожа неудач — оборванный деплоем прогон
|
||||
# не судим (в т.ч. не он решает, «измерен» ли свежайший результат ниже).
|
||||
scanned = len(rows)
|
||||
rows = [r for r in rows if not _was_interrupted(getattr(r, "counters", None))]
|
||||
if not rows:
|
||||
return
|
||||
|
|
@ -547,7 +556,10 @@ def _alert_if_consecutive_zero_results(db: Session, source: str) -> None:
|
|||
return
|
||||
|
||||
streak = _leading_streak(rows, _is_zero_done)
|
||||
capped = streak >= STREAK_SCAN_LIMIT
|
||||
# Потолок — по сканированному окну, как у _alert_if_consecutive_failures:
|
||||
# по отфильтрованному списку одна interrupted-строка делает `capped`
|
||||
# недостижимым, и лестница за последней вехой в пределах окна молчит навсегда.
|
||||
capped = scanned >= STREAK_SCAN_LIMIT and streak >= len(rows)
|
||||
if not capped and not _streak_alert_due(streak, n):
|
||||
return
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue