fix: 修复自适应线程池限流扩容及缩容滞后
This commit is contained in:
+120
-20
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user