{"record":{"id":"d84e6d44180d37ab","repo":"ZhuLinsen/daily_stock_analysis","slug":"error-d84e6d","errorCode":null,"errorMessage":"题材新闻搜索等待超过调用截止时间","messagePattern":"题材新闻搜索等待超过调用截止时间","errorType":"exception","errorClass":"TimeoutError","httpStatus":null,"severity":"warning","filePath":"src/search_service.py","lineNumber":2754,"sourceCode":"        deadline: Optional[float] = None,\n    ) -> Tuple[Optional['SearchResponse'], bool, Optional[threading.Event], bool]:\n        \"\"\"Return a cache hit or exclusive ownership of one cache fill.\n\n        Waiters never proceed to provider work without becoming the owner. If\n        an owner finishes without a cacheable response, all waiters compete for\n        the next reservation and the losers keep waiting on that new owner.\n        \"\"\"\n        waited = False\n        while True:\n            cached, cache_owner, cache_event = self._get_cached_or_reserve(key)\n            if cached is not None or cache_owner:\n                return cached, cache_owner, cache_event, waited\n            if cache_event is None:  # Defensive: the reservation API promises an event here.\n                raise RuntimeError(\"搜索缓存请求合并状态异常\")\n            waited = True\n            remaining = None if deadline is None else deadline - time.monotonic()\n            if remaining is not None and remaining <= 0:\n                raise TimeoutError(\"题材新闻搜索等待超过调用截止时间\")\n            if remaining is None:\n                cached = self._wait_for_cached(key, cache_event)\n            else:\n                cached = self._wait_for_cached(key, cache_event, timeout_seconds=remaining)\n            if cached is not None:\n                return cached, False, None, waited\n            if deadline is not None and time.monotonic() >= deadline:\n                raise TimeoutError(\"题材新闻搜索等待超过调用截止时间\")\n\n    def _put_cache(self, key: str, response: 'SearchResponse') -> None:\n        \"\"\"Store a successful SearchResponse in cache.\"\"\"\n        with self._cache_lock:\n            # Hard cap: evict oldest entries when cache exceeds limit\n            _MAX_CACHE_SIZE = 500\n            if len(self._cache) >= _MAX_CACHE_SIZE:\n                now = time.time()\n                # First pass: remove expired entries\n                expired = [k for k, (ts, _) in self._cache.items() if now - ts > self._cache_ttl]","sourceCodeStart":2736,"sourceCodeEnd":2772,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/search_service.py#L2736-L2772","documentation":"TimeoutError from the request-coalescing wait loop in the search cache: this caller lost the race to own the cache key, waited on the owner's event, and the overall caller-supplied deadline passed before either a cached result appeared or ownership became available. Checked before waiting (remaining <= 0) — so it can fire immediately if the deadline is already exhausted.","triggerScenarios":"Two or more concurrent searches for the same topic key with a deadline: the loser blocks on the owner's cache event; if the owner takes longer than the loser's remaining deadline, the loser raises this instead of starting its own search. Also fires when a caller passes a deadline that is already in the past while the key is contended.","commonSituations":"Fan-out analysis requesting the same topic concurrently under tight per-request deadlines; a slow owner search stretching beyond followers' budgets; retry storms piling onto one key; deadlines computed from a shared request clock that has nearly expired.","solutions":["Increase or remove the caller's deadline so followers can wait for the in-flight result instead of timing out","Retry the call — by then the owner has usually populated the cache and the retry returns instantly","Warm the cache ahead of burst windows (prefetch the topic) so followers never wait","If deadlines are strict by design, treat this as normal backpressure and degrade gracefully (skip topic news) rather than retrying"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"def has_deadline_slack(deadline: float, min_seconds: float = 1.0) -> bool:\n    return deadline is None or deadline - time.monotonic() >= min_seconds","typeGuard":null,"tryCatchPattern":"try:\n    resp = search_topic_news(topic, deadline=deadline)\nexcept TimeoutError as exc:\n    if \"调用截止时间\" in str(exc):\n        resp = get_cached_topic_news(topic)  # usually populated by now; else degrade\n    else:\n        raise","preventionTips":["Set deadlines longer than the single-search timeout plus contention slack","Prefetch hot topics before burst windows so followers hit cache, not waits","Treat coalescing timeouts as backpressure: degrade gracefully rather than retry-loop under load"],"tags":["timeout","concurrency","cache","search"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}