fix: 修复自适应线程池限流扩容及缩容滞后

This commit is contained in:
2026-09-11 15:59:46 +08:00
parent 13f72178e9
commit f3faad0391
4 changed files with 262 additions and 179 deletions
+120 -20
View File
@@ -5,6 +5,7 @@
(通过注入假时钟做确定性验证)。
"""
import threading
import time
from nodes.adaptive_pool import AdaptiveThreadPool, decide
@@ -76,19 +77,21 @@ def test_pool_progress_reports_window_avg_after_first_window() -> None:
"""
clock = FakeClock()
seen: list[tuple[int, int, float, float, int]] = []
def progress(done, total, rate, avg_time, workers):
# 推进窗口时钟但不增加 worker 耗时,确定性触发快响应扩容。
seen.append((done, total, rate, avg_time, workers))
if done == 1:
clock.advance(11)
pool = AdaptiveThreadPool(
worker=lambda item: item,
min_workers=1, max_workers=16,
window_seconds=10.0, fast_threshold=0.3,
clock=clock,
on_progress=lambda done, total, rate, avg_time, workers: seen.append(
(done, total, rate, avg_time, workers)
),
clock=clock, on_progress=progress,
)
clock.advance(11) # 首个任务完成即越过窗口 → 触发评估。
pool.map(list(range(5)))
# 第一个完成的任务回调在窗口评估前:回退累计平均(>0
assert seen[0][3] > 0
# 第一个完成的任务回调在窗口评估前:真实 worker 耗时为 0,均值也为 0
assert seen[0][3] == 0.0
# 窗口评估后:回调携带最近窗口平均(≈0.0),且线程已扩容到 2。
assert any(item[3] == 0.0 for item in seen)
assert any(item[4] == 2 for item in seen)
@@ -165,9 +168,9 @@ def test_pool_grows_when_fast() -> None:
min_workers=1, max_workers=16,
window_seconds=10.0, fast_threshold=0.3,
clock=clock,
on_progress=lambda done, *_: clock.advance(11) if done == 1 else None,
)
# 拨快时钟越过窗口:首个任务完成即触发评估 → 平均响应≈0 < 0.3 → +1 线程
clock.advance(11)
# 首个任务完成后越过窗口 → 平均响应≈0 < 0.3 → +1 个在途额度
pool.map(list(range(4)))
assert pool.max_concurrency == 2
@@ -184,11 +187,8 @@ def test_pool_shrink_when_slow() -> None:
pool._resize(2) # 先扩到 2 个线程。
clock.advance(11)
pool._tick(2.0) # 窗口内平均 2.0 > 1.0 → 缩回 1。
deadline = time.monotonic() + 2
while len(pool._threads) > 1 and time.monotonic() < deadline:
time.sleep(0.01)
assert len(pool._threads) == 1
pool._stop.set()
# 缩容只调整提交额度;实际积压任务行为由下方并发回归测试验证。
assert pool._target_workers == 1
def test_resize_shrink_idempotent() -> None:
@@ -199,12 +199,8 @@ def test_resize_shrink_idempotent() -> None:
pool._resize(2)
pool._resize(2) # 目标已是 2:幂等,不再放哨兵。
assert pool._target_workers == 2
# 只有 1 个线程被哨兵退出,最终存活 2 个
deadline = time.monotonic() + 2
while len(pool._threads) > 2 and time.monotonic() < deadline:
time.sleep(0.01)
assert len(pool._threads) == 2
pool._stop.set()
# 重复缩容不会遗留哨兵影响下一批,真实 map 必须返回全部输入
assert pool.map(list(range(20))) == list(range(20))
def test_pool_survives_mixed_grow_shrink() -> None:
@@ -259,6 +255,110 @@ def test_pool_effective_max_recovers_after_clean_window() -> None:
assert pool._effective_max_workers == 16
def test_failure_at_single_worker_never_expands() -> None:
"""真实 map 内报告限流:1 个在途任务不能因上限 20 变 19 而突然扩容。"""
targets = []
def worker(item):
if item == 0:
pool.report_failure()
targets.append(pool._target_workers)
return item
pool = AdaptiveThreadPool(worker=worker, max_workers=20, window_seconds=1000)
assert pool.map(list(range(30))) == list(range(30))
assert targets == [1]
def test_failure_shrink_limits_backlogged_work() -> None:
"""积压任务中从 4 降到 1:已开始任务可完成,后续实际并发必须为 1。"""
barrier = threading.Barrier(4, timeout=3)
reduced = threading.Event()
lock = threading.Lock()
active = 0
subsequent_peaks = []
def worker(item):
nonlocal active
with lock:
active += 1
if item >= 5:
subsequent_peaks.append(active)
try:
if 1 <= item <= 4:
barrier.wait()
if item == 1:
# 连续限流将有效上限压到下限,存量请求不强制中断。
for _ in range(3):
pool.report_failure()
reduced.set()
assert reduced.wait(3)
elif item >= 5:
# 模拟 I/O 等待,给其他工作线程实际进入任务的机会。
time.sleep(0.005)
return item
finally:
with lock:
active -= 1
def progress(done, *_):
if done == 1:
pool._resize(4)
pool = AdaptiveThreadPool(worker=worker, max_workers=4, window_seconds=1000,
on_progress=progress)
items = list(range(40))
assert pool.map(items) == items
assert subsequent_peaks and max(subsequent_peaks) == 1
def test_error_window_does_not_regrow() -> None:
"""报告限流的同一窗口即使响应很快,也不能重新加并发。"""
clock = FakeClock()
pool = AdaptiveThreadPool(worker=lambda item: item, max_workers=4, clock=clock)
pool.report_failure()
clock.advance(11)
pool._tick(0.01)
assert pool._target_workers <= 1
def test_retry_map_preserves_reduced_limit() -> None:
"""两轮真实 map:首轮连续限流后,重试期间实际并发及进度都遵守新上限。"""
lock = threading.Lock()
active = 0
peak = 0
progress_counts = []
def worker(item):
nonlocal active, peak
with lock:
active += 1
peak = max(peak, active)
try:
if item == "limited":
for _ in range(3):
pool.report_failure()
raise RuntimeError("rate limited")
time.sleep(0.002)
return item
finally:
with lock:
active -= 1
pool = AdaptiveThreadPool(
worker=worker, max_workers=4, window_seconds=1000,
on_progress=lambda done, *_: progress_counts.append(done),
)
assert isinstance(pool.map(["limited"])[0], RuntimeError)
assert pool._effective_max_workers == 1
# 主动申请扩容也受已收紧额度约束,下一轮不能重置有效上限。
pool._resize(4)
assert pool._target_workers == 1
assert pool.map(list(range(12))) == list(range(12))
assert peak == 1
assert progress_counts == [1] + list(range(1, 13))
def test_pool_decide_uses_effective_max() -> None:
"""扩容上限按有效最大线程数:错误窗口内即使响应快也不超过收紧后的上限。"""
clock = FakeClock()