Files
chongming-core/test/test_scale_instance_distribution.py
T

187 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
外部进程多实例全局分摊 —— distribute_quota 逻辑单测(无需网关/缓存后端)。
用 dict 作缓存桩,模拟同类型多个外部进程实例注册,验证扩缩容时全局 count 按
「存活实例数 N」分摊(而非每个进程都全额),以及实例退出/租约过期后的接管行为。
"""
import os
import sys
import time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../utils/worker/src"))
import chongming_worker.runtime as r
PASS = 0
FAIL = 0
def report(name, cond, detail=""):
global PASS, FAIL
if cond:
PASS += 1
print(f"✅ {name}" + (f" — {detail}" if detail else ""))
else:
FAIL += 1
print(f"❌ {name}" + (f" — {detail}" if detail else ""))
def fake_cache():
return {}
def as_instance(iid, cache, worker_type="demo"):
"""把当前身份设为 iid,并在缓存登记该实例参与方。"""
old = os.environ.get("CHONGMING_WORKER_INSTANCE")
os.environ["CHONGMING_WORKER_INSTANCE"] = iid
try:
r.register_instance(worker_type, cache)
finally:
if old is None:
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
else:
os.environ["CHONGMING_WORKER_INSTANCE"] = old
def quota_as(iid, worker_type, total, cache):
"""以指定实例身份计算其分摊配额(external 分支)。"""
old = os.environ.get("CHONGMING_WORKER_INSTANCE")
os.environ["CHONGMING_WORKER_INSTANCE"] = iid
try:
return r.distribute_quota(cache, worker_type, total, gw_id=None)
finally:
if old is None:
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
else:
os.environ["CHONGMING_WORKER_INSTANCE"] = old
def unreg_instance(iid, cache, worker_type="demo"):
"""以指定实例身份从参与方列表移除它。"""
old = os.environ.get("CHONGMING_WORKER_INSTANCE")
os.environ["CHONGMING_WORKER_INSTANCE"] = iid
try:
r.unregister_instance(worker_type, cache)
finally:
if old is None:
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
else:
os.environ["CHONGMING_WORKER_INSTANCE"] = old
def run():
cache = fake_cache()
wt = "demo"
# 1. 单实例(未启用实例注册,参与方为空)→ 兜底全额(兼容旧行为)
q = r.distribute_quota(cache, wt, 6, gw_id=None)
report("单实例无参与方 → 全额承担 6", q == 6, f"got={q}")
# 2. 两个实例并存:全局 6 分摊 → 各 3
as_instance("inst-a", cache, wt)
as_instance("inst-b", cache, wt)
qa = quota_as("inst-a", wt, 6, cache)
qb = quota_as("inst-b", wt, 6, cache)
report("两个实例并存 → 各分 3", qa == 3 and qb == 3, f"a={qa}, b={qb}")
# 3. 三个实例、总数不能整除:7 // 3 余 1 → 按序给第一个补 1
as_instance("inst-c", cache, wt)
qs = {i: quota_as(i, wt, 7, cache) for i in ("inst-a", "inst-b", "inst-c")}
report(
"三个实例 ≠整除 → 总和=7 且余量补靠前实例",
sum(qs.values()) == 7 and set(qs.values()) == {2, 2, 3},
f"{qs}",
)
# 4. 一个实例退出(unregister)→ 其余接管全部
unreg_instance("inst-c", cache, wt)
qa2 = quota_as("inst-a", wt, 6, cache)
qb2 = quota_as("inst-b", wt, 6, cache)
report(
"实例退出后由剩余接管 → 各分 3",
qa2 == 3 and qb2 == 3,
f"a={qa2}, b={qb2}",
)
# 5. 租约过期清理:伪造一个过期的参与方,读实例列表应排除它
cache[r._SCALE_INSTANCES_KEY.format(type=wt)] = [
["inst-stale", time.time() - r._INSTANCE_LEASE - 10],
["inst-fresh", time.time()],
]
ids = r._read_instances(cache, wt)
report(
"死实例按租约剔除",
ids == ["inst-fresh"],
f"instances={ids}",
)
# 6. 实例数=1 时全额(不误分摊)
cache[r._SCALE_INSTANCES_KEY.format(type=wt)] = [
[r.current_instance_id(), time.time()]
]
q1 = r.distribute_quota(cache, wt, 5, gw_id=None)
report("单存活实例 → 全额 5", q1 == 5, f"got={q1}")
# 7. inline(有网关身份)路径回归:按在线网关分摊不受影响
cache[r._SCALE_GATEWAYS_KEY] = [["gw-a", time.time()], ["gw-b", time.time()]]
qa_in = r.distribute_quota(cache, wt, 6, "gw-a")
qb_in = r.distribute_quota(cache, wt, 6, "gw-b")
report(
"inline 按网关分摊回归 → 各 3",
qa_in == 3 and qb_in == 3,
f"gw-a={qa_in}, gw-b={qb_in}",
)
# 8. 逐实例绝对覆盖:写入后能被读回且供启动解析(external)使用
r._write_own_instance_count(cache, wt, 4, "inst-a")
r._write_own_instance_count(cache, wt, 2, "inst-b")
report(
"instcounts 覆盖写入/读回",
r._own_instance_count(cache, wt, "inst-a") == 4
and r._own_instance_count(cache, wt, "inst-b") == 2,
f"{r._read_instance_counts(cache, wt)}",
)
# 启动解析:external(gw_id=None)有绝对覆盖时直接采用,而非被全局均分
old = os.environ.get("CHONGMING_WORKER_INSTANCE")
os.environ["CHONGMING_WORKER_INSTANCE"] = "inst-a"
try:
restored = r.resolve_worker_count(wt, 6, cache, "cache", gw_id=None)
finally:
if old is None:
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
else:
os.environ["CHONGMING_WORKER_INSTANCE"] = old
report("resolve_worker_count 启动时采用本实例绝对数", restored == 4, f"got={restored}")
# 9. 清除覆盖后回退全局均分
r._clear_instance_counts(cache, wt)
report(
"清除覆盖后覆盖表为空",
r._read_instance_counts(cache, wt) == {},
f"{r._read_instance_counts(cache, wt)}",
)
# 10. 缺省实例身份唯一(hostname:pid):同机未指定 --instance 的多进程不会并成一个参与方
r.set_current_instance_id(None)
old_env = os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
try:
default_iid = r.current_instance_id()
finally:
if old_env is not None:
os.environ["CHONGMING_WORKER_INSTANCE"] = old_env
else:
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
import socket as _socket
report(
"缺省实例身份含进程区分({host}:{pid})",
default_iid == f"{_socket.gethostname()}:{os.getpid()}",
f"got={default_iid}",
)
print("\n" + "=" * 56)
print(f"测试结果: ✅ {PASS} 通过 ❌ {FAIL} 失败")
return 0 if FAIL == 0 else 1
if __name__ == "__main__":
sys.exit(run())