ZhuLinsen/daily_stock_analysis · warning · TimeoutError

{task_name} ranking fetch timeout after {timeout_value:g}s

Error message

{task_name} ranking fetch timeout after {timeout_value:g}s

What it means

TimeoutError(f'{task_name} ranking fetch timeout after {timeout_value:g}s') is raised in MarketHotspotService._call_with_timeout when future.result(timeout=timeout_value) raises FutureTimeoutError — i.e. the ranking fetch worker thread did not complete within the allotted wall-clock seconds, and the future itself did not already contain that exception. The inflight key is then put into a cooldown (retry_after = now + max(timeout, RANKING_FETCH_TIMEOUT_RETRY_DELAY_SECONDS)) so repeated calls back off instead of hammering the slow source.

Source

Thrown at src/services/market_hotspot_service.py:447

        effective_inflight_key = inflight_key or task_name
        future = cls._get_or_submit_ranking_fetch(
            task,
            inflight_key=effective_inflight_key,
            task_name=task_name,
        )
        try:
            return future.result(timeout=timeout_value)
        except FutureTimeoutError as exc:
            if future.done() and future.exception(timeout=0) is exc:
                raise
            cls._mark_ranking_fetch_timeout(
                effective_inflight_key,
                future,
                retry_after=time.monotonic()
                + max(timeout_value, RANKING_FETCH_TIMEOUT_RETRY_DELAY_SECONDS),
            )
            raise TimeoutError(
                f"{task_name} ranking fetch timeout after {timeout_value:g}s"
            ) from exc

    @classmethod
    def _get_or_submit_ranking_fetch(
        cls,
        task: Callable[[], Any],
        *,
        inflight_key: Hashable,
        task_name: str,
    ) -> Future:
        submitted: Future
        worker: threading.Thread
        with cls._ranking_fetch_futures_lock:
            retry_entry = cls._ranking_fetch_retry_after.get(inflight_key)
            now = time.monotonic()
            if retry_entry is not None:
                retry_future, retry_after = retry_entry

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Measure the actual source latency with curl -w '%{time_total}' against the ranking endpoint and set the timeout to ~2-3x that
  2. Retry after the cooldown window (RANKING_FETCH_TIMEOUT_RETRY_DELAY_SECONDS = 0.2s minimum) rather than immediately re-calling
  3. Check network egress/proxy health if all task_names time out simultaneously
  4. If only one task_name fails, that specific data provider is degraded — rely on the service's per-source fallback instead of raising the global timeout
  5. Look for leaked in-flight futures (detached futures never completing) if timeouts persist even after the source recovers

Example fix

# before
result = svc._call_with_timeout(fetch, timeout_seconds=1.5, task_name="eastmoney")

# after: sized to observed p99 latency
result = svc._call_with_timeout(fetch, timeout_seconds=6.0, task_name="eastmoney")
Defensive patterns

Strategy: retry

Try / catch

try:
    data = MarketHotspotService._call_with_timeout(fetch, timeout_seconds=6.0, task_name=name)
except TimeoutError as exc:
    if "after" in str(exc):
        logger.warning("%s slow; cooling down, will retry later", name)
        data = cached_ranking(name)  # fallback to last good data

Prevention

When it happens

Trigger: A ranking data source (e.g. EastMoney/THS ranking endpoints behind MarketHotspotService) responding slower than the configured timeout; network degradation; the source hanging without reading the socket; or the timeout set lower than the source's typical latency.

Common situations: Upstream ranking API slowdowns during peak trading hours, cross-border network latency, a stale in-flight future from an earlier request still holding the slot, or timeouts tuned too tight after a provider change.

Understand the failure class

Related errors


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/512108040b26a28c. Report an issue: GitHub.