merge: W-CFG-B NacosHotConfig 接进 genconfig(阶段二步骤4,fable 终审:hash/父链/diff 亲读/126 全量复跑绿)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
344330a48c
147
tier2/gen-worker/scripts/demo_nacos_hot_read.py
Normal file
147
tier2/gen-worker/scripts/demo_nacos_hot_read.py
Normal file
@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""demo_nacos_hot_read.py —— 真 Nacos 热读端到端演示(配置控制面阶段二·步骤4 证据脚本)。
|
||||
|
||||
【这个脚本证明什么】
|
||||
在一个进程内同时扮演「写侧」(模拟 yudao 激活编排 publish 生效 dataId)与「读侧」(worker 起本工厂
|
||||
接 Nacos 热源),坐实设计 §4 步骤4「验证」头一条:改 Nacos 生效 dataId → worker 数秒内经 genconfig
|
||||
读到新预算/阈值,不重启。流程:
|
||||
1. 起读侧工厂(启用旗打开 + 指向【演示专用 dataId】,绝不碰生产 gen-hot-params-tier2/cheap);
|
||||
2. 写侧 publish {budget.rmb_hard_limit: 33.0} → 断言 genconfig 数秒内热读到 33.0;
|
||||
3. 改值 publish 44.0 → 断言再热读到 44.0;
|
||||
4. 清理:删除演示 dataId、detach 读侧、验证回落本地。
|
||||
|
||||
【不动 live】只碰演示专用 dataId(gen-hot-params-demo),不部署、不重启任何 live 进程。
|
||||
|
||||
【内网铁律】Nacos 走 HTTP 直连内网 100.64.0.8:8848,必须在建 client 前把 host 塞进 NO_PROXY 绕系统
|
||||
fake-ip 代理(工厂内已做;本脚本的独立 publish client 也自旁路一次)。凭据从 env 读(NACOS_PASSWORD),
|
||||
权威副本在 docs/内网凭据与端点.md;脚本不硬编码口令。
|
||||
|
||||
跑法(在 gen-worker/ 下,主仓解释器):
|
||||
NACOS_PASSWORD=<读凭据档> PYTHONPATH=tier2/gen-worker \
|
||||
cheap-worker/.venv/bin/python tier2/gen-worker/scripts/demo_nacos_hot_read.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
# 包内/直跑兼容:把 gen-worker/ 加进 sys.path(PYTHONPATH 已指 gen-worker 时这步冗余保险)。
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_GW = os.path.dirname(_HERE) # gen-worker/
|
||||
if _GW not in sys.path:
|
||||
sys.path.insert(0, _GW)
|
||||
|
||||
# 演示专用 dataId(绝不碰生产 gen-hot-params-tier2 / gen-hot-params-cheap)。
|
||||
DEMO_DATA_ID = "gen-hot-params-demo"
|
||||
GROUP = "DEFAULT_GROUP"
|
||||
AREA, KEY = "budget", "rmb_hard_limit"
|
||||
COMPOSITE = f"{AREA}.{KEY}" # genconfig 热源复合键口径
|
||||
|
||||
|
||||
def _bypass_proxy(host: str) -> None:
|
||||
"""把 host 并进 NO_PROXY/no_proxy(建 publish client 前;与工厂/nacos_registry 同款内网旁路)。"""
|
||||
for k in ("NO_PROXY", "no_proxy"):
|
||||
cur = os.environ.get(k, "")
|
||||
segs = [s.strip() for s in cur.split(",") if s.strip()]
|
||||
if host not in segs:
|
||||
segs.append(host)
|
||||
os.environ[k] = ",".join(segs)
|
||||
|
||||
|
||||
def _wait_until(pred, timeout: float = 20.0, interval: float = 0.5):
|
||||
"""轮询等 pred() 为真;返回耗时秒,超时返 None。用于等长轮询推送到达。"""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
if pred():
|
||||
return time.time() - start
|
||||
time.sleep(interval)
|
||||
return None
|
||||
|
||||
|
||||
def _read() -> float:
|
||||
"""当前 genconfig 读到的 budget.rmb_hard_limit(经热源→env→YAML→内置默认取值链)。"""
|
||||
from worker import genconfig
|
||||
return genconfig.get(AREA, KEY, 50.0)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
nacos_addr = os.environ.get("NACOS_SERVER_ADDR", "100.64.0.8:8848")
|
||||
host = nacos_addr.split(":")[0]
|
||||
_bypass_proxy(host)
|
||||
|
||||
if not os.environ.get("NACOS_PASSWORD"):
|
||||
print("[demo] 缺 NACOS_PASSWORD(读 docs/内网凭据与端点.md,export 后重跑)", flush=True)
|
||||
return 2
|
||||
|
||||
# 读侧工厂:启用旗打开 + 指向演示专用 dataId(env 覆盖档默认)。
|
||||
os.environ["TIER2_GENCONFIG_NACOS"] = "1"
|
||||
os.environ["TIER2_GENCONFIG_NACOS_DATAID"] = DEMO_DATA_ID
|
||||
|
||||
import nacos # noqa: PLC0415
|
||||
from worker import genconfig, genconfig_nacos # noqa: PLC0415
|
||||
|
||||
# 写侧独立 publish client(模拟 yudao 激活编排的写侧;鉴权 + 内网旁路已就绪)。
|
||||
pub = nacos.NacosClient(
|
||||
nacos_addr,
|
||||
namespace=os.environ.get("NACOS_NAMESPACE", ""), # 默认公共空间空串(勿写 public)
|
||||
username=os.environ.get("NACOS_USERNAME", "nacos"),
|
||||
password=os.environ["NACOS_PASSWORD"],
|
||||
)
|
||||
# 清掉可能的旧演示残留,保证从「空 dataId」起步。
|
||||
try:
|
||||
pub.remove_config(DEMO_DATA_ID, GROUP)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[demo] 预清理(忽略):{type(e).__name__}: {e}", flush=True)
|
||||
|
||||
baseline = _read()
|
||||
print(f"[demo] 纯本地基线 {COMPOSITE} = {baseline}(未接热源前的 YAML/内置默认)", flush=True)
|
||||
|
||||
hot = genconfig_nacos.build_and_attach_hot_source(tier="tier2")
|
||||
if hot is None:
|
||||
print("[demo] FAIL 工厂未 attach(旗未开或构造失败)", flush=True)
|
||||
return 1
|
||||
print(f"[demo] 读侧已订阅 dataId={DEMO_DATA_ID};演示 dataId 现为空 → genconfig 回落本地 = {_read()}", flush=True)
|
||||
|
||||
# ① publish 33.0 → 断言数秒内热读到。
|
||||
v1 = 33.0
|
||||
ok1 = pub.publish_config(DEMO_DATA_ID, GROUP, json.dumps({COMPOSITE: v1}))
|
||||
print(f"[demo] 写侧 publish {COMPOSITE}={v1}(publish_config→{ok1}),等热读推送…", flush=True)
|
||||
took1 = _wait_until(lambda: _read() == v1)
|
||||
if took1 is None:
|
||||
print(f"[demo] FAIL 超时未热读到 {v1}(当前 {_read()})", flush=True)
|
||||
pub.remove_config(DEMO_DATA_ID, GROUP)
|
||||
return 1
|
||||
print(f"[demo] OK genconfig {took1:.2f}s 内热读到 {COMPOSITE}={_read()}(worker 未重启)", flush=True)
|
||||
|
||||
# ② 改值 44.0 → 再断言热读到。
|
||||
v2 = 44.0
|
||||
pub.publish_config(DEMO_DATA_ID, GROUP, json.dumps({COMPOSITE: v2}))
|
||||
print(f"[demo] 写侧改值 publish {COMPOSITE}={v2},等热读推送…", flush=True)
|
||||
took2 = _wait_until(lambda: _read() == v2)
|
||||
if took2 is None:
|
||||
print(f"[demo] FAIL 超时未热读到改后值 {v2}(当前 {_read()})", flush=True)
|
||||
pub.remove_config(DEMO_DATA_ID, GROUP)
|
||||
return 1
|
||||
print(f"[demo] OK genconfig {took2:.2f}s 内热读到改后值 {COMPOSITE}={_read()}", flush=True)
|
||||
|
||||
# ③ 清理:删演示 dataId + detach 读侧 + 验证回落本地。
|
||||
pub.remove_config(DEMO_DATA_ID, GROUP)
|
||||
genconfig_nacos.detach()
|
||||
genconfig.reload()
|
||||
print(f"[demo] 已删除演示 dataId={DEMO_DATA_ID}、detach 读侧;detach 后 {COMPOSITE} 回落本地 = {_read()}", flush=True)
|
||||
print("[demo] 演示成功:改 Nacos 生效 dataId → genconfig 数秒热读到新值、不重启;清理完毕、未碰生产 dataId。", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_rc = main()
|
||||
# nacos v1 SDK 的后台守护轮询线程(_do_pulling / _process_polling_result)在进程退出时会因其
|
||||
# multiprocessing 队列被关闭抛 EOFError —— 这是 SDK 的 teardown 噪声,与演示结果无关(所有断言在此
|
||||
# 之前已打印 OK),且生产 worker 是 uvicorn 长驻进程、不退出、根本不触发。此处 flush 后 os._exit
|
||||
# 干净终止、跳过该 daemon 线程的 teardown,让证据输出无歧义。
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
336
tier2/gen-worker/tests/test_genconfig_nacos.py
Normal file
336
tier2/gen-worker/tests/test_genconfig_nacos.py
Normal file
@ -0,0 +1,336 @@
|
||||
"""test_genconfig_nacos.py —— genconfig 接 Nacos 热源(配置控制面阶段二·步骤4)单测。
|
||||
|
||||
全程 mock / fake,不触真 Nacos。锁死设计 §4 步骤4「验证」要求的四条异常/回落路径,外加两档隔离、
|
||||
优先级、门阈值热读、脏值兜底与工厂 best-effort:
|
||||
|
||||
回落链四条(先锁死才许真接线):
|
||||
① 未 start / 未 attach → genconfig 走原链(env→YAML→内置默认),行为 = 现状;
|
||||
热源已 attach 但缺该键 → 回落 env(热源无值不截断)。
|
||||
② Nacos 不可达 → NacosHotConfig 磁盘快照/默认兜底(get_config 抛)→ store 空 → genconfig 回落本地。
|
||||
③ namespace 配错 → 订阅的空间/dataId 收不到值(get_config 返 None)→ store 空 → 回落本地
|
||||
(佐证前置任务:写读两侧 namespaceId 必须同一真实值,否则配置静默推不到)。
|
||||
④ YAML 回落 → 热源无该键、无 env → 取本地 YAML 与内置默认。
|
||||
|
||||
另:优先级 Nacos > env(命中即压过 env);cheap 与 tier2 两档各读各 dataId 的热源、不串档;
|
||||
软预算(budget)与门阈值(gates)都吃上热读;脏热值保旧回落不穿透;工厂旗默认关 / best-effort /
|
||||
按档 dataId / NO_PROXY 在建 client 前 / 回退开关 detach。
|
||||
|
||||
跑法:
|
||||
PYTHONPATH=tier2/gen-worker cheap-worker/.venv/bin/python -m pytest \
|
||||
tier2/gen-worker/tests/test_genconfig_nacos.py -v
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
# 包内/直跑兼容:把 gen-worker/ 加进 sys.path(PYTHONPATH 已指 gen-worker 时这步是冗余保险)。
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_GW = os.path.dirname(_HERE) # gen-worker/
|
||||
if _GW not in sys.path:
|
||||
sys.path.insert(0, _GW)
|
||||
|
||||
from worker import genconfig, genconfig_nacos # noqa: E402 —— 被测模块
|
||||
from worker.nacos_hotconfig import NacosHotConfig # noqa: E402
|
||||
|
||||
|
||||
# 所有测试可能碰到的 env,fixture 统一快照 + 还原,防跨测/跨会话泄漏。
|
||||
_ENV_KEYS = (
|
||||
"TIER2_GEN__BUDGET__RMB_HARD_LIMIT",
|
||||
"TIER2_GEN__GATES__QPASS_GO_MIN",
|
||||
"TIER2_GENCONFIG",
|
||||
"TIER2_GENCONFIG_NACOS",
|
||||
"TIER2_GENCONFIG_NACOS_DATAID",
|
||||
"TIER2_GENCONFIG_NACOS_GROUP",
|
||||
"NACOS_SERVER_ADDR",
|
||||
"NACOS_NAMESPACE",
|
||||
"NACOS_USERNAME",
|
||||
"NACOS_PASSWORD",
|
||||
"NO_PROXY",
|
||||
"no_proxy",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def clean_genconfig():
|
||||
"""每测前后:detach 热源 + reload YAML 缓存 + 清相关 env,保证从纯本地基线起跑、测后不泄漏。"""
|
||||
genconfig.detach_hot_source()
|
||||
genconfig.reload()
|
||||
snap = {k: os.environ.get(k) for k in _ENV_KEYS}
|
||||
for k in _ENV_KEYS:
|
||||
os.environ.pop(k, None)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
genconfig.detach_hot_source()
|
||||
genconfig.reload()
|
||||
for k, v in snap.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
|
||||
|
||||
class _FakeHotSource:
|
||||
"""最小热源:满足 genconfig 期望的 get(key, default)->value 契约(NacosHotConfig 读接口的子集)。
|
||||
|
||||
genconfig 用 src.get("area.key", _MISSING) 查:store 无该键时把 genconfig 传入的 sentinel 原样返回,
|
||||
genconfig 据此判「热源没这个键」继续回落。故这里 default 必须原样透传、不能替换成别的默认。
|
||||
"""
|
||||
|
||||
def __init__(self, store: dict, raise_on_get: bool = False):
|
||||
self._store = dict(store)
|
||||
self._raise = raise_on_get # True 模拟热源取值异常(get() 抛)
|
||||
|
||||
def get(self, key, default=None):
|
||||
if self._raise:
|
||||
raise RuntimeError("hot source boom")
|
||||
return self._store.get(key, default)
|
||||
|
||||
|
||||
class _FakeNacosClient:
|
||||
"""fake v1 nacos client:喂给真 NacosHotConfig,验路径②③(不可达 / 读不到)。契约对齐已核验的 v1。"""
|
||||
|
||||
def __init__(self, content, unreachable: bool = False):
|
||||
self._content = content
|
||||
self.unreachable = unreachable # True → get_config 抛(模拟连磁盘快照都没有的不可达)
|
||||
|
||||
def get_config(self, data_id, group, timeout=None, no_snapshot=None):
|
||||
if self.unreachable:
|
||||
raise RuntimeError("nacos unreachable")
|
||||
return self._content # None 模拟「订阅的空间/dataId 收不到值」(namespace 配错表现)
|
||||
|
||||
def add_config_watcher(self, data_id, group, cb, content=None):
|
||||
pass # 订阅 no-op:这些回落测试不涉及推送热更
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 回落链四条(设计 §4 步骤4 验证要求)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def test_path1_not_attached_uses_local_chain(clean_genconfig):
|
||||
"""① 未 attach → genconfig 完全走原链(env→YAML→内置默认),行为 = 现状字节不变。"""
|
||||
assert genconfig._hot_source is None # 默认未接热源
|
||||
baseline = genconfig.get("budget", "rmb_hard_limit", 50.0) # 纯本地基线(真 YAML 或内置默认)
|
||||
# 设 env 一次性覆盖仍照常生效(未 attach 时 env 就是最高优先级)。
|
||||
os.environ["TIER2_GEN__BUDGET__RMB_HARD_LIMIT"] = "88.0"
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == 88.0
|
||||
assert baseline != 88.0 # 基线不是 env 值,证明确实走了本地链
|
||||
|
||||
|
||||
def test_path1b_attached_but_key_absent_falls_back_to_env(clean_genconfig):
|
||||
"""① 热源已 attach 但缺该键 → 回落 env(热源无值不把取值截断成 None)。"""
|
||||
genconfig.attach_hot_source(_FakeHotSource({"gates.qpass_go_min": 0.9})) # 只有别的键
|
||||
os.environ["TIER2_GEN__BUDGET__RMB_HARD_LIMIT"] = "77.0"
|
||||
# 热源没有 budget.rmb_hard_limit → 回落 env 77.0(不是热源那条 gates 键、更不是 None)。
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == 77.0
|
||||
|
||||
|
||||
def test_path2_nacos_unreachable_falls_back_to_local(clean_genconfig):
|
||||
"""② Nacos 不可达:真 NacosHotConfig + 不可达 client → store 空 → genconfig 回落本地。"""
|
||||
genconfig.detach_hot_source()
|
||||
baseline = genconfig.get("budget", "rmb_hard_limit", 50.0) # 先取纯本地基线
|
||||
fake = _FakeNacosClient(None, unreachable=True)
|
||||
hot = NacosHotConfig(client=fake, data_id="gen-hot-params-tier2", group="DEFAULT_GROUP", defaults={})
|
||||
hot.start() # 不可达:get_config 抛,best-effort 留空 store,不抛
|
||||
genconfig.attach_hot_source(hot)
|
||||
# 热源 store 空(不可达兜底)→ genconfig 回落本地基线,不被 Nacos 连累。
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == baseline
|
||||
|
||||
|
||||
def test_path3_namespace_misconfig_reads_nothing_falls_back(clean_genconfig):
|
||||
"""③ namespace 配错:订阅空间/dataId 收不到值(get_config 返 None)→ store 空 → 回落本地。
|
||||
|
||||
佐证前置任务:写侧 publish 进真实 namespaceId、读侧订阅另一个(如误写 public)→ 永远读不到,
|
||||
表现就是本条——store 停在空、genconfig 回落本地。故写读两侧 namespaceId 必须一次性对齐。
|
||||
"""
|
||||
genconfig.detach_hot_source()
|
||||
baseline = genconfig.get("budget", "rmb_hard_limit", 50.0)
|
||||
fake = _FakeNacosClient(None) # 非不可达,但订阅的空间读不到任何内容
|
||||
hot = NacosHotConfig(client=fake, data_id="gen-hot-params-tier2", group="DEFAULT_GROUP", defaults={})
|
||||
hot.start()
|
||||
genconfig.attach_hot_source(hot)
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == baseline
|
||||
|
||||
|
||||
def test_path4_yaml_fallback_when_hot_and_env_absent(clean_genconfig):
|
||||
"""④ YAML 回落:热源无该键、无 env → 取本地 YAML 与内置默认(结果 = 纯本地基线)。"""
|
||||
genconfig.detach_hot_source()
|
||||
baseline = genconfig.get("budget", "rmb_hard_limit", 50.0)
|
||||
genconfig.attach_hot_source(_FakeHotSource({})) # 空热源(无任何键)
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == baseline # 回落本地,与未 attach 一致
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 优先级 / 两档隔离 / 门阈值 / 脏值
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def test_hot_source_overrides_env_and_yaml(clean_genconfig):
|
||||
"""优先级 Nacos > env > YAML:热源命中即压过 env(配置中心才是权威,遗留 env 不得静默盖住激活值)。"""
|
||||
os.environ["TIER2_GEN__BUDGET__RMB_HARD_LIMIT"] = "77.0" # 机器上遗留实验 env
|
||||
genconfig.attach_hot_source(_FakeHotSource({"budget.rmb_hard_limit": 99.0}))
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == 99.0 # 热源赢
|
||||
|
||||
|
||||
def test_two_tiers_read_own_dataid(clean_genconfig):
|
||||
"""两档各读各 dataId:tier2 进程热源只有 tier2 键、cheap 进程热源只有 cheap 键,互不串档。"""
|
||||
# —— tier2 进程:先取 cheap 键的纯本地基线(fixture 保证起始未 attach),再 attach tier2 dataId 的热源 ——
|
||||
cheap_local = genconfig.get("budget", "cheap_rmb_hard_limit", 10.0) # cheap 键纯本地基线
|
||||
genconfig.attach_hot_source(_FakeHotSource({"budget.rmb_hard_limit": 55.0}))
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == 55.0 # tier2 键命中热源
|
||||
assert genconfig.get("budget", "cheap_rmb_hard_limit", 10.0) == cheap_local # cheap 键热源没有→回落本地
|
||||
|
||||
# —— cheap 进程:换成 attach cheap dataId 的热源(store 里是 budget.cheap_rmb_hard_limit)——
|
||||
genconfig.detach_hot_source()
|
||||
tier2_baseline = genconfig.get("budget", "rmb_hard_limit", 50.0) # tier2 键的纯本地基线
|
||||
genconfig.attach_hot_source(_FakeHotSource({"budget.cheap_rmb_hard_limit": 12.0}))
|
||||
assert genconfig.get("budget", "cheap_rmb_hard_limit", 10.0) == 12.0 # cheap 键命中热源
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == tier2_baseline # tier2 键热源没有→回落本地
|
||||
|
||||
|
||||
def test_gate_thresholds_also_hot_read(clean_genconfig):
|
||||
"""门阈值(gates 区)与软预算(budget 区)一样吃上热读——路 B 覆盖预算 + 阈值两类。"""
|
||||
genconfig.attach_hot_source(_FakeHotSource({
|
||||
"budget.rmb_hard_limit": 66.0,
|
||||
"gates.qpass_go_min": 0.66,
|
||||
}))
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == 66.0
|
||||
assert genconfig.get("gates", "qpass_go_min", 0.40) == 0.66
|
||||
|
||||
|
||||
def test_hot_value_coerced_by_type(clean_genconfig):
|
||||
"""热源值按内置默认类型转型:JSON 里的字符串 "45" 落到 float 旋钮上得 45.0(与 env/YAML 同款转型)。"""
|
||||
genconfig.attach_hot_source(_FakeHotSource({"budget.rmb_hard_limit": "45"}))
|
||||
got = genconfig.get("budget", "rmb_hard_limit", 50.0)
|
||||
assert got == 45.0 and isinstance(got, float)
|
||||
|
||||
|
||||
def test_dirty_hot_value_falls_back_no_throw(clean_genconfig):
|
||||
"""脏热值(转不动的字符串)→ 保旧回落本地、不抛、不穿透成 None。"""
|
||||
genconfig.detach_hot_source()
|
||||
baseline = genconfig.get("budget", "rmb_hard_limit", 50.0)
|
||||
genconfig.attach_hot_source(_FakeHotSource({"budget.rmb_hard_limit": "not-a-number"}))
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == baseline # 脏值被挡,回落本地
|
||||
|
||||
|
||||
def test_hot_source_get_exception_falls_back(clean_genconfig):
|
||||
"""热源 get 自身抛异常 → genconfig 视为「没取到」回落本地,绝不连累主链。"""
|
||||
genconfig.detach_hot_source()
|
||||
baseline = genconfig.get("budget", "rmb_hard_limit", 50.0)
|
||||
genconfig.attach_hot_source(_FakeHotSource({"budget.rmb_hard_limit": 99.0}, raise_on_get=True))
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == baseline
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 工厂(build_and_attach_hot_source)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def test_factory_disabled_by_default(clean_genconfig):
|
||||
"""工厂旗默认关:不设 TIER2_GENCONFIG_NACOS → build 返回 None、不 attach、genconfig 走原链。"""
|
||||
assert genconfig_nacos.is_enabled() is False
|
||||
hot = genconfig_nacos.build_and_attach_hot_source(tier="tier2")
|
||||
assert hot is None
|
||||
assert genconfig._hot_source is None
|
||||
|
||||
|
||||
def test_factory_attaches_and_genconfig_reads_hot(clean_genconfig):
|
||||
"""启用旗打开 + fake client 有值 → 工厂建 client → NacosHotConfig 初读 → attach → genconfig 命中热值。"""
|
||||
os.environ["TIER2_GENCONFIG_NACOS"] = "1"
|
||||
fake_client = _FakeNacosClient(json.dumps({"budget.rmb_hard_limit": 42.0}))
|
||||
with mock.patch("nacos.NacosClient", return_value=fake_client):
|
||||
hot = genconfig_nacos.build_and_attach_hot_source(tier="tier2")
|
||||
assert hot is not None
|
||||
assert genconfig._hot_source is hot
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == 42.0 # 端到端:经工厂 attach 后读到热值
|
||||
|
||||
|
||||
def test_factory_best_effort_on_client_error(clean_genconfig):
|
||||
"""工厂 best-effort:client 构造抛 → build 返回 None、不抛、genconfig 未 attach、回落纯本地。"""
|
||||
os.environ["TIER2_GENCONFIG_NACOS"] = "1"
|
||||
with mock.patch("nacos.NacosClient", side_effect=RuntimeError("nacos unreachable")):
|
||||
hot = genconfig_nacos.build_and_attach_hot_source(tier="tier2") # 不应抛
|
||||
assert hot is None
|
||||
assert genconfig._hot_source is None
|
||||
|
||||
|
||||
def test_factory_dataid_per_tier(clean_genconfig):
|
||||
"""按档隔离:tier="cheap" 订阅 gen-hot-params-cheap、tier="tier2" 订阅 gen-hot-params-tier2。"""
|
||||
os.environ["TIER2_GENCONFIG_NACOS"] = "1"
|
||||
seen = {}
|
||||
|
||||
def _spy_hotconfig(*, client, data_id, group, defaults):
|
||||
seen["data_id"] = data_id
|
||||
seen["defaults"] = defaults
|
||||
return mock.MagicMock()
|
||||
|
||||
# cheap 档
|
||||
with mock.patch("nacos.NacosClient", return_value=mock.MagicMock()), \
|
||||
mock.patch("worker.nacos_hotconfig.NacosHotConfig", side_effect=_spy_hotconfig):
|
||||
genconfig_nacos.build_and_attach_hot_source(tier="cheap")
|
||||
assert seen["data_id"] == "gen-hot-params-cheap"
|
||||
assert seen["defaults"] == {} # 坑④:热源 defaults 必须空,不引入第二默认值
|
||||
|
||||
genconfig.detach_hot_source()
|
||||
# tier2 档
|
||||
with mock.patch("nacos.NacosClient", return_value=mock.MagicMock()), \
|
||||
mock.patch("worker.nacos_hotconfig.NacosHotConfig", side_effect=_spy_hotconfig):
|
||||
genconfig_nacos.build_and_attach_hot_source(tier="tier2")
|
||||
assert seen["data_id"] == "gen-hot-params-tier2"
|
||||
|
||||
|
||||
def test_factory_dataid_env_override(clean_genconfig):
|
||||
"""env TIER2_GENCONFIG_NACOS_DATAID 显式覆盖档默认(演示脚本用它指向演示专用 dataId)。"""
|
||||
os.environ["TIER2_GENCONFIG_NACOS"] = "1"
|
||||
os.environ["TIER2_GENCONFIG_NACOS_DATAID"] = "gen-hot-params-demo"
|
||||
seen = {}
|
||||
|
||||
def _spy_hotconfig(*, client, data_id, group, defaults):
|
||||
seen["data_id"] = data_id
|
||||
return mock.MagicMock()
|
||||
|
||||
with mock.patch("nacos.NacosClient", return_value=mock.MagicMock()), \
|
||||
mock.patch("worker.nacos_hotconfig.NacosHotConfig", side_effect=_spy_hotconfig):
|
||||
genconfig_nacos.build_and_attach_hot_source(tier="tier2") # 档默认本应 tier2,被 env 覆盖
|
||||
assert seen["data_id"] == "gen-hot-params-demo"
|
||||
|
||||
|
||||
def test_factory_proxy_bypass_before_client_build(clean_genconfig):
|
||||
"""内网铁律:NO_PROXY 旁路必须在【建 client 前】完成 —— 构造 NacosClient 时 NO_PROXY 已含 Nacos host。"""
|
||||
os.environ["TIER2_GENCONFIG_NACOS"] = "1"
|
||||
os.environ["NACOS_SERVER_ADDR"] = "100.64.0.8:8848"
|
||||
seen = {}
|
||||
|
||||
def _spy_ctor(*_a, **_kw):
|
||||
seen["no_proxy_at_build"] = os.environ.get("NO_PROXY", "")
|
||||
return mock.MagicMock()
|
||||
|
||||
with mock.patch("nacos.NacosClient", side_effect=_spy_ctor), \
|
||||
mock.patch("worker.nacos_hotconfig.NacosHotConfig", return_value=mock.MagicMock()):
|
||||
genconfig_nacos.build_and_attach_hot_source(tier="tier2")
|
||||
assert "100.64.0.8" in seen.get("no_proxy_at_build", "")
|
||||
|
||||
|
||||
def test_factory_namespace_defaults_empty_string(clean_genconfig):
|
||||
"""真实 namespaceId 默认空串(勿写 public;前置任务口径)——构造 client 时 namespace 关键字 == ""。"""
|
||||
os.environ["TIER2_GENCONFIG_NACOS"] = "1"
|
||||
ctor = mock.MagicMock(return_value=mock.MagicMock())
|
||||
with mock.patch("nacos.NacosClient", ctor), \
|
||||
mock.patch("worker.nacos_hotconfig.NacosHotConfig", return_value=mock.MagicMock()):
|
||||
genconfig_nacos.build_and_attach_hot_source(tier="tier2")
|
||||
_args, kwargs = ctor.call_args
|
||||
assert kwargs.get("namespace") == "" # 默认公共空间 = 空串
|
||||
assert kwargs.get("username") == "nacos"
|
||||
|
||||
|
||||
def test_rollback_switch_detach(clean_genconfig):
|
||||
"""回退开关:detach 后 genconfig 回落纯本地链(设计 §3.6/§6 一处开关切回本地 YAML)。"""
|
||||
genconfig.attach_hot_source(_FakeHotSource({"budget.rmb_hard_limit": 99.0}))
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) == 99.0
|
||||
genconfig_nacos.detach() # 回退
|
||||
assert genconfig._hot_source is None
|
||||
assert genconfig.get("budget", "rmb_hard_limit", 50.0) != 99.0 # 回落本地,不再是热值
|
||||
@ -7,8 +7,14 @@
|
||||
调生成 = 改 YAML 重跑,不改码、不重同步。这正是图说 B 族「配置注册表(唯一事实源 / 版本化 / 运行时读)」
|
||||
在生成器旋钮这一面的最小落地——配置是真相,代码只是按 id 取值的消费方。
|
||||
|
||||
【取值三级回落(运行时读,失败绝不中断主链)】
|
||||
【取值优先级(运行时读,失败绝不中断主链)】
|
||||
get(area, key, default) 每次调用按以下顺序取值,任一级取到即用:
|
||||
⓪ Nacos 热源(配置控制面阶段二接入,默认不启用):worker 启动期若由工厂 genconfig_nacos
|
||||
.build_and_attach_hot_source() attach 了一个 NacosHotConfig(env TIER2_GENCONFIG_NACOS 启用),
|
||||
则最先查它——配置中心激活的预算/门阈值经 Nacos 长轮询推送热读进来,改一次配置中心、下一次生成
|
||||
即读到新值,不重启。未启用(默认)时本级不存在,取值链 = 原三级,行为与阶段二前逐字节一致。
|
||||
热源以复合键 "area.key" 存值(genconfig 是 (area,key) 二维,复合键无歧义扁平化);缺该键则继续
|
||||
往下回落,绝不引入第二默认值(热源只给「有就用」的值,缺失全交下面 env/YAML/内置默认)。
|
||||
① env 单点覆盖:环境变量 TIER2_GEN__<AREA>__<KEY>(全大写、双下划线分隔)——临时压一个值不必改文件,
|
||||
适合「这一批我想试 max_iters=60」这种一次性实验(改完 unset 即恢复)。
|
||||
② 外部 YAML:tier2/config/generation.yaml(路径可被 env TIER2_GENCONFIG 指向别处)的 area.key。
|
||||
@ -154,6 +160,41 @@ _cache_path: str | None = None
|
||||
_warned: set[str] = set()
|
||||
|
||||
|
||||
# ── Nacos 热源(配置控制面阶段二步骤4:genconfig 读取后端提升到 Nacos)────────────────────
|
||||
# 工厂 genconfig_nacos.build_and_attach_hot_source() 在 worker 启动期把一个 NacosHotConfig 实例
|
||||
# attach 进这个进程级单例槽;get() 取值链最前端先查它(见 get() 的 ⓪ 层)。未 attach(默认)→ get()
|
||||
# 完全走原链(env→YAML→内置默认→调用方 default),行为与阶段二前逐字节一致。
|
||||
# 为什么是进程级单例、且 genconfig 不认档:cheap Service 与 tier2 Service 是两个独立进程,却都
|
||||
# `from worker import genconfig` 共用本模块。两档隔离不靠 genconfig 区分,而靠【各档进程 attach 各档
|
||||
# dataId 的热源】——cheap 进程 attach cheap dataId 的 NacosHotConfig(store 里是 budget.cheap_* 键)、
|
||||
# tier2 进程 attach tier2 dataId 的(store 里是 budget.rmb_hard_limit 等)。genconfig 只查「当前进程
|
||||
# attach 的那个热源」,故同一份 get() 代码在两档进程里各取各的 dataId,天然不串档。
|
||||
_hot_source: Any = None
|
||||
|
||||
# sentinel:区分「热源里没有这个键」与「热源里键值恰好是 None」。热源缺键必须继续回落到 env/YAML/
|
||||
# 内置默认,不能被当成一个 None 值截断(否则会吞掉下面所有回落级)。
|
||||
_MISSING: Any = object()
|
||||
|
||||
|
||||
def attach_hot_source(source: Any) -> None:
|
||||
"""worker 启动期由工厂注入 Nacos 热源(NacosHotConfig 实例);此后 get() 最先查它。
|
||||
|
||||
幂等可重入:重复 attach 覆盖为最新源。source 只需满足 get(key, default)->value 契约
|
||||
(NacosHotConfig 即是),genconfig 不依赖其内部实现——故单测可注入任意 fake 源。
|
||||
"""
|
||||
global _hot_source
|
||||
_hot_source = source
|
||||
|
||||
|
||||
def detach_hot_source() -> None:
|
||||
"""卸载热源,get() 回落纯本地链(env→YAML→内置默认→调用方 default)。
|
||||
|
||||
这是设计 §3.6/§6「回退开关」在进程内的一处落点:热源出问题时切回纯本地,调用面一行不改。
|
||||
"""
|
||||
global _hot_source
|
||||
_hot_source = None
|
||||
|
||||
|
||||
def _warn_once(tag: str, msg: str) -> None:
|
||||
"""同一 tag 只告警一次(防 best-effort 路径在循环里刷屏);可追溯日志,带 [tier2-genconfig] 前缀。"""
|
||||
if tag in _warned:
|
||||
@ -284,7 +325,31 @@ def get(area: str, key: str, default: Any = None) -> Any:
|
||||
# 转型基准:内置默认有该 key 用它的类型,否则用 default 的类型(都为 None 则不强转)。
|
||||
like = builtin.get(key, default)
|
||||
|
||||
# ① env 单点覆盖(最高优先级,便于一次性实验,不必改文件)。
|
||||
# ⓪ Nacos 热源(配置控制面阶段二:最高优先级)。未 attach(默认)/ 热源无此键 / 取值异常 → 静默
|
||||
# 继续回落,绝不引入第二默认值(热源只给「有就用」的值,缺失一律交下面 env/YAML/内置默认)。
|
||||
# 复合键 area.key:把 genconfig 的 (area,key) 二维无歧义扁平化成热源 store 的一个键;激活编排 /
|
||||
# 演示脚本按同一 "area.key" 组 dataId JSON。热源命中即返回,根本不读 YAML —— mtime 缓存无从盖住热值。
|
||||
src = _hot_source
|
||||
if src is not None:
|
||||
try:
|
||||
hot_val = src.get(f"{area}.{key}", _MISSING)
|
||||
except Exception as exc: # noqa: BLE001 —— 热源取值绝不连累主链:异常即视为「没取到」回落
|
||||
hot_val = _MISSING
|
||||
_warn_once(
|
||||
f"hot-err:{area}.{key}",
|
||||
f"Nacos 热源取 {area}.{key} 异常({type(exc).__name__}: {exc})→ 回落 env/YAML/默认。",
|
||||
)
|
||||
if hot_val is not _MISSING:
|
||||
coerced = _coerce(hot_val, like)
|
||||
if coerced is not None:
|
||||
return coerced
|
||||
# 热源有该键但值转不动(类型脏)→ 告警回落(与 env/YAML 脏值同处置,不让脏热值穿透)。
|
||||
_warn_once(
|
||||
f"hot-bad:{area}.{key}",
|
||||
f"Nacos 热源 {area}.{key}={hot_val!r} 无法转成 {type(like).__name__} → 忽略,回落 env/YAML/默认。",
|
||||
)
|
||||
|
||||
# ① env 单点覆盖(Nacos 热源之下、YAML 之上;一次性实验逃生舱,不必改文件)。
|
||||
env_name = f"{_ENV_PREFIX}{area.upper()}__{key.upper()}"
|
||||
env_raw = os.environ.get(env_name)
|
||||
if env_raw is not None:
|
||||
|
||||
156
tier2/gen-worker/worker/genconfig_nacos.py
Normal file
156
tier2/gen-worker/worker/genconfig_nacos.py
Normal file
@ -0,0 +1,156 @@
|
||||
"""genconfig_nacos.py —— 把 genconfig 的读取后端提升到 Nacos 的启动期工厂(配置控制面阶段二·步骤4)。
|
||||
|
||||
【这份解决什么问题】
|
||||
阶段〇建好了 nacos_hotconfig.NacosHotConfig(只读视图:订阅一个 Nacos dataId、长轮询热更、线程安全读),
|
||||
但它「只消费已建好的 v1 NacosClient、自己不建 client」。阶段二步骤4 把这最后一段接起来:本模块是
|
||||
**worker 启动期**的单例工厂,负责把 v1 client 构造齐(server / 鉴权 / NO_PROXY 旁路 / 真实 namespaceId /
|
||||
group / 按档 dataId),注入 NacosHotConfig,start() 订阅,再 attach 进 genconfig 作「热源」。此后进程内
|
||||
所有 genconfig.get(area,key) 调用最先查 Nacos 热源——middleware 的软预算目标、门判阈值都自动吃上配置
|
||||
中心的激活值,调用面一行不改。
|
||||
|
||||
【显式启用开关(默认关 = 现状字节不变)】
|
||||
env TIER2_GENCONFIG_NACOS ∈ {1,true,yes,on} 才启用;不设(默认)时本工厂 no-op、不 attach、不牵 nacos
|
||||
依赖,genconfig 完全走原链(env→YAML→内置默认),行为与阶段二前逐字节一致。这个旗同时是设计 §3.6/§6
|
||||
的【回退开关】:线上出问题就关旗重启,genconfig 立刻切回纯本地 YAML(运行时也可 genconfig.detach_hot_source())。
|
||||
|
||||
【按档隔离的 dataId(设计 §3.3)】
|
||||
cheap-worker 与 tier2 gen-worker 是两个独立进程、各起一个本工厂:tier="tier2" 订阅 gen-hot-params-tier2、
|
||||
tier="cheap" 订阅 gen-hot-params-cheap。两档各 attach 各自 dataId 的热源,publish 一档不冲掉另一档的键。
|
||||
genconfig 本身不认档(见 genconfig._hot_source 注释),隔离全靠「哪个进程订阅哪个 dataId」。
|
||||
|
||||
【NO_PROXY 内网铁律(阶段〇同款坑)】
|
||||
本机系统代理是 fake-ip 198.18.x;nacos client 走 HTTP 直连内网 100.64.0.8:8848 必须绕过它,且必须在
|
||||
【建 client 前】把 Nacos host 塞进 NO_PROXY/no_proxy(urllib 建连时读环境,晚设不生效,否则栽 502)。与
|
||||
service/nacos_registry._bypass_proxy_for 同款;此处 worker 包内自带一份(worker 是底层,不反向依赖 service 包)。
|
||||
|
||||
【真实 namespaceId = 空串(前置任务口径)】
|
||||
默认公共空间的 namespaceId 是空串,不是字面量 "public"(显示名≠id)。写侧(yudao publish)与读侧(本工厂)
|
||||
必须对齐同一真实 namespaceId,否则激活写进一个空间、worker 在另一个空间读、配置永远推不到(前置任务 056433d5
|
||||
已把 nacos_registry / Java / 凭据档统一到空串,本工厂沿同口径:NACOS_NAMESPACE 默认空串)。
|
||||
|
||||
【绝不引入第二默认值(坑④·tech-decisions §13)】
|
||||
注入 NacosHotConfig 的 defaults 刻意为空 {}:热源缺某键时 NacosHotConfig.get 返回 genconfig 传入的
|
||||
sentinel,由 genconfig 回落链(env→YAML→内置默认)接管——热源侧登记默认值 = 第二默认源,会静默盖住单源
|
||||
(双源默认值影蔽刚在收敛环栽过),故这里 defaults 必须空。
|
||||
|
||||
【best-effort 铁律】
|
||||
构造 / 订阅 全程 try/except:启用了但 Nacos 不可达 / 鉴权失败 / 缺 nacos 库 → 只告警、不抛、不 attach,
|
||||
genconfig 回落纯本地链,生成主链不被配置中心连累。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
# genconfig 顶层零重依赖(不牵 agentscope),安全直接 import。包内相对导入优先,PYTHONPATH=gen-worker 直跑兜底。
|
||||
try:
|
||||
from . import genconfig # 包内相对导入(worker.genconfig_nacos)
|
||||
except ImportError: # pragma: no cover —— PYTHONPATH=gen-worker 直跑时的绝对导入兜底
|
||||
from worker import genconfig # type: ignore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worker.nacos_hotconfig import NacosHotConfig
|
||||
|
||||
|
||||
# 启用旗(默认关 = 现状不变;同时是设计 §3.6/§6 回退开关)。与 genconfig 的 TIER2_GEN__ 前缀区分:
|
||||
# TIER2_GEN__ 是「压某个旋钮值」,本旗是「是否把读取后端接到 Nacos」的总开关。
|
||||
_ENABLE_FLAG = "TIER2_GENCONFIG_NACOS"
|
||||
|
||||
# 按档默认 dataId(设计 §3.3 按档隔离)。env TIER2_GENCONFIG_NACOS_DATAID 显式指定则不分档、直接用它
|
||||
# (演示脚本用它指向演示专用 dataId,不碰生产 dataId)。
|
||||
_DEFAULT_DATAIDS = {
|
||||
"tier2": "gen-hot-params-tier2",
|
||||
"cheap": "gen-hot-params-cheap",
|
||||
}
|
||||
|
||||
_TRUE = ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
"""启用旗是否打开(默认关)。调用方可先查再决定是否 build;也是回退开关的判据。"""
|
||||
return os.environ.get(_ENABLE_FLAG, "").strip().lower() in _TRUE
|
||||
|
||||
|
||||
def _bypass_proxy_for(host: str) -> None:
|
||||
"""把 host 并进 NO_PROXY/no_proxy(内网直连绕系统 fake-ip 代理;必须在建 nacos client 前调)。
|
||||
|
||||
与 service/nacos_registry._bypass_proxy_for 同款逻辑(worker 是底层,不反向 import service 包,故各存
|
||||
一份):两个大小写变体都写;逗号分隔精确比对各段,避免 "100.64.0.8" 与 "100.64.0.80" 之类子串误判;
|
||||
已在列表就不重复追加。
|
||||
"""
|
||||
for key in ("NO_PROXY", "no_proxy"):
|
||||
cur = os.environ.get(key, "")
|
||||
segments = [seg.strip() for seg in cur.split(",") if seg.strip()]
|
||||
if host not in segments:
|
||||
segments.append(host)
|
||||
os.environ[key] = ",".join(segments)
|
||||
|
||||
|
||||
def _resolve_data_id(tier: str) -> str:
|
||||
"""按档解析 dataId:env TIER2_GENCONFIG_NACOS_DATAID 显式覆盖 > 档默认 > 兜底 tier2 档名(未知档不炸)。"""
|
||||
override = os.environ.get("TIER2_GENCONFIG_NACOS_DATAID", "").strip()
|
||||
if override:
|
||||
return override
|
||||
return _DEFAULT_DATAIDS.get(tier, _DEFAULT_DATAIDS["tier2"])
|
||||
|
||||
|
||||
def build_and_attach_hot_source(*, tier: str = "tier2") -> "NacosHotConfig | None":
|
||||
"""worker 启动期调:构造 v1 NacosClient → 注入 NacosHotConfig → start() → attach 进 genconfig。
|
||||
|
||||
Args:
|
||||
tier: 档位("tier2" / "cheap"),决定订阅哪个 dataId(设计 §3.3 按档隔离)。
|
||||
|
||||
Returns:
|
||||
已 attach 的 NacosHotConfig(供调用方持有 / 关停 / 测试);启用旗未开或构造失败 → 返回 None
|
||||
(genconfig 走原链)。全程 best-effort:任何异常只告警、不抛、不 attach。
|
||||
|
||||
注入 NacosHotConfig 的 defaults 刻意为空 {}:热源缺某键时 get 返回 sentinel,由 genconfig 回落链
|
||||
(env→YAML→内置默认)接管——热源侧【绝不】登记第二套默认值(坑④双源默认值影蔽,tech-decisions §13)。
|
||||
"""
|
||||
if not is_enabled():
|
||||
# 默认关:不 attach、不牵 nacos 依赖,genconfig 走原链(现状字节不变)。
|
||||
return None
|
||||
|
||||
nacos_addr = os.environ.get("NACOS_SERVER_ADDR", "100.64.0.8:8848")
|
||||
namespace = os.environ.get("NACOS_NAMESPACE", "") # 默认公共空间 = 空串(勿写 public;前置任务口径)
|
||||
username = os.environ.get("NACOS_USERNAME", "nacos")
|
||||
password = os.environ.get("NACOS_PASSWORD", "")
|
||||
group = os.environ.get("TIER2_GENCONFIG_NACOS_GROUP", "DEFAULT_GROUP")
|
||||
data_id = _resolve_data_id(tier)
|
||||
|
||||
# 内网铁律:必须在【建 client 前】把 Nacos host 塞进 NO_PROXY,否则 HTTP 直连被 fake-ip 代理拦(502)。
|
||||
_bypass_proxy_for(nacos_addr.split(":")[0])
|
||||
|
||||
try:
|
||||
import nacos # noqa: PLC0415 —— 惰性 import:默认关时不牵 nacos 依赖(本地/CLI/6c6g 免装)
|
||||
from worker.nacos_hotconfig import NacosHotConfig # noqa: PLC0415
|
||||
|
||||
client = nacos.NacosClient(
|
||||
nacos_addr,
|
||||
namespace=namespace,
|
||||
username=username,
|
||||
password=password,
|
||||
)
|
||||
# defaults={} 是硬约束:热源不登记默认值,缺键交 genconfig 回落链(杜绝双源默认值影蔽,坑④)。
|
||||
hot = NacosHotConfig(client=client, data_id=data_id, group=group, defaults={})
|
||||
hot.start() # 初读 + 订阅长轮询推送(best-effort:Nacos 不可达也不抛,留空 store 让 genconfig 回落)
|
||||
genconfig.attach_hot_source(hot)
|
||||
print(
|
||||
f"[genconfig-nacos] 已接 Nacos 热源:tier={tier} dataId={data_id} group={group} "
|
||||
f"namespace={namespace!r} nacos={nacos_addr}",
|
||||
flush=True,
|
||||
)
|
||||
return hot
|
||||
except Exception as e: # noqa: BLE001 —— best-effort:接线失败不 attach、不抛,genconfig 回落纯本地链
|
||||
print(
|
||||
f"[genconfig-nacos] 接 Nacos 热源失败(genconfig 回落纯本地 YAML,best-effort):"
|
||||
f"{type(e).__name__}: {e}",
|
||||
flush=True,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def detach() -> None:
|
||||
"""卸载热源,genconfig 回落纯本地链(设计 §3.6/§6 回退开关的显式入口;转调 genconfig.detach_hot_source)。"""
|
||||
genconfig.detach_hot_source()
|
||||
Loading…
x
Reference in New Issue
Block a user