Preserve scan cache on timeout
This commit is contained in:
@@ -2233,6 +2233,71 @@ def test_scan_terminal_service_returns_stale_payload_after_failed_refresh(monkey
|
||||
assert stale["stale_reason"] == "upstream 504"
|
||||
|
||||
|
||||
def test_scan_terminal_timeout_does_not_replace_better_cached_snapshot(monkeypatch):
|
||||
import time
|
||||
|
||||
filters = {"scan_mode": "tradable", "limit": 5}
|
||||
normalized_filters = scan_terminal_service._normalize_scan_terminal_filters(filters)
|
||||
scan_terminal_cache._SCAN_TERMINAL_CACHE.clear()
|
||||
previous_payload = {
|
||||
"generated_at": "2026-05-31T00:00:00Z",
|
||||
"snapshot_id": "scan-existing",
|
||||
"filters": normalized_filters,
|
||||
"summary": {"candidate_total": 2, "visible_count": 2},
|
||||
"top_signal": {"id": "old-1"},
|
||||
"rows": [{"id": "old-1"}, {"id": "old-2"}],
|
||||
"status": "ready",
|
||||
"stale": False,
|
||||
"stale_reason": None,
|
||||
"last_success_at": None,
|
||||
"last_failed_at": None,
|
||||
}
|
||||
scan_terminal_cache.set_cached_scan_terminal_payload(
|
||||
normalized_filters,
|
||||
previous_payload,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
scan_terminal_service,
|
||||
"CITIES",
|
||||
{"fast": {"tz": 0}, "slow": {"tz": 0}},
|
||||
)
|
||||
monkeypatch.setattr(scan_terminal_service, "SCAN_TERMINAL_BUILD_TIMEOUT_SEC", 0.01)
|
||||
monkeypatch.setattr(scan_terminal_service, "SCAN_TERMINAL_MAX_WORKERS", 2)
|
||||
|
||||
def _scan_city(city_name, *_args, **_kwargs):
|
||||
if city_name == "slow":
|
||||
time.sleep(0.05)
|
||||
return {
|
||||
"city": city_name,
|
||||
"candidate_total": 1,
|
||||
"primary_scores": [80.0],
|
||||
"rows": [
|
||||
{
|
||||
"id": f"{city_name}-row",
|
||||
"market_key": f"{city_name}-market",
|
||||
"edge_percent": 4.0,
|
||||
"final_score": 80.0,
|
||||
"volume": 1000,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(scan_terminal_service, "_scan_city_terminal_rows", _scan_city)
|
||||
|
||||
stale = scan_terminal_service.build_scan_terminal_payload(filters, force_refresh=True)
|
||||
|
||||
assert stale["status"] == "stale"
|
||||
assert stale["stale"] is True
|
||||
assert [row["id"] for row in stale["rows"]] == ["old-1", "old-2"]
|
||||
assert stale["stale_reason"].startswith("scan terminal build timed out")
|
||||
cached = scan_terminal_cache.get_cached_scan_terminal_payload(
|
||||
normalized_filters,
|
||||
ttl_sec=3600,
|
||||
)
|
||||
assert [row["id"] for row in cached["rows"]] == ["old-1", "old-2"]
|
||||
|
||||
|
||||
def test_scan_terminal_service_returns_failed_without_success_snapshot(monkeypatch):
|
||||
filters = {"scan_mode": "tradable", "limit": 5}
|
||||
scan_terminal_cache._SCAN_TERMINAL_CACHE.clear()
|
||||
|
||||
@@ -47,6 +47,35 @@ def _normalize_city_key(value: Any) -> str:
|
||||
return ALIASES.get(text, text)
|
||||
|
||||
|
||||
def _rows_count(payload: Dict[str, Any]) -> int:
|
||||
rows = payload.get("rows")
|
||||
return len(rows) if isinstance(rows, list) else 0
|
||||
|
||||
|
||||
def _build_stale_payload_for_timeout_if_better_cached(
|
||||
*,
|
||||
filters: Dict[str, Any],
|
||||
cached_entry: Dict[str, Any],
|
||||
ranked_rows: List[Dict[str, Any]],
|
||||
timeout_message: Optional[str],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
success_payload = cached_entry.get("success_payload")
|
||||
if not isinstance(success_payload, dict) or not success_payload.get("rows"):
|
||||
return None
|
||||
if _rows_count(success_payload) < len(ranked_rows):
|
||||
return None
|
||||
|
||||
error_message = timeout_message or "市场扫描快照正在刷新中"
|
||||
set_scan_terminal_failure_state(filters, error_message=error_message)
|
||||
failed_entry = get_scan_terminal_cache_entry(filters) or cached_entry
|
||||
return build_stale_scan_terminal_payload(
|
||||
filters=filters,
|
||||
success_payload=success_payload,
|
||||
error_message=error_message,
|
||||
failed_at=failed_entry.get("last_failed_at"),
|
||||
)
|
||||
|
||||
|
||||
def _start_scan_terminal_background_refresh(filters: Dict[str, Any]) -> bool:
|
||||
if not mark_scan_terminal_refreshing(filters):
|
||||
return False
|
||||
@@ -189,6 +218,16 @@ def _build_scan_terminal_payload_uncached(
|
||||
|
||||
summary = ranked_result["summary"]
|
||||
top_signal = ranked_result["top_signal"]
|
||||
if timed_out:
|
||||
stale_payload = _build_stale_payload_for_timeout_if_better_cached(
|
||||
filters=filters,
|
||||
cached_entry=cached_entry,
|
||||
ranked_rows=ranked_rows,
|
||||
timeout_message=timeout_message,
|
||||
)
|
||||
if stale_payload is not None:
|
||||
return stale_payload
|
||||
|
||||
payload = {
|
||||
"generated_at": datetime.utcnow().isoformat() + "Z",
|
||||
"filters": filters,
|
||||
|
||||
Reference in New Issue
Block a user