Витрина лендинга: пустой пересчёт больше не считается успешным, а итог игры «Проверьте себя» говорит, что квартиры отобраны #3550
2 changed files with 99 additions and 1 deletions
|
|
@ -357,6 +357,22 @@ async def _job_landing_showcase_deals(
|
||||||
counters = await loop.run_in_executor(
|
counters = await loop.run_in_executor(
|
||||||
None, lambda: refresh_landing_showcase_deals(db, **kwargs)
|
None, lambda: refresh_landing_showcase_deals(db, **kwargs)
|
||||||
)
|
)
|
||||||
|
# #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)
|
ctx.runs.mark_done(db, run_id, counters)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("scheduler: landing_showcase_deals crashed run_id=%d", run_id)
|
logger.exception("scheduler: landing_showcase_deals crashed run_id=%d", run_id)
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
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
|
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. Живая БД: строка реально ложится в таблицу ────────────────────────────
|
# ── 4. Живая БД: строка реально ложится в таблицу ────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue