feat(gateway): 接口5排队插队优先级,两级队列均支持

- 全局槽换为 _PrioritySemaphore:priority 大者排到等待队列最前,同级 FIFO,
  无等待者时才允许直接获取(收紧 asyncio.Semaphore 的 barging);
  授予槽位瞬间请求被取消时归还槽位,防泄漏
- worker 池 Condition FIFO 等待改为显式优先级等待队列,release 唤醒队首;
  被唤醒但 worker 被抢时按原序号重排;授予/取消竞态时转手唤醒下一位
- forward.py 新增 PRIORITY_PATHS(现仅接口5 /api/v1/hairline/generate)
- 新增 4 个单测:两级队列的插队 + 同级 FIFO
This commit is contained in:
xsl
2026-09-23 22:24:21 +08:00
parent 6f46fc0ccd
commit 3350e0ad77
3 changed files with 271 additions and 37 deletions
+115
View File
@@ -234,3 +234,118 @@ def test_no_primary_config_all_are_primary():
])
w = _run_acquire(workers)
assert w.url == "http://w1:1"
# ---------------------------------------------------------------------------
# 优先级插队(接口5):全局槽 + worker 池
# ---------------------------------------------------------------------------
_PRIO_CFG = {
"dispatch": {
"queue_wait_seconds": 5, # 测试用:等待足够长,避免排队超时
"max_global_concurrency": 1,
"max_queue_wait_seconds": 5,
}
}
def test_global_slot_priority_jumps_queue():
"""普通请求已排队时,优先级请求插队到最前,先获得槽位。"""
pool.init_global_slot(_PRIO_CFG)
order = []
async def run():
assert await pool.acquire_global_slot(_PRIO_CFG) is True # 占住唯一槽
async def normal():
if await pool.acquire_global_slot(_PRIO_CFG):
order.append("normal")
pool.release_global_slot()
async def prio():
if await pool.acquire_global_slot(_PRIO_CFG, priority=1):
order.append("prio")
pool.release_global_slot()
t1 = asyncio.create_task(normal())
await asyncio.sleep(0.05) # 确保 normal 先进入等待队列
t2 = asyncio.create_task(prio())
await asyncio.sleep(0.05) # prio 此时应已插队到队首
pool.release_global_slot() # 释放槽位 → 应唤醒 prio 而非 normal
await asyncio.wait_for(asyncio.gather(t1, t2), timeout=3)
asyncio.run(run())
assert order == ["prio", "normal"], f"优先级请求应先获得槽位,实际顺序={order}"
assert pool.get_pool_status()["global_busy"] == 0
def test_global_slot_same_priority_fifo():
"""同优先级仍按 FIFO,不插队。"""
pool.init_global_slot(_PRIO_CFG)
order = []
async def run():
assert await pool.acquire_global_slot(_PRIO_CFG) is True
async def waiter(tag):
if await pool.acquire_global_slot(_PRIO_CFG):
order.append(tag)
pool.release_global_slot()
tasks = []
for tag in ("a", "b", "c"):
tasks.append(asyncio.create_task(waiter(tag)))
await asyncio.sleep(0.05) # 保证入队顺序
pool.release_global_slot()
await asyncio.wait_for(asyncio.gather(*tasks), timeout=3)
asyncio.run(run())
assert order == ["a", "b", "c"], f"同优先级应保持 FIFO,实际顺序={order}"
def _run_worker_prio_scenario(acquire_specs):
"""构造「唯一 worker 忙 → 多个请求排队 → 释放」场景,返回获得顺序。
acquire_specs: [(tag, priority), ...],按此顺序发起请求。
"""
workers = _make_pool([
("http://w1:1", True, True, True), # 唯一 worker,初始即忙
])
order = []
async def run():
pool._pool_condition = asyncio.Condition()
pool._worker_waiters.clear()
orig = pool._workers
pool._workers = workers
try:
async def waiter(tag, prio_):
w = await pool.acquire_worker(_PRIO_CFG, priority=prio_)
order.append(tag)
await pool.release_worker(w)
tasks = []
for tag, prio_ in acquire_specs:
tasks.append(asyncio.create_task(waiter(tag, prio_)))
await asyncio.sleep(0.05) # 保证入队顺序
# 释放 worker → 唤醒优先级最高的等待者
await pool.release_worker(workers["http://w1:1"])
await asyncio.wait_for(asyncio.gather(*tasks), timeout=3)
finally:
pool._workers = orig
pool._worker_waiters.clear()
asyncio.run(run())
return order
def test_worker_queue_priority_jumps_queue():
"""普通请求已排队时,优先级请求插队到最前,先获得 worker。"""
order = _run_worker_prio_scenario([("normal", 0), ("prio", 1)])
assert order == ["prio", "normal"], f"优先级请求应先获得 worker,实际顺序={order}"
def test_worker_queue_same_priority_fifo():
"""同优先级仍按 FIFO,不插队。"""
order = _run_worker_prio_scenario([("a", 0), ("b", 0), ("c", 0)])
assert order == ["a", "b", "c"], f"同优先级应保持 FIFO,实际顺序={order}"