Gate probe: dedicated executor + /metrics negative cache

The 5s probe fetched /metrics via asyncio.to_thread, which shares the
DEFAULT executor with second-long truncation tokenizations -- 96 of
those queue-jumped the probe and the state machine never ticked (gate
frozen at 1 while results flowed, ETA 6h). Probes now run on a
dedicated single-thread executor, and after 3 consecutive fetch
failures the gate stops asking for /metrics entirely (this endpoint
404s; pure demand mode from then on).

Verified under a choked default executor: gate ticks 1->2 on schedule
and metrics_dead engages after 3 failures.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sora 2026-09-15 02:56:04 +00:00
parent 8cc5f6a9c6
commit 6b336a9e5d

View File

@ -190,6 +190,12 @@ class AdaptiveGate:
self._prev = (None, None) # (level, rate) we came from / last-good self._prev = (None, None) # (level, rate) we came from / last-good
self._bis = (None, None) # bisect bounds (lo=good, hi=bad) self._bis = (None, None) # bisect bounds (lo=good, hi=bad)
self._good_rate = 0.0 # throughput at the good bound (baseline) self._good_rate = 0.0 # throughput at the good bound (baseline)
self._fetch_exec = None # DEDICATED executor for /metrics probes:
# the shared default pool is occupied by
# second-long tokenization jobs, and the
# probe queued behind them never ticked
# (gate frozen at 1 while results flowed)
self._metrics_dead = False # 3 consecutive fetch failures -> stop asking
self.stats = {'probe': 0, 'ramp': 0, 'hold_queue': 0, 'backoff_fail': 0, self.stats = {'probe': 0, 'ramp': 0, 'hold_queue': 0, 'backoff_fail': 0,
'backoff_queue': 0, 'ramp_demand': 0, 'bisect': 0} 'backoff_queue': 0, 'ramp_demand': 0, 'bisect': 0}
@ -369,6 +375,9 @@ class AdaptiveGate:
self._wake() self._wake()
async def _probe_once(self) -> None: async def _probe_once(self) -> None:
if self._metrics_dead:
self._no_signal_ramp() # endpoint said no thrice: stop asking
return
import urllib.request import urllib.request
url = f'{self.adapter.api_base.rstrip("/")}/metrics' url = f'{self.adapter.api_base.rstrip("/")}/metrics'
@ -378,12 +387,23 @@ class AdaptiveGate:
return resp.read().decode('utf-8', 'ignore') return resp.read().decode('utf-8', 'ignore')
try: try:
# thread: the blocking fetch must never stall the event loop # DEDICATED single-thread executor: the shared asyncio pool is
# (an unreachable host parks urlopen for the full 4s timeout) # full of second-long truncation tokenizations, and a probe
text = await asyncio.to_thread(_fetch) # queued behind them never ran (gate appeared frozen)
if self._fetch_exec is None:
import concurrent.futures
self._fetch_exec = concurrent.futures.ThreadPoolExecutor(
max_workers=1, thread_name_prefix='gate-probe')
loop = asyncio.get_running_loop()
text = await loop.run_in_executor(self._fetch_exec, _fetch)
except Exception: except Exception:
self._fetch_fails = getattr(self, '_fetch_fails', 0) + 1
if self._fetch_fails >= 3:
self._metrics_dead = True # 404/unreachable: pure demand mode
self._no_signal_ramp() # no metrics: demand-driven fallback self._no_signal_ramp() # no metrics: demand-driven fallback
return return
self._fetch_fails = 0
running = queue = None running = queue = None
for line in text.splitlines(): for line in text.splitlines():
if line.startswith('sglang:num_running_reqs'): if line.startswith('sglang:num_running_reqs'):