|
24 | 24 | import time |
25 | 25 | import logging |
26 | 26 | import requests |
27 | | -from concurrent.futures import ThreadPoolExecutor, as_completed |
| 27 | +from concurrent.futures import ThreadPoolExecutor, as_completed, wait, FIRST_COMPLETED |
28 | 28 | from dataclasses import dataclass |
29 | 29 | from requests.adapters import HTTPAdapter |
30 | 30 | from typing import Callable, Literal, Optional |
@@ -2114,31 +2114,41 @@ def _do_group(idxs): |
2114 | 2114 | return idxs, translations |
2115 | 2115 |
|
2116 | 2116 | executor = ThreadPoolExecutor(max_workers=max_concurrent) |
2117 | | - futures = [executor.submit(_do_group, g) for g in groups] |
| 2117 | + pending = list(groups) |
| 2118 | + futures = {} |
2118 | 2119 | try: |
2119 | | - for future in as_completed(futures): |
2120 | | - try: |
2121 | | - idxs, translations = future.result() |
2122 | | - except WorkBudgetExceeded as exc: |
2123 | | - if exc.reason == "cancelled": |
2124 | | - with fatal_lock: |
2125 | | - protocol_error = fatal_protocol_error[0] |
2126 | | - if protocol_error is not None: |
2127 | | - raise protocol_error |
2128 | | - raise |
2129 | | - for j, idx in enumerate(idxs): |
2130 | | - # Each entry carries the provider that ACTUALLY served it |
2131 | | - # (the fallback provider when the primary failed). |
2132 | | - results[idx] = ( |
2133 | | - translations[j] |
2134 | | - if j < len(translations) |
2135 | | - else BatchTranslationItem( |
2136 | | - "[TRANSLATION ERROR: missing segment]", |
2137 | | - "", |
2138 | | - False, |
2139 | | - "failed", |
| 2120 | + # Bounded window: never hold more than max_concurrent futures at once. |
| 2121 | + # Previously every group was submitted upfront, so 8 concurrent API |
| 2122 | + # requests could park dozens of threads on the 2s upstream semaphore. |
| 2123 | + while pending or futures: |
| 2124 | + while pending and len(futures) < max_concurrent: |
| 2125 | + g = pending.pop(0) |
| 2126 | + futures[executor.submit(_do_group, g)] = g |
| 2127 | + done, _ = wait(list(futures), return_when=FIRST_COMPLETED) |
| 2128 | + for future in done: |
| 2129 | + idxs = futures.pop(future) |
| 2130 | + try: |
| 2131 | + idxs, translations = future.result() |
| 2132 | + except WorkBudgetExceeded as exc: |
| 2133 | + if exc.reason == "cancelled": |
| 2134 | + with fatal_lock: |
| 2135 | + protocol_error = fatal_protocol_error[0] |
| 2136 | + if protocol_error is not None: |
| 2137 | + raise protocol_error |
| 2138 | + raise |
| 2139 | + for j, idx in enumerate(idxs): |
| 2140 | + # Each entry carries the provider that ACTUALLY served it |
| 2141 | + # (the fallback provider when the primary failed). |
| 2142 | + results[idx] = ( |
| 2143 | + translations[j] |
| 2144 | + if j < len(translations) |
| 2145 | + else BatchTranslationItem( |
| 2146 | + "[TRANSLATION ERROR: missing segment]", |
| 2147 | + "", |
| 2148 | + False, |
| 2149 | + "failed", |
| 2150 | + ) |
2140 | 2151 | ) |
2141 | | - ) |
2142 | 2152 | except (SegmentProtocolError, WorkBudgetExceeded): |
2143 | 2153 | for future in futures: |
2144 | 2154 | future.cancel() |
|
0 commit comments