Skip to content

Commit 1932bf8

Browse files
Refine connection-error reporting and throttling
1 parent c00ecc4 commit 1932bf8

1 file changed

Lines changed: 106 additions & 8 deletions

File tree

src/gabriel/utils/openai_utils.py

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ class StatusTracker:
200200
num_tasks_succeeded: int = 0
201201
num_tasks_failed: int = 0
202202
num_rate_limit_errors: int = 0
203+
num_connection_errors: int = 0
203204
num_api_errors: int = 0
204205
num_timeout_errors: int = 0
205206
num_other_errors: int = 0
@@ -1215,6 +1216,12 @@ def _rate_limit_decrement(concurrency_cap: int) -> int:
12151216
return max(2, int(math.ceil(max(concurrency_cap * 0.4, 3))))
12161217

12171218

1219+
def _connection_error_decrement(concurrency_cap: int) -> int:
1220+
"""Return a step-down size for connection errors matching rate-limit aggression."""
1221+
1222+
return _rate_limit_decrement(concurrency_cap)
1223+
1224+
12181225
def _smooth_wait_based_cap(
12191226
current_cap: int,
12201227
candidate_cap: int,
@@ -2904,6 +2911,7 @@ async def get_all_responses(
29042911
quiet: bool = False,
29052912
global_cooldown: int = 15,
29062913
rate_limit_window: float = 30.0,
2914+
connection_error_window: float = 30.0,
29072915
token_sample_size: int = 20,
29082916
status_report_interval: Optional[float] = 120.0,
29092917
planning_rate_limit_buffer: float = PLANNING_RATE_LIMIT_BUFFER,
@@ -2949,6 +2957,11 @@ async def get_all_responses(
29492957
throttling, while successful calls reset the counters and allow the pool to
29502958
scale back up.
29512959
2960+
Connection errors (e.g., transient network drops, Wi‑Fi/VPN instability, or bandwidth limitations)
2961+
are handled similarly: the helper tracks recent connection failures over
2962+
``connection_error_window`` seconds and reduces parallelism when repeated
2963+
failures occur, while logging a hint to check network stability.
2964+
29522965
Timeout bursts now scale with the active level of parallelism. The helper
29532966
triggers a protective restart only after observing roughly 1.25× the
29542967
current parallel worker count worth of timeouts within
@@ -4201,9 +4214,13 @@ def _append_results(rows: List[Dict[str, Any]]) -> None:
42014214
successes_since_adjust = 0
42024215
active_workers = 0
42034216
rate_limit_window = max(1.0, float(rate_limit_window))
4217+
connection_error_window = max(1.0, float(connection_error_window))
42044218
rate_limit_error_times: Deque[float] = deque()
42054219
last_concurrency_scale_down = 0.0
42064220
last_concurrency_scale_up = 0.0
4221+
connection_error_times: Deque[float] = deque()
4222+
connection_errors_since_adjust = 0
4223+
last_connection_scale_down = 0.0
42074224
usage_samples: List[Tuple[int, int, int]] = []
42084225
estimated_output_tokens = planning_output_tokens
42094226
estimated_output_tokens_per_prompt_live = float(planning_output_tokens)
@@ -4236,6 +4253,7 @@ def _effective_parallel_ceiling() -> int:
42364253
last_wait_adjust = 0.0
42374254
timeout_notes: Deque[str] = deque(maxlen=3)
42384255
timeout_errors_since_last_status = 0
4256+
connection_errors_since_last_status = 0
42394257
first_timeout_logged = False
42404258
current_tokens_per_call = float(estimated_tokens_per_call)
42414259
last_rate_limit_concurrency_change = 0.0
@@ -4454,6 +4472,7 @@ def emit_parallelization_status(
44544472
context="reporting parallelization status",
44554473
)
44564474
timeout_text = ""
4475+
connection_text = ""
44574476
total_completed = processed
44584477
if status.num_timeout_errors or total_completed:
44594478
denom = max(total_completed, 1)
@@ -4475,6 +4494,10 @@ def emit_parallelization_status(
44754494
total_cost, sampled = cost_snapshot
44764495
cost_text = f"cost_so_far={'~' if sampled else ''}${total_cost:.2f}"
44774496
prefix = f"[{label}] " if label else ""
4497+
if status.num_connection_errors or connection_errors_since_last_status:
4498+
connection_text = f"connection_errors={status.num_connection_errors}"
4499+
if status_report_interval is not None and connection_errors_since_last_status:
4500+
connection_text += f" (+{connection_errors_since_last_status} since last)"
44784501
status_bits: List[str] = [
44794502
f"cap={concurrency_cap}",
44804503
f"active={active_workers}",
@@ -4490,6 +4513,8 @@ def emit_parallelization_status(
44904513
status_bits.append(ppm_piece)
44914514
if timeout_text:
44924515
status_bits.append(timeout_text)
4516+
if connection_text:
4517+
status_bits.append(connection_text)
44934518
msg = f"{prefix}{timestamp} | {reason_clean}: " + ", ".join(status_bits)
44944519
if message_verbose:
44954520
print(msg)
@@ -4754,12 +4779,50 @@ def maybe_adjust_concurrency() -> None:
47544779
successes_since_adjust = 0
47554780
rate_limit_errors_since_adjust = 0
47564781

4782+
def maybe_adjust_for_connection_errors() -> None:
4783+
nonlocal concurrency_cap, connection_errors_since_adjust, last_connection_scale_down
4784+
nonlocal last_rate_limit_concurrency_change
4785+
now = time.time()
4786+
window_start = now - connection_error_window
4787+
while connection_error_times and connection_error_times[0] < window_start:
4788+
connection_error_times.popleft()
4789+
recent_errors = len(connection_error_times)
4790+
error_window_threshold = max(3, int(math.ceil(concurrency_cap * 0.06)))
4791+
consecutive_threshold = max(2, int(math.ceil(concurrency_cap * 0.04)))
4792+
should_scale_down = False
4793+
if recent_errors >= error_window_threshold:
4794+
should_scale_down = True
4795+
elif connection_errors_since_adjust >= consecutive_threshold:
4796+
should_scale_down = True
4797+
if should_scale_down and (
4798+
(now - last_connection_scale_down) >= max(1.0, connection_error_window * 0.75)
4799+
):
4800+
decrement = _connection_error_decrement(concurrency_cap)
4801+
new_cap = max(1, concurrency_cap - decrement)
4802+
if new_cap != concurrency_cap:
4803+
old_cap = concurrency_cap
4804+
concurrency_cap = new_cap
4805+
reason = (
4806+
f"[network recovery] Cutting workers from {old_cap} to {new_cap} "
4807+
f"after {recent_errors} connection errors in the last "
4808+
f"{int(round(connection_error_window))}s. "
4809+
"If this persists, check network stability or bandwidth limits, "
4810+
"or reduce `n_parallels`."
4811+
)
4812+
logger.warning(reason)
4813+
emit_parallelization_status(reason, force=True)
4814+
connection_errors_since_adjust = 0
4815+
connection_error_times.clear()
4816+
last_connection_scale_down = now
4817+
last_rate_limit_concurrency_change = now
4818+
47574819
async def worker() -> None:
47584820
nonlocal processed, call_count, nonlocal_timeout, active_workers, concurrency_cap, cooldown_until
47594821
nonlocal estimated_output_tokens, rate_limit_errors_since_adjust, successes_since_adjust, stop_event
47604822
nonlocal max_parallel_ceiling, last_wait_adjust, current_tokens_per_call, timeout_errors_since_last_status
47614823
nonlocal throughput_ceiling_ppm, observed_input_tokens_total, observed_output_tokens_total
4762-
nonlocal observed_reasoning_tokens_total, observed_usage_count
4824+
nonlocal observed_reasoning_tokens_total, observed_usage_count, connection_errors_since_adjust
4825+
nonlocal connection_errors_since_last_status
47634826
while True:
47644827
if stop_event.is_set():
47654828
break
@@ -4970,7 +5033,13 @@ async def _maybe_retry(backoff: float) -> bool:
49705033
if new_cap < 1:
49715034
new_cap = 1
49725035
now = time.time()
4973-
safe_to_increase = (now - status.time_of_last_rate_limit_error) >= rate_limit_window
5036+
last_connection_error = (
5037+
connection_error_times[-1] if connection_error_times else 0.0
5038+
)
5039+
safe_to_increase = (
5040+
(now - status.time_of_last_rate_limit_error) >= rate_limit_window
5041+
and (now - last_connection_error) >= connection_error_window
5042+
)
49745043
if new_cap > concurrency_cap:
49755044
max_increase = max(1, int(math.ceil(concurrency_cap * 0.12)))
49765045
if not safe_to_increase:
@@ -5148,6 +5217,7 @@ async def _maybe_retry(backoff: float) -> bool:
51485217
status.num_tasks_succeeded += 1
51495218
successes_since_adjust += 1
51505219
rate_limit_errors_since_adjust = 0
5220+
connection_errors_since_adjust = 0
51515221
maybe_adjust_concurrency()
51525222
_maybe_trigger_threshold_refresh()
51535223
if processed % save_every_x_responses == 0:
@@ -5241,13 +5311,26 @@ async def _maybe_retry(backoff: float) -> bool:
52415311
if error_detail:
52425312
base_message = f"{base_message}: {error_detail}"
52435313
error_logs[ident].append(error_detail)
5314+
if "connection error" in detail_lower:
5315+
status.num_connection_errors += 1
5316+
connection_errors_since_adjust += 1
5317+
connection_errors_since_last_status += 1
5318+
connection_error_times.append(time.time())
5319+
maybe_adjust_for_connection_errors()
5320+
_emit_first_error(
5321+
f"{base_message}. This can indicate network instability or bandwidth limitations.",
5322+
dedup_key=("connection-error", error_detail or None),
5323+
)
52445324
error_key: Hashable = (
52455325
"non-timeout-error",
52465326
type(e).__name__,
52475327
error_detail or None,
52485328
)
5249-
_emit_first_error(base_message, dedup_key=error_key)
5250-
logger.warning(base_message)
5329+
if "connection error" not in detail_lower:
5330+
_emit_first_error(base_message, dedup_key=error_key)
5331+
logger.warning(base_message)
5332+
else:
5333+
logger.debug(base_message)
52515334
if attempts_left - 1 > 0:
52525335
backoff = random.uniform(1, 2) * (2 ** (max_retries - attempts_left))
52535336
await _maybe_retry(backoff)
@@ -5412,9 +5495,17 @@ async def _maybe_retry(backoff: float) -> bool:
54125495
except APIConnectionError as e:
54135496
inflight.pop(ident, None)
54145497
status.num_api_errors += 1
5415-
logger.warning(f"Connection error for {ident}: {e}")
5416-
_emit_first_error(f"Connection error encountered: {e}")
5498+
status.num_connection_errors += 1
5499+
_emit_first_error(
5500+
f"Connection error encountered: {e}. This can indicate network instability or bandwidth limitations.",
5501+
dedup_key=("connection-error", str(e)),
5502+
)
5503+
logger.debug(f"Connection error for {ident}: {e}")
54175504
error_logs[ident].append(str(e))
5505+
connection_error_times.append(time.time())
5506+
connection_errors_since_adjust += 1
5507+
connection_errors_since_last_status += 1
5508+
maybe_adjust_for_connection_errors()
54185509
if attempts_left - 1 > 0 and not stop_event.is_set():
54195510
backoff = random.uniform(1, 2) * (2 ** (max_retries - attempts_left))
54205511
await _maybe_retry(backoff)
@@ -5516,7 +5607,7 @@ async def status_reporter() -> None:
55165607
if status_report_interval is None:
55175608
return
55185609
try:
5519-
nonlocal timeout_errors_since_last_status
5610+
nonlocal timeout_errors_since_last_status, connection_errors_since_last_status
55205611
while not stop_event.is_set():
55215612
await asyncio.sleep(status_report_interval)
55225613
if stop_event.is_set() or processed >= status.num_tasks_started:
@@ -5525,6 +5616,7 @@ async def status_reporter() -> None:
55255616
"Periodic status update", force=True, label=None
55265617
)
55275618
timeout_errors_since_last_status = 0
5619+
connection_errors_since_last_status = 0
55285620
except asyncio.CancelledError:
55295621
pass
55305622

@@ -5646,6 +5738,7 @@ async def status_reporter() -> None:
56465738
quiet=quiet,
56475739
global_cooldown=global_cooldown,
56485740
rate_limit_window=rate_limit_window,
5741+
connection_error_window=connection_error_window,
56495742
token_sample_size=token_sample_size,
56505743
status_report_interval=status_report_interval,
56515744
planning_rate_limit_buffer=planning_rate_limit_buffer,
@@ -5660,7 +5753,12 @@ async def status_reporter() -> None:
56605753
logger.warning(f"{status.num_tasks_failed} requests failed.")
56615754
if status.num_rate_limit_errors > 0:
56625755
logger.warning(
5663-
f"{status.num_rate_limit_errors} rate limit errors encountered; consider reducing concurrency."
5756+
f"{status.num_rate_limit_errors} rate limit errors encountered; consider reducing concurrency via lower n_parallels."
5757+
)
5758+
if status.num_connection_errors > 0:
5759+
logger.warning(
5760+
f"{status.num_connection_errors} API connection errors encountered, indicating network instability or bandwidth limitations; "
5761+
"consider reducing concurrency via lower n_parallels."
56645762
)
56655763
if status.num_timeout_errors > 0:
56665764
logger.warning(f"{status.num_timeout_errors} timeouts encountered.")

0 commit comments

Comments
 (0)