Витрина лендинга: пустой пересчёт больше не считается успешным, а итог игры «Проверьте себя» говорит, что квартиры отобраны #3550
2 changed files with 99 additions and 1 deletions
|
|
@ -357,7 +357,23 @@ async def _job_landing_showcase_deals(
|
|||
counters = await loop.run_in_executor(
|
||||
None, lambda: refresh_landing_showcase_deals(db, **kwargs)
|
||||
)
|
||||
ctx.runs.mark_done(db, run_id, counters)
|
||||
# #3511: пустая витрина — не успех. Результатного ключа кита
|
||||
# (`_RESULT_COUNTER_KEYS`) у счётчиков витрины нет, сводка просроченных
|
||||
# судит её по статусу, и `done` с written=0 гасил часы свежести так же,
|
||||
# как удачный прогон, — а страница тем временем теряла таблицу целиком.
|
||||
if not counters.get("written"):
|
||||
logger.error(
|
||||
"landing_showcase_deals run_id=%d: витрина пуста (written=0, "
|
||||
"considered=%s, eligible=%s) — прогон неуспешен",
|
||||
run_id,
|
||||
counters.get("considered"),
|
||||
counters.get("eligible"),
|
||||
)
|
||||
ctx.runs.mark_failed(
|
||||
db, run_id, "витрина пуста: прогон не записал ни одной строки (written=0)", counters
|
||||
)
|
||||
else:
|
||||
ctx.runs.mark_done(db, run_id, counters)
|
||||
except Exception:
|
||||
logger.exception("scheduler: landing_showcase_deals crashed run_id=%d", run_id)
|
||||
ctx.runs.mark_failed(db, run_id, "landing_showcase_deals failed", {})
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ from datetime import UTC, datetime, timedelta
|
|||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -211,6 +212,87 @@ def test_showcase_counters_do_not_fake_freshness() -> None:
|
|||
assert sched.run_brought_data("failed", {"considered": 200, "written": 0}) is False
|
||||
|
||||
|
||||
# ── 3а. Пустой прогон витрины не гасит часы свежести (#3511) ────────────────
|
||||
|
||||
|
||||
class _RecordedRuns:
|
||||
"""`ctx.runs`, который кладёт финал прогона строкой `_STALE_SOURCES_SQL`.
|
||||
|
||||
Тогда тест идёт ТЕМ ЖЕ путём, что и сводка на проде: обработчик ставит статус,
|
||||
`freshness_rows` судит, принёс ли прогон данные, `stale_sources` — просрочку.
|
||||
Проверка «вызван mark_failed» такой путь не проходит: она зелёная и тогда,
|
||||
когда статус верный, а мера свежести его не видит.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.rows: list[Any] = []
|
||||
self.finished_at = NOW
|
||||
|
||||
def _row(self, status: str, counters: dict[str, Any]) -> None:
|
||||
self.rows.append(
|
||||
SimpleNamespace(
|
||||
source=SOURCE,
|
||||
interval_days=str(_seeded_interval_days()),
|
||||
created_at=NOW - timedelta(days=30),
|
||||
finished_at=self.finished_at,
|
||||
status=status,
|
||||
counters=counters,
|
||||
)
|
||||
)
|
||||
|
||||
def mark_done(self, db: Any, run_id: int, counters: dict[str, Any]) -> None:
|
||||
self._row("done", counters)
|
||||
|
||||
def mark_failed(self, db: Any, run_id: int, error: str, counters: dict[str, Any]) -> None:
|
||||
self._row("failed", counters)
|
||||
|
||||
|
||||
async def _run_handler(runs: _RecordedRuns, counters: dict[str, int], age_days: float) -> None:
|
||||
runs.finished_at = NOW - timedelta(days=age_days)
|
||||
with patch(
|
||||
"app.tasks.landing_showcase_deals.refresh_landing_showcase_deals",
|
||||
return_value=counters,
|
||||
):
|
||||
await _job_landing_showcase_deals(MagicMock(), 1, {}, SimpleNamespace(runs=runs))
|
||||
|
||||
|
||||
_FULL = {"considered": 200, "priced": 180, "eligible": 160, "written": 20}
|
||||
_EMPTY = {"considered": 200, "priced": 180, "eligible": 160, "written": 0}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_showcase_run_does_not_reset_freshness() -> None:
|
||||
"""Последний непустой прогон старше 3× такта, вчерашний пустой → витрина в тревоге.
|
||||
|
||||
До #3511 пустой прогон завершался `done`, и `run_brought_data` (судит по
|
||||
статусу: результатного ключа кита у витрины нет) засчитывал его свежестью —
|
||||
опустевший блок лэндинга молчал бы сколько угодно.
|
||||
"""
|
||||
runs = _RecordedRuns()
|
||||
await _run_handler(runs, _FULL, age_days=_ACCEPTANCE_CYCLES * _seeded_interval_days() + 1)
|
||||
await _run_handler(runs, _EMPTY, age_days=1)
|
||||
|
||||
stale = _stale_now(runs.rows)
|
||||
assert [s.source for s in stale] == [SOURCE], (
|
||||
f"пустой прогон витрины засчитан свежестью: статусы {[r.status for r in runs.rows]}"
|
||||
)
|
||||
assert stale[0].age_days > _ACCEPTANCE_CYCLES * _seeded_interval_days()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nonempty_showcase_run_keeps_freshness() -> None:
|
||||
"""Контроль: тот же сценарий с непустым последним прогоном — тревоги нет.
|
||||
|
||||
Зелёный с обеих сторон правки; без него правка могла бы валить любой прогон.
|
||||
"""
|
||||
runs = _RecordedRuns()
|
||||
await _run_handler(runs, _FULL, age_days=_ACCEPTANCE_CYCLES * _seeded_interval_days() + 1)
|
||||
await _run_handler(runs, _FULL, age_days=1)
|
||||
|
||||
assert _stale_now(runs.rows) == []
|
||||
assert [r.status for r in runs.rows] == ["done", "done"]
|
||||
|
||||
|
||||
# ── 4. Живая БД: строка реально ложится в таблицу ────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue