feat(gateway): worker 池主备分组派发,主全挂才用备机
- pool.py: WorkerState 增加 primary 标记;acquire_worker 分级派发—— 主组有在线只在主组内挑空闲(全忙排队,不退备组),主组全部下线才用备机 - config.py: 新增可选 primary_workers 配置(须为 workers 子集,自动过滤+告警); 不配置/空列表 = 全部视为主 worker,兼容旧行为 - config.json(不入 git): 3 台 GPU worker 设为主,max_global_concurrency 1→3 - app.py: /health 增加 workers_primary_total / workers_primary_healthy - tests: 新增 4 条主备派发单测(主优先/主全挂用备/主全忙不走备/无配置兼容) - docs(实现说明): 补充 primary_workers 配置说明,改配置需重启网关生效
This commit is contained in:
@@ -152,3 +152,85 @@ def test_cooldown_seconds_falls_back_to_60(monkeypatch):
|
||||
from gateway import config as gw_config
|
||||
monkeypatch.setattr(gw_config, "_config_cache", None) # get_config 会抛 RuntimeError
|
||||
assert pool._cooldown_seconds() == 60.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 主备分组派发(primary_workers)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_DISPATCH_CFG = {
|
||||
"dispatch": {
|
||||
"queue_wait_seconds": 0.05, # 测试用:快速超时
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _make_pool(specs):
|
||||
"""specs: [(url, online, busy, primary), ...] → 构造 pool._workers。"""
|
||||
return {
|
||||
url: WorkerState(url=url, online=online, busy=busy, primary=primary)
|
||||
for url, online, busy, primary in specs
|
||||
}
|
||||
|
||||
|
||||
def _run_acquire(workers):
|
||||
async def run():
|
||||
pool._pool_condition = asyncio.Condition()
|
||||
orig = pool._workers
|
||||
pool._workers = workers
|
||||
try:
|
||||
return await pool.acquire_worker(_DISPATCH_CFG)
|
||||
finally:
|
||||
pool._workers = orig
|
||||
return asyncio.run(run())
|
||||
|
||||
|
||||
def test_primary_preferred_over_backup():
|
||||
"""主 worker 在线时空闲主优先于备 worker。"""
|
||||
workers = _make_pool([
|
||||
("http://backup:1", True, False, False),
|
||||
("http://primary:1", True, False, True),
|
||||
])
|
||||
w = _run_acquire(workers)
|
||||
assert w.url == "http://primary:1"
|
||||
assert w.busy is True
|
||||
|
||||
|
||||
def test_backup_used_only_when_all_primary_down():
|
||||
"""主 worker 全部下线时才用备 worker。"""
|
||||
workers = _make_pool([
|
||||
("http://backup:1", True, False, False),
|
||||
("http://primary:1", False, False, True),
|
||||
("http://primary:2", False, False, True),
|
||||
])
|
||||
w = _run_acquire(workers)
|
||||
assert w.url == "http://backup:1"
|
||||
|
||||
|
||||
def test_backup_not_used_when_primary_busy():
|
||||
"""主 worker 在线但全忙时不退到备 worker(等待后超时拒绝)。"""
|
||||
workers = _make_pool([
|
||||
("http://backup:1", True, False, False),
|
||||
("http://primary:1", True, True, True),
|
||||
])
|
||||
|
||||
async def run():
|
||||
pool._pool_condition = asyncio.Condition()
|
||||
try:
|
||||
await pool.acquire_worker(_DISPATCH_CFG)
|
||||
except pool.NoWorkerAvailable:
|
||||
return True
|
||||
return False
|
||||
|
||||
assert asyncio.run(run()) is True, "主组全忙时不得使用备 worker"
|
||||
assert workers["http://backup:1"].busy is False
|
||||
|
||||
|
||||
def test_no_primary_config_all_are_primary():
|
||||
"""未配置 primary_workers 时全部视为主 worker(兼容旧行为)。"""
|
||||
workers = _make_pool([
|
||||
("http://w1:1", True, False, True),
|
||||
("http://w2:1", True, False, True),
|
||||
])
|
||||
w = _run_acquire(workers)
|
||||
assert w.url == "http://w1:1"
|
||||
|
||||
Reference in New Issue
Block a user