fix: apply the non-productive stall budget to the embedding loop

The embedding loop shares the wall-clock entered_at and the same stall
verdict, so it failed the same way through a different path: one timed-out
attempt, then any round with no runnable source, and _on_no_runnable
declared the scope dead. Issue #8 only recorded the chat path; the
regression test pins this one.
This commit is contained in:
2026-08-06 09:42:07 -04:00
parent 02c3d06ec6
commit 6d0f3c9044
2 changed files with 68 additions and 6 deletions
+8 -6
View File
@@ -39,7 +39,7 @@ from polygateway.errors import (
)
from polygateway.middleware.breaker import BreakerGate
from polygateway.middleware.ratelimit import QuotaGate
from polygateway.middleware.retry import _failure_reason, backoff_delay
from polygateway.middleware.retry import StallClock, _failure_reason, backoff_delay
from polygateway.middleware.telemetry import TelemetryEmitter
from polygateway.sources import SourceCooldownMemo
from polygateway.types import (
@@ -179,13 +179,15 @@ class EmbeddingClient:
) -> _BatchOutcome:
fails = 0
reasons: dict[str, str] = {}
entered_at = self._now()
# 只计非生产性等待(issue #8): 真实尝试由重试预算治理,不重复烧 stall 预算
clock = StallClock(self._now)
while True:
picked, gate_rejections = await self._pick_runnable(reasons)
if picked is None:
await self._on_no_runnable(gate_rejections, reasons, entered_at)
await self._on_no_runnable(gate_rejections, reasons, clock)
continue
outcome = await self._attempt(batch, *picked, reasons, session_id, parent_call_id)
async with clock.attempting():
outcome = await self._attempt(batch, *picked, reasons, session_id, parent_call_id)
if isinstance(outcome, _BatchOutcome):
return outcome
fails += 1
@@ -228,7 +230,7 @@ class EmbeddingClient:
return None, gate_rejections
async def _on_no_runnable(
self, gate_rejections: int, reasons: dict[str, str], entered_at: float
self, gate_rejections: int, reasons: dict[str, str], clock: StallClock
) -> None:
if gate_rejections == len(self._sources):
names = tuple(s.name for s in self._sources)
@@ -245,7 +247,7 @@ class EmbeddingClient:
per_source_reasons=reasons,
)
stall = self._bp.stall_window_s
if self._now() - entered_at > stall and await self._quota.progress_age_s() > stall:
if clock.stalled_s() > stall and await self._quota.progress_age_s() > stall:
names = tuple(s.name for s in self._sources)
raise AllSourcesExhausted(
scope=self._scope,