feat(overlay): 覆盖网络线 序㊾ —— 探针观测面改「两台中继并集」(附 序㊽ 源码/文档补提交)
序㊾(本棒):
- scripts/overlay-probe.cjs:OBS-01 / OBS-08 / OBS-09 的数据源由「只读 47 中继」
改为「按两台中继取并集」,消除 worker 归属漂移时的假红 / 假 SKIP
· endpoints 以 network:hostId:port 为键合并,online 取「或」、localPort 取在线那一侧
· used 按 network/hostId 去重计数(不求和,避免凭空放大在册数)
· localPort 属中继机回环落点 ⇒ 按归属分机探活(106 侧落点由 106 机上探)
· derived(OBS-11)保持 47 视角;阈值与判据一律未放宽
· OBS-16 计数约束:对 47 /status 的读取仍为三次、Δ 只取 47 的 counters;
对端 106 的采样为独立一次,落在第三次采样之后,不进 (status2, status3] 门窗口
· 新增 --peer-status-fixture(并集的对端那一半)与「并集不可取证」强制留痕
- 交接单《覆盖网络-序45-低熵块治理-测熵与实现》§16 全节(§8 前前缀逐字未变)
- 参数表 §11.16 补记(§10 现算指纹未变,值格未动)
附(前几棒已完成并已部署、但尚未入仓的源码 / 文档):
- src/net/relay/content/*.ts、src/net/relay/index.ts、main.ts:块级寻址 C 域分离
- src/supervisor/orchestrator.ts、src/worker/agent.ts:日志采集与巡检(方案 C)
- test/overlay-content.test.mjs:随附用例(npm test = 200 pass / 0 fail / 1 skipped,Node 22)
- scripts/dshlog.mjs(跨机日志取证)、scripts/overlay-entropy.cjs(熵探针)
- dsh-server-docs/04-调整方案/129、133;INDEX.md / docs-manifest.json / 交接单 README 登记
This commit is contained in:
1 parent
45b4999d24
commit
d2ef362a98
20 files changed
+2998
-183
No files matched your search
+385
-65
@@ -63,6 +63,31 @@
|
||||
* ⛔ 零新增暴露面、⛔ 零新增参数键)。⚠️ **只在 47 视角为空时**才去读(正常态**零额外 ssh**)。
|
||||
* 夹具模式用 `--peer-status-fixture <对端 /status 原文>`;⛔ 不给 ⇒ 记"不可判(夹具模式)"。
|
||||
*
|
||||
* ## 🆕 观测面**并集**(序㊾:⛔ 单看 47 会把"合法拓扑态"判成红)
|
||||
* `OBS-01` / `OBS-08` / `OBS-09` 的绿**取决于 106 worker 挂在哪台中继** —— worker 通道按**抖动**换址
|
||||
* (`[relay-switch]`)⇒ 一旦落到 **106 自家中继**,47 视角就是
|
||||
* `used=1 / endpoints=[w-106:<PEER_AGENT_PORT> offline, w-106:<实例口> offline]` ⇒ `OBS-01` FAIL、`OBS-08` FAIL、
|
||||
* `OBS-09` SKIP —— 三条**全是假红 / 假 SKIP**(客户端好好的,只是"不在我这一台")。
|
||||
*
|
||||
* 口径 = **按 `hostId`(含 `network`)合并两台中继的视图**:
|
||||
*
|
||||
* eps = merge(47.endpoints[], 106.endpoints[]) // 键 = `network:hostId:port`;`online` 取**或**;
|
||||
* `localPort` 取**在线那一侧**的(离线条目的落点已失效)
|
||||
* used = |{ network/hostId | s ∈ 47.sessions[] ∪ 106.sessions[] }| // ⛔ **不是求和** —— 同一节点在
|
||||
* 两台都残留会话/条目时求和会**重复计数**(那是另一方向的假绿)
|
||||
* reg = deriveInstanceEndpoints(eps, agentPorts) // `OBS-09` 的派生改用并集(⛔ 判据本身一字不改)
|
||||
*
|
||||
* 🔴 **实例探活必须回到"那一台"上做**:`localPort` 是**中继机回环**落点 ⇒ 并集里来自 106 的端点
|
||||
* 得 ssh 到 106 探(47 上那个口**根本不存在**)⇒ 否则必然 `000` ⇒ **假红**。
|
||||
* 🔴 **只并这三项的数据源**:`OBS-11` 的 `derived`(= 47 自己的监听面)/`OBS-02`(47 自身容量自洽)/
|
||||
* `OBS-13`·`OBS-16`(47 的 `counters`)**一律保持 47 视角** —— 并集只消除"看不见",
|
||||
* ⛔ 不放宽判据、⛔ 不改任何阈值(**真 FAIL 不许被糊成 PASS**)。
|
||||
* 🔴 **`OBS-16` 的 Δ 不受影响**:对端那份是**另算的一次独立采样**(读的是 106 的 `counters`)⇒
|
||||
* ⛔ 不进 `ΔstatusHits` 算式;且对 47 `/status` 的读取次数**一次都没变**(仍是三次,复用已采到的那份)。
|
||||
* ⚠️ 真机模式**无条件**读一次对端(⇒ 并集是**完整**的);⛔ 不做"缺什么补什么"的按需读 ——
|
||||
* 那会留下一个**假绿**口子(106 上多一台 47 不知道的节点时,47 视角照样绿)。
|
||||
* ⚠️ 对端读不回来 ⇒ **不硬失败**,回落 47 视角并**强制留痕**「并集不可取证(…)⇒ 仅 47 视角」。
|
||||
*
|
||||
* ## 🆕 `OBS-16` 的**门窗口判据**(序㉑ · 在册缺陷 P-1 的验收)
|
||||
* `OBS-13` 的 `ΔstatusHits ≥ 1` 只证明"计数器没卡死"——它由**探针自己两次读**即满足,⛔ 证明不了
|
||||
* "订阅生效期间轮询停了"(旧判据下门 95% 时间开着、轮询照旧在跑,而 `OBS-13` **仍然全绿** ⇒
|
||||
@@ -544,6 +569,32 @@ function remoteFacts(r, sshPort, target, probePairs) {
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* 只取「实例面 HTTP 码」的一次 ssh(序㊾)。
|
||||
*
|
||||
* 为什么不能复用 `remoteFacts()`:并集里**属于对端中继**的端点,其回环落点**只在对端那台机上存在**
|
||||
* ⇒ 必须 ssh 到**那一台**探;而 `remoteFacts()` 顺带取的 `ss` / `nft` / 门户码 / relay RSS
|
||||
* **只对 47 有意义**(拉到 106 上会造出另一套"看起来有数据、其实判错对象"的读数)。
|
||||
* 键名与 `remoteFacts()` 一致(`instcode_<port>`)⇒ 调用方把两次结果并进同一个 map 即可。
|
||||
* ⛔ 返回值里**没有**该端口 = 取数失败 ⇒ 由判据 FAIL 并点名(⛔ 此处不兜底成 `000`)。
|
||||
* @param {{port:number, localPort:number}[]} pairs 归属**该中继**的在册实例端点
|
||||
*/
|
||||
function remoteInstanceCodes(r, sshPort, target, pairs) {
|
||||
const host = r.need('RELAY_BIND')
|
||||
const cmd = pairs
|
||||
.map(
|
||||
(p) =>
|
||||
`echo "instcode_${p.port}=$(curl -s -o /dev/null -w '%{http_code}' --http1.1 http://${host}:${p.localPort}/)"`,
|
||||
)
|
||||
.join('; ')
|
||||
const out = new Map()
|
||||
for (const line of ssh(sshPort, target, cmd, r.num('SSH_TIMEOUT_MS')).split(/\r?\n/)) {
|
||||
const i = line.indexOf('=')
|
||||
if (i > 0) out.set(line.slice(0, i), line.slice(i + 1).trim())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* 派生「在册实例端点」——**唯一来源 = relay 自身 `/status.endpoints[]`**。
|
||||
*
|
||||
@@ -584,6 +635,68 @@ function parseInstanceFixture(value) {
|
||||
return { map, error: '' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 端点表**并集**(序㊾)—— 两台 relay **各自只看得见挂在自己身上的客户端** ⇒ 单看一台必然假红。
|
||||
*
|
||||
* 口径(三件事,缺一件就会造出另一类误判):
|
||||
* ① 键 = **`network:hostId:port`** —— 同一 `hostId` 在不同网络下是两条(relay 自己就按
|
||||
* `network/hostId` 索引会话);只按 `hostId:port` 去重会把两个网络**并成一条**。
|
||||
* ② `online` 取**或** —— "在任何一台看来在线"就是在线(离线那侧只是**没挂在我这儿**)。
|
||||
* ③ `localPort` 取**在线那一侧**的 —— 离线条目里的回环口是**已失效**的落点(拿它探活必得 `000`)。
|
||||
* `from` 记「这条最终采信谁的 `localPort`」⇒ 实例探活**必须回到那一台机器**上做(见调用点)。
|
||||
* ⛔ 本函数**只做合并**,⛔ 不判任何东西:判据仍由调用方按原阈值出 PASS/FAIL。
|
||||
* @param {any[]} list47 47 视角 `/status.endpoints[]`
|
||||
* @param {any[]} list106 对端中继 `/status.endpoints[]`(读不回来时传 `undefined`)
|
||||
* @returns {{ep:any, from:'47'|'106'}[]} 合并后的端点表(顺序:先 47 侧、后仅 106 有的)
|
||||
*/
|
||||
function mergeEndpoints(list47, list106) {
|
||||
const keyOf = (e) => `${e.network}:${e.hostId}:${e.port}`
|
||||
const byKey = new Map()
|
||||
const put = (e, from) => {
|
||||
if (e === null || typeof e !== 'object') return
|
||||
const key = keyOf(e)
|
||||
const cur = byKey.get(key)
|
||||
if (cur === undefined) {
|
||||
byKey.set(key, { ep: { ...e }, from })
|
||||
return
|
||||
}
|
||||
// 只在「新的一侧在线、已记的离线」时替换 ⇒ `localPort` 取自**在线那一侧**;同态时保留先来的(47 优先)。
|
||||
if (cur.ep.online !== true && e.online === true) byKey.set(key, { ep: { ...e }, from })
|
||||
}
|
||||
for (const e of Array.isArray(list47) ? list47 : []) put(e, '47')
|
||||
for (const e of Array.isArray(list106) ? list106 : []) put(e, '106')
|
||||
return [...byKey.values()]
|
||||
}
|
||||
|
||||
/**
|
||||
* 在册节点数的**并集**(序㊾):relay 的 `capacity.used` = **本机** `sessions.size`
|
||||
* ⇒ 单看一台会把挂在另一台上的节点**漏算**(`OBS-01` 假红)。
|
||||
*
|
||||
* 口径 = 按 **`network/hostId`** 去重后的**并集基数**。🔴 ⛔ **不是 `a.used + b.used`** ——
|
||||
* 同一节点在两台都残留会话时求和会**重复计数**(那是另一方向的**假绿**)。
|
||||
* ⚠️ 任一侧 `sessions[]` 不可用(对端读不回来 / 结构异常)⇒ 记 `exact=false` 并**回落**
|
||||
* 到 `max(各侧 capacity.used)`(= 退化成单侧口径,⛔ 不假装是并集)。
|
||||
* @returns {{used:number, exact:boolean}}
|
||||
*/
|
||||
function unionUsed(status47, status106) {
|
||||
const sides = [status47, status106]
|
||||
const keys = new Set()
|
||||
let complete = true
|
||||
for (const s of sides) {
|
||||
const ses = (s ?? {}).sessions
|
||||
if (!Array.isArray(ses)) {
|
||||
complete = false
|
||||
continue
|
||||
}
|
||||
for (const x of ses) if (x !== null && typeof x === 'object') keys.add(`${x.network}/${x.hostId}`)
|
||||
}
|
||||
if (complete) return { used: keys.size, exact: true }
|
||||
const nums = sides
|
||||
.map((s) => Number(((s ?? {}).capacity ?? {}).used))
|
||||
.filter((n) => Number.isFinite(n))
|
||||
return { used: nums.length === 0 ? Number.NaN : Math.max(...nums), exact: false }
|
||||
}
|
||||
|
||||
function readFixture(file) {
|
||||
if (!fs.existsSync(file)) throw new Error(`夹具不存在:${file}`)
|
||||
return fs.readFileSync(file, 'utf8')
|
||||
@@ -602,8 +715,9 @@ function usage() {
|
||||
' 🔴 序㊲ 起口径 = **按端口映射**(如 "--instance-fixture 21001=401");\n' +
|
||||
' ⛔ 旧的位置式 "<本机码>,<对端码>" 已退役 ⇒ 给了**直接报错退出**;\n' +
|
||||
' ⛔ 派生端口缺码 ⇒ 在册仍判 ⇒ FAIL 并点名(契约面,⛔ 不静默放行)\n' +
|
||||
' --peer-status-fixture <对端中继 /status 原文> ⬅️ 只有「47 视角端点表为空」的**可分**需要\n' +
|
||||
' (OBS-08 的空表归类 / OBS-09 的 SKIP 留痕;缺省 ⇒ 记"不可判(夹具模式)")\n' +
|
||||
' --peer-status-fixture <对端中继 /status 原文> ⬅️ 🆕 序㊾:**观测面并集的对端那一半**\n' +
|
||||
' (OBS-01 / OBS-08 / OBS-09 的并集数据源 + OBS-08「空表可分」的判别源;\n' +
|
||||
' 缺省 ⇒ 记"并集不可取证(夹具模式未给 …)"+ 仅按 47 视角判)\n' +
|
||||
' --content-fixture <content 块 JSON> ⬅️ 只有 OBS-17 需要(内容面判别器);缺省 ⇒ 无 content 块 ⇒ FAIL 并点名\n' +
|
||||
// 序㉖:OBS-19(抖动块)/ OBS-20(容量余量)**不需要新参数** —— 数据源就是 `--status-fixture`/真机 `/status`。
|
||||
' 🆕 序㉖:OBS-19(`status.jitter` 结构+口径)/ OBS-20(`capacity.utilPct < utilMaxPct`)复用上面的 /status 源\n' +
|
||||
@@ -631,7 +745,11 @@ function usage() {
|
||||
' --direct-fixture <直连自检读数 JSON>\n' +
|
||||
' (producer = `node scripts/overlay-direct-probe.cjs selfcheck --json`;\n' +
|
||||
' 真机 = `cat <DIRECT_SELFCHECK_FILE>`;**文件不存在 ⇒ 三条一起 SKIP + 留痕**;\n' +
|
||||
' 🔴 夹具模式缺本夹具 ⇒ SKIP + 留痕,⛔ 不去 ssh(序㊳ 的封闭性纪律))\n'
|
||||
' 🔴 夹具模式缺本夹具 ⇒ SKIP + 留痕,⛔ 不去 ssh(序㊳ 的封闭性纪律))\n' +
|
||||
// 序㊻:OBS-29(块 id 域分离 C)—— 数据源 = **本机进程内自检**(直取 lib)⇒ 无新参数、⛔ 零 ssh。
|
||||
' 🆕 序㊻:OBS-29(块 id per-network 域分离 C 真生效)**不需要新参数** —— 数据源 = 本机进程内自检\n' +
|
||||
' (正腿:跨网必不同且都非裸哈希 / 同网必相同;负腿:去 network 维度必红、取空必红)\n' +
|
||||
' ⚠️ 它判的是**实现与装配**;真机腿(content.blockIdKeyed + 两机指纹一致)待部署后补\n'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -719,6 +837,63 @@ function main() {
|
||||
* 窗口必须真的够长(≥ 若干倍轮询周期),否则"轮询还在跑"与"轮询停了"在计数上分不开。
|
||||
*/
|
||||
let status3
|
||||
/**
|
||||
* ── 对端(106)中继 `/status` 的**唯一取数入口**(序㊾)──
|
||||
*
|
||||
* 两台中继**各自只看得见挂在自己身上的客户端** ⇒ `OBS-01` / `OBS-08` / `OBS-09` 必须看**并集**。
|
||||
* 这里取一次、缓存一次 ⇒ 下面的"并集"与 `OBS-08` 的"空表可分"**共用同一份**(⛔ 零重复 ssh)。
|
||||
*
|
||||
* - 真机:**一次 ssh** 取回 `/status` 原文 + relay 单元活性(`__RELAY_ACTIVE__` 哨兵 —— 它把
|
||||
* "中继未运行"与"读取失败"分开)。
|
||||
* - 夹具:`--peer-status-fixture`;⛔ 缺省 ⇒ **不 ssh**,记"并集不可取证"(⚠️ 秩序不可颠倒:
|
||||
* `else if (fixture)` 必须排在 `else { ssh }` **之前**,否则夹具结论里会混进生产读数)。
|
||||
* @returns {{ok:boolean, status?:any, active?:string, why?:string}}
|
||||
*/
|
||||
let peerView
|
||||
const readPeerView = () => {
|
||||
if (peerView !== undefined) return peerView
|
||||
const target106 = r.need('SSH_TARGET_106')
|
||||
const fail = (why) => {
|
||||
peerView = { ok: false, why }
|
||||
return peerView
|
||||
}
|
||||
let raw
|
||||
if (peerStatusFx !== undefined) {
|
||||
try {
|
||||
raw = readFixture(peerStatusFx)
|
||||
} catch (err) {
|
||||
return fail(`夹具读取失败(⛔ 与"确实没挂"可分):${err.message}`)
|
||||
}
|
||||
} else if (fixture) {
|
||||
// ⛔ 夹具模式**绝不 ssh**(否则"夹具结论"里混进生产读数)。
|
||||
return fail('夹具模式未给 `--peer-status-fixture`(⛔ 不去 ssh)')
|
||||
} else {
|
||||
// 一次 ssh 取回**两件事**:`/status` 原文 + relay 单元活性(后者用于把"未运行"与"读取失败"分开)。
|
||||
try {
|
||||
raw = ssh(
|
||||
r.num('SSH_PORT'),
|
||||
target106,
|
||||
`curl -s ${r.need('RELAY_STATUS_URL')}; echo "__RELAY_ACTIVE__=$(systemctl is-active ${r.need('RELAY_UNIT_NAME')} 2>/dev/null || true)"`,
|
||||
r.num('SSH_TIMEOUT_MS'),
|
||||
)
|
||||
} catch (err) {
|
||||
return fail(`对端中继(${target106})ssh 读取失败(⛔ 与"确实没挂"可分):${err.message}`)
|
||||
}
|
||||
}
|
||||
const marker = raw.lastIndexOf('__RELAY_ACTIVE__=')
|
||||
const active = marker >= 0 ? raw.slice(marker + '__RELAY_ACTIVE__='.length).trim() : 'unknown'
|
||||
const body = marker >= 0 ? raw.slice(0, marker) : raw
|
||||
try {
|
||||
peerView = { ok: true, status: JSON.parse(body), active }
|
||||
return peerView
|
||||
} catch (err) {
|
||||
return fail(
|
||||
active === 'active'
|
||||
? `对端中继(${target106})在运行但 /status 解析失败(⛔ 读取失败,与"确实没挂"可分):${err.message}`
|
||||
: `对端中继(${target106})未给出可解析的 /status 且单元 is-active=${active}:${err.message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
let facts
|
||||
if (fixture) {
|
||||
process.stderr.write('⚠️ FIXTURE 本次为**夹具模式**:未连接任何远端,结论不得当生产判据\n')
|
||||
@@ -776,21 +951,44 @@ function main() {
|
||||
process.stderr.write(`❌ 第三次取 /status 失败:${err.message}\n`)
|
||||
return EXIT_USAGE
|
||||
}
|
||||
// 序㊲:实例面取样点 = **从 `/status.endpoints[]` 派生**(⛔ 不再读表内固定端口 ⇒ 端口漂移不再致假 SKIP)。
|
||||
const preEps = Array.isArray(status.endpoints) ? status.endpoints : []
|
||||
const probePairs = deriveInstanceEndpoints(preEps, agentPorts).map((e) => ({
|
||||
port: Number(e.port),
|
||||
localPort: Number(e.localPort),
|
||||
}))
|
||||
/**
|
||||
* 🆕 序㊾:对端中继视图 —— **放在第三次采样之后**。
|
||||
* ⚠️ 位置的三个理由:① 它读的是 **106 的 `counters`**,⛔ 不进 `OBS-16` 的 `ΔstatusHits` 算式;
|
||||
* ② `OBS-16` 的门窗口 = `(status2, status3]`,这段里**只有 `sleepSync`** ⇒ ⛔ 不进窗口;
|
||||
* ③ 对 47 `/status` 的读取次数**一次都没变**(仍是三次,复用已采到的那份)。
|
||||
*/
|
||||
const peerPre = readPeerView()
|
||||
// 序㊲ 的派生口径不变(仍在**并集**上做):⛔ 不读表内固定端口 ⇒ 端口漂移不致假 SKIP。
|
||||
const merged = mergeEndpoints(status.endpoints, peerPre.ok ? peerPre.status.endpoints : undefined)
|
||||
/**
|
||||
* 🔴 实例探活**必须回到"那一台"上做**:`localPort` 是**中继机回环**落点 ⇒ 并集里归属 106 的端点,
|
||||
* 其落点**只存在于 106**(在 47 上探必然 `000` = **假红**)⇒ 按 `from` 分机。
|
||||
*/
|
||||
const pairsOf = (from) =>
|
||||
deriveInstanceEndpoints(
|
||||
merged.filter((m) => m.from === from).map((m) => m.ep),
|
||||
agentPorts,
|
||||
).map((e) => ({ port: Number(e.port), localPort: Number(e.localPort) }))
|
||||
const pairs47 = pairsOf('47')
|
||||
const pairs106 = pairsOf('106')
|
||||
try {
|
||||
facts = remoteFacts(r, sshPort, target, probePairs)
|
||||
facts = remoteFacts(r, sshPort, target, pairs47)
|
||||
} catch (err) {
|
||||
process.stderr.write(`❌ 远端只读取数失败:${err.message}\n`)
|
||||
return EXIT_USAGE
|
||||
}
|
||||
// ⚠️ 只在并集里**真有** 106 侧在册实例时才发这次 ssh(正常态 = 零额外取数)。
|
||||
// 取不回来 ⇒ **不硬失败**:该端口的码缺席 ⇒ `OBS-09` 判 FAIL 并点名(与 curl 得 `000` 同款)。
|
||||
if (pairs106.length > 0) {
|
||||
try {
|
||||
for (const [k, v] of remoteInstanceCodes(r, sshPort, r.need('SSH_TARGET_106'), pairs106)) facts.set(k, v)
|
||||
} catch (err) {
|
||||
process.stderr.write(`⚠️ 对端(106)实例面取数失败(码缺席 ⇒ OBS-09 会 FAIL 并点名):${err.message}\n`)
|
||||
}
|
||||
}
|
||||
// 端口的码读不回来(curl 失败 ⇒ `000`)**不在这里兜底**:留给判据判 FAIL 并点名。
|
||||
codeByPort = new Map()
|
||||
for (const p of probePairs) {
|
||||
for (const p of [...pairs47, ...pairs106]) {
|
||||
const v = Number(facts.get(`instcode_${p.port}`))
|
||||
if (Number.isFinite(v)) codeByPort.set(p.port, v)
|
||||
}
|
||||
@@ -798,7 +996,23 @@ function main() {
|
||||
|
||||
const counters = status.counters ?? {}
|
||||
const cap = status.capacity ?? {}
|
||||
/**
|
||||
* **47 视角**端点表 —— 🔴 它**仍然是** `OBS-11` 的 `derived`(= **47 自己的**监听面)与
|
||||
* `OBS-02`(47 自身容量的自洽性)的数据源 ⇒ ⛔ **不许**被下面的并集替换(并集只消除"看不见")。
|
||||
*/
|
||||
const eps = Array.isArray(status.endpoints) ? status.endpoints : []
|
||||
/**
|
||||
* 🆕 序㊾:`OBS-01` / `OBS-08` / `OBS-09` 的**并集**数据源(口径见文件头同名小节)。
|
||||
* ⛔ 只换数据源 —— 阈值、派生规则、判据形态**一字未动**(真 FAIL 不许被糊成 PASS)。
|
||||
*/
|
||||
const peerViewNow = readPeerView()
|
||||
const epsUnion = mergeEndpoints(eps, peerViewNow.ok ? peerViewNow.status.endpoints : undefined).map((m) => m.ep)
|
||||
const usedUnion = unionUsed(status, peerViewNow.ok ? peerViewNow.status : undefined)
|
||||
/**
|
||||
* 并集不完整时**强制留痕**(⛔ 不静默降级):对端读不回来 ⇒ 这三项退化成 47 视角(**可能假红**)。
|
||||
* ⚠️ 与之对照:`OBS-08` 的"空表可分"仍会按 §文件头把它判成 FAIL 并点名(⛔ 不静默当绿)。
|
||||
*/
|
||||
const unionNote = peerViewNow.ok ? '' : ` |⚠️ 并集不可取证(${peerViewNow.why})⇒ 本项仅按 47 视角判`
|
||||
|
||||
const relayBind = r.need('RELAY_BIND')
|
||||
const required = parseList(r.need('LISTEN_REQUIRED'))
|
||||
@@ -807,7 +1021,11 @@ function main() {
|
||||
// 拨号池**不另立键**(⛔ 避免两处漂移):复用既有 `DIAL_POOL_BOUND`,主机取 `RELAY_BIND`。
|
||||
ranges.push(...parseRanges(r.need('DIAL_POOL_BOUND'), relayBind, r.bad))
|
||||
const nftAllowed = parseList(r.need('NFT_ALLOW_INBOUND'))
|
||||
// `derived` 的**唯一来源** = relay 自身 `/status` 的端点回环落点(动态值 ⇒ ⛔ 不许写死进参数表)。
|
||||
/**
|
||||
* `derived` 的**唯一来源** = relay 自身 `/status` 的端点回环落点(动态值 ⇒ ⛔ 不许写死进参数表)。
|
||||
* 🔴 序㊾:这里是 **47 视角**的 `eps`、⛔ **不是**并集 —— 它要回答的是「**47 这台机上**有哪些监听
|
||||
* 是合法派生出来的」;把 106 的落点并进来等于**凭空放宽** `OBS-11` 的"多出"判据。
|
||||
*/
|
||||
const derived = new Set(eps.map((e) => `${relayBind}:${e.localPort}`))
|
||||
|
||||
// 🆕 序㊱:两个新键**必须在 `r.bad` 检查之前**取出 —— `need()` 是**惰性**的(调用才记账),
|
||||
@@ -838,15 +1056,16 @@ function main() {
|
||||
rows.push(skip || (fixture && judged !== true) ? { id, ok: true, skip: true, text } : { id, ok, text })
|
||||
|
||||
/**
|
||||
* ── 「47 视角端点表为空」的**可分**(序㊲;详见文件头 `OBS-08` 同名小节)──
|
||||
* ── 「**两台中继都**看不到该客户端」的**可分**(序㊲;详见文件头 `OBS-08` 同名小节)──
|
||||
*
|
||||
* 两个成因**必须分开**(旧口径一律 FAIL ⇒ 其中一个是**假红**):
|
||||
* ⓐ 客户端挂在**另一台中继**上(拓扑态,⛔ 非故障)⇒ `elsewhere`
|
||||
* ⓑ 两台都看不到它的会话 ⇒ 确实没挂客户端 ⇒ `nowhere`
|
||||
* ⓒ 对端中继读不回来 ⇒ 不可判 ⇒ `unknown`(⛔ 不许静默当绿)
|
||||
*
|
||||
* 🔴 **只在需要时才去读**(47 视角非空的正常态 ⇒ **零额外 ssh**);结果缓存(两个判据共用一次读)。
|
||||
* 🔴 判别源 = **对端中继自身 `/status`**(回环、只读)⇒ ⛔ 零新增暴露面、⛔ 零新增参数键。
|
||||
* 🆕 序㊾:这份取数已收敛到 `readPeerView()` **一处**(与并集共用同一份 ⇒ ⛔ 零重复 ssh);
|
||||
* ⇒ 本分支现在只在「**并集也为空**」时才走到(单看 47 为空、其实挂在对端 ⇒ 已由并集直接判绿)。
|
||||
*/
|
||||
let emptyView
|
||||
const classifyEmptyView = () => {
|
||||
@@ -856,42 +1075,10 @@ function main() {
|
||||
emptyView = { kind: 'unknown', trace: `对端中继(${target106})**不可判**:${why}` }
|
||||
return emptyView
|
||||
}
|
||||
let raw
|
||||
if (peerStatusFx !== undefined) {
|
||||
try {
|
||||
raw = readFixture(peerStatusFx)
|
||||
} catch (err) {
|
||||
return unknown(`夹具读取失败(⛔ 与"确实没挂"可分):${err.message}`)
|
||||
}
|
||||
} else if (fixture) {
|
||||
// ⛔ 夹具模式**绝不 ssh**(否则"夹具结论"里混进生产读数)。
|
||||
return unknown('夹具模式未给 `--peer-status-fixture`(⛔ 不去 ssh)')
|
||||
} else {
|
||||
// 一次 ssh 取回**两件事**:`/status` 原文 + relay 单元活性(后者用于把"未运行"与"读取失败"分开)。
|
||||
try {
|
||||
raw = ssh(
|
||||
r.num('SSH_PORT'),
|
||||
target106,
|
||||
`curl -s ${r.need('RELAY_STATUS_URL')}; echo "__RELAY_ACTIVE__=$(systemctl is-active ${r.need('RELAY_UNIT_NAME')} 2>/dev/null || true)"`,
|
||||
r.num('SSH_TIMEOUT_MS'),
|
||||
)
|
||||
} catch (err) {
|
||||
return unknown(`ssh 读取失败(⛔ 与"确实没挂"可分):${err.message}`)
|
||||
}
|
||||
}
|
||||
const marker = raw.lastIndexOf('__RELAY_ACTIVE__=')
|
||||
const active = marker >= 0 ? raw.slice(marker + '__RELAY_ACTIVE__='.length).trim() : 'unknown'
|
||||
const body = marker >= 0 ? raw.slice(0, marker) : raw
|
||||
let peerStatus
|
||||
try {
|
||||
peerStatus = JSON.parse(body)
|
||||
} catch (err) {
|
||||
return unknown(
|
||||
active === 'active'
|
||||
? `对端中继在运行但 /status 解析失败(⛔ 读取失败,与"确实没挂"可分):${err.message}`
|
||||
: `对端中继未给出可解析的 /status 且单元 is-active=${active}:${err.message}`,
|
||||
)
|
||||
}
|
||||
const pv = readPeerView()
|
||||
if (!pv.ok) return unknown(pv.why)
|
||||
const active = pv.active
|
||||
const peerStatus = pv.status
|
||||
const peerSessions = Array.isArray(peerStatus.sessions) ? peerStatus.sessions : []
|
||||
// 「有会话在声明端口」= 该客户端**确实挂在**对端中继上(agent 面也算 —— 它只说明"实例面还没声明")。
|
||||
const attached = peerSessions.filter((s) => Array.isArray(s.ports) && s.ports.length > 0)
|
||||
@@ -918,22 +1105,24 @@ function main() {
|
||||
}
|
||||
|
||||
/**
|
||||
* ── `OBS-09`:**在册 ⇒ 必须可达**(序 ㊲ 口径修正:判据从事实派生、⛔ 不再依赖表内固定端口)──
|
||||
* ── `OBS-09`:**在册 ⇒ 必须可达**(序 ㊲ 口径修正 / 🆕 序㊾ 数据源换并集)──
|
||||
*
|
||||
* 两种模式**都要判** ⇒ 单独成函数,⛔ 不塞进 `!fixture` 分支(否则夹具模式证不了
|
||||
* "既能判 PASS 也能判 FAIL")。`eps` / `codeSet` / `add` 均已就绪。
|
||||
* "既能判 PASS 也能判 FAIL")。`epsUnion` / `codeSet` / `add` 均已就绪。
|
||||
* 🔴 序㊾:派生改在**并集**上做(单看 47 ⇒ worker 挂到 106 自家中继时派生为空 ⇒ **假 SKIP**);
|
||||
* ⛔ 判据本身(在线 + 非 agent 口 ⇒ 必须可达且码 ∈ 集合)**一字未动**。
|
||||
*/
|
||||
const judgeObs09 = () => {
|
||||
// 「在册」= `/status.endpoints[]` 里 `online === true` 且 **非 agent 端口**的实例端点(⛔ 表值不参与)。
|
||||
const reg = deriveInstanceEndpoints(eps, agentPorts)
|
||||
const agentOnly = eps.filter((e) => e.online === true && agentPorts.has(Number(e.port))).length
|
||||
const offline = eps.filter((e) => e.online !== true).length
|
||||
const reg = deriveInstanceEndpoints(epsUnion, agentPorts)
|
||||
const agentOnly = epsUnion.filter((e) => e.online === true && agentPorts.has(Number(e.port))).length
|
||||
const offline = epsUnion.filter((e) => e.online !== true).length
|
||||
if (reg.length === 0) {
|
||||
add(
|
||||
'OBS-09',
|
||||
true,
|
||||
`在册实例面 无(从 /status.endpoints[] 派生为空:端点表 ${eps.length} 条 = agent ${agentOnly} 条 + 离线 ${offline} 条)` +
|
||||
` ⇒ SKIP + 留痕|${classifyEmptyView().trace}`,
|
||||
`在册实例面 无(从并集端点表派生为空:并集 ${epsUnion.length} 条 = agent ${agentOnly} 条 + 离线 ${offline} 条)` +
|
||||
` ⇒ SKIP + 留痕|${classifyEmptyView().trace}${unionNote}`,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
@@ -951,10 +1140,11 @@ function main() {
|
||||
add(
|
||||
'OBS-09',
|
||||
unknown.length === 0 && bad.length === 0,
|
||||
`在册实例面(派生 ${reg.length} 条):${probes.map(show).join(' ')}` +
|
||||
`在册实例面(并集派生 ${reg.length} 条):${probes.map(show).join(' ')}` +
|
||||
` (阈值 ∈ {${codeSet.join(',')}})` +
|
||||
`${bad.length > 0 ? ` |❌ ${bad.map(show).join(' ')}` : ''}` +
|
||||
`${unknown.length > 0 ? ` |❌ 缺码 ${unknown.map((p) => p.port).join(',')}(夹具未给该端口的码 / 真机取不到)` : ''}`,
|
||||
`${unknown.length > 0 ? ` |❌ 缺码 ${unknown.map((p) => p.port).join(',')}(夹具未给该端口的码 / 真机取不到)` : ''}` +
|
||||
`${unionNote}`,
|
||||
true,
|
||||
)
|
||||
}
|
||||
@@ -968,15 +1158,15 @@ function main() {
|
||||
* ⚠️ 数据源就是 `/status` ⇒ **两种模式都要判**(与 `OBS-09` 同规则:夹具模式也要能证"既可 PASS 也可 FAIL")。
|
||||
*/
|
||||
const judgeObs08 = () => {
|
||||
if (eps.length === 0) {
|
||||
if (epsUnion.length === 0) {
|
||||
const cls = classifyEmptyView()
|
||||
if (cls.kind === 'elsewhere') {
|
||||
add('OBS-08', true, `端点表 0 条 ⇒ SKIP + 留痕|${cls.trace}`, true, true)
|
||||
add('OBS-08', true, `并集端点表 0 条 ⇒ SKIP + 留痕|${cls.trace}`, true, true)
|
||||
} else {
|
||||
add(
|
||||
'OBS-08',
|
||||
false,
|
||||
`端点表 0 条 ⇒ FAIL(${cls.kind === 'nowhere' ? '两台中继都无该客户端会话' : '不可判'})|${cls.trace}`,
|
||||
`并集端点表 0 条 ⇒ FAIL(${cls.kind === 'nowhere' ? '两台中继都无该客户端会话' : '不可判'})|${cls.trace}`,
|
||||
true,
|
||||
)
|
||||
}
|
||||
@@ -984,14 +1174,23 @@ function main() {
|
||||
}
|
||||
add(
|
||||
'OBS-08',
|
||||
eps.every((e) => e.online === true),
|
||||
`端点表 ${eps.length} 条 / 离线 ${eps.filter((e) => e.online !== true).length} 条`,
|
||||
epsUnion.every((e) => e.online === true),
|
||||
`端点表(并集) ${epsUnion.length} 条 / 离线 ${epsUnion.filter((e) => e.online !== true).length} 条` +
|
||||
` |并集: 47 视角 ${eps.length} 条 + 对端 ${peerViewNow.ok ? (Array.isArray(peerViewNow.status.endpoints) ? peerViewNow.status.endpoints.length : 0) : '?'} 条 去重后 ${epsUnion.length} 条` +
|
||||
`${unionNote}`,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if (!fixture) {
|
||||
add('OBS-01', Number(cap.used) >= r.num('MIN_HOSTS'), `在册节点 used=${cap.used} (阈值 ≥ ${r.num('MIN_HOSTS')})`)
|
||||
add(
|
||||
'OBS-01',
|
||||
usedUnion.used >= r.num('MIN_HOSTS'),
|
||||
`在册节点 used=${usedUnion.used} (阈值 ≥ ${r.num('MIN_HOSTS')})` +
|
||||
` |并集: 47 视角 used=${cap.used} + 对端 used=${peerViewNow.ok ? ((peerViewNow.status.capacity ?? {}).used ?? '?') : '?'}` +
|
||||
`${usedUnion.exact ? ' ⇒ 按 network/hostId 去重后计' : ' ⇒ ⚠️ sessions[] 不全、回落 max(各侧 used)(⛔ 不假装是并集)'}` +
|
||||
`${unionNote}`,
|
||||
)
|
||||
add(
|
||||
'OBS-02',
|
||||
Number(cap.max) === r.num('RELAY_MAX_HOSTS') && Number(cap.free) === Number(cap.max) - Number(cap.used),
|
||||
@@ -1187,6 +1386,8 @@ function main() {
|
||||
dHitsGate <= r.num('PRESENCE_GATE_HITS_MAX'),
|
||||
`门窗口 ΔstatusHits=${dHitsGate}(≥ 1 = 探针自身读数|≤ ${r.num('PRESENCE_GATE_HITS_MAX')} = 除探针外**零**命中)` +
|
||||
`,窗口 ${r.num('PRESENCE_GATE_WINDOW_MS')}ms,期间 subs=${counters3.subs}` +
|
||||
// 🆕 序㊾:对端那份是**另算的独立采样**(读 106 的 counters),⛔ 不进本算式 —— 显式留痕,防日后误并。
|
||||
` |Δ 口径 = **仅 47** 的 counters(对端 106 的采样另算一次、⛔ 不入 Δ;对 47 的读数次数仍为三次)` +
|
||||
`${counters3Ok ? '' : ' ❌ 判别器缺'}`,
|
||||
true,
|
||||
)
|
||||
@@ -2222,6 +2423,125 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ── `OBS-29`:**块 id 的 per-network 域分离(C)真生效**(序㊻ · `04-133 §3`)──
|
||||
*
|
||||
* 🔴 本行是**判据重裁**的产物:原设计的负腿("稀释源换成确定性派生量 ⇒ 必红")随 **D 被实测
|
||||
* 判为"作用面为空"**(序㊺)而**失去对象** ⇒ 重裁为 **C 自己的判据面**(编号仍自 `OBS-29` 起)。
|
||||
*
|
||||
* 判据(⛔ **正腿与负腿缺一不可**):
|
||||
* ① **正腿 P1**:同字节 + **不同 network** ⇒ 块 id **不同**,**且两侧都 ≠ 裸哈希**
|
||||
* (⚠️ 只判"不同"不够:两侧**都**回落裸哈希时也会"不同"以外的形态漏判 —— 所以必须同时钉住
|
||||
* "都不等于裸哈希",这才是"域分离真的生效"而不是"两边走了两条不同的错路")。
|
||||
* ② **正腿 P2**:同 network + 同字节 ⇒ 块 id **相同**(⛔ 只测 P1 会漏掉"**去重被干掉**")。
|
||||
* ③ **正腿 P3**:`store` 写侧复算(`putRejected = 0`)+ 读侧复算(`corruptReads = 0`)
|
||||
* + `chunker#reassemble` 用同一把域密钥能重组回原内容 ⇒ **证调用点无漏改**
|
||||
* (🔴 漏一处 ⇒ 写侧复算必抛 ⇒ 本腿必红)。
|
||||
* ④ 🔴 **负腿 N1(必须真跑、必须具名 `flat-key-collapses`)**:把 `network` 维度**去掉**
|
||||
* (两侧取同一 network ⇒ 同一把域密钥)⇒ ①的谓词**必须转假**。
|
||||
* ⑤ 🔴 **负腿 N2(具名 `empty-key-falls-back-to-bare-hash`)**:`netKey` **取空**
|
||||
* ⇒ 谓词**必须转假**,且**取空与生效必须可分**(`keyed ≠ bare`)。
|
||||
*
|
||||
* 数据源 = **本机进程内自检**(直取 `lib/net/relay/content/*`)⇒ ⛔ **零 ssh、零生产依赖、
|
||||
* 夹具模式照跑**(故 `judged = true`)。⚠️ 它判的是"**实现与装配**",不判"生产已生效"。
|
||||
*
|
||||
* ⏳ **真机腿(登记为回头条件 · 部署那一棒必须补)**:`/status.content` 断言
|
||||
* `blockIdKeyed === true`,且 **47 与 106 的 `content.blockIdKeyId` 逐字相同** ——
|
||||
* 跨机口径不一致 ⇒ 跨机取块**全部**判校验失败(块本身是好的,最难定位的形态)。
|
||||
*/
|
||||
{
|
||||
const LEG_NAMES = [
|
||||
'P1-跨网必不同且都非裸哈希',
|
||||
'P2-同网必相同(去重未丢)',
|
||||
'P3-装配面无漏改',
|
||||
'N1-flat-key-collapses',
|
||||
'N2-empty-key-falls-back-to-bare-hash',
|
||||
]
|
||||
const leg = {}
|
||||
let hardFail
|
||||
let detail = ''
|
||||
try {
|
||||
const chunker = require('../lib/net/relay/content/chunker.js')
|
||||
const cmod = require('../lib/net/relay/content/crypto.js')
|
||||
const rtmod = require('../lib/net/relay/content/runtime.js')
|
||||
// 🔴 产物必须**已换代**:旧 `lib` 里没有这两个导出 ⇒ FAIL 点名(这正是"装了没生效"的机器判据)。
|
||||
if (
|
||||
typeof cmod.deriveBlockIdKey !== 'function' ||
|
||||
typeof cmod.ContentCipher !== 'function' ||
|
||||
typeof rtmod.ContentRuntime !== 'function'
|
||||
) {
|
||||
hardFail = '❌ `lib` 产物未换代(缺 `deriveBlockIdKey` / `ContentCipher` / `ContentRuntime`)⇒ 先 `npm run build`'
|
||||
} else {
|
||||
const key = Buffer.alloc(cmod.KEY_LEN, 0x5a)
|
||||
const bytes = Buffer.alloc(1024 * 1024 + 3, 0)
|
||||
for (let i = 0; i < bytes.length; i += 1) bytes[i] = (i * 31 + 7) & 0xff // 确定性、逐块不同
|
||||
const cipher = new cmod.ContentCipher({ groupKey: 'ops|content', epoch: 1, key })
|
||||
const rtOf = (network) => new rtmod.ContentRuntime({ network, group: 'content', cipher })
|
||||
const idsOf = (network) => rtOf(network).planContent(bytes).ids
|
||||
const bare = chunker.planOf(bytes).ids
|
||||
const a = idsOf('ops')
|
||||
const b = idsOf('u:1')
|
||||
const same = idsOf('ops')
|
||||
// ① P1 —— 跨网必不同,且两侧都不是裸哈希(**逐块同位比对**,⛔ 不做跨位比对)
|
||||
const crossDiff = a.length === b.length && a.every((x, i) => x !== b[i])
|
||||
leg['P1-跨网必不同且都非裸哈希'] =
|
||||
crossDiff && a.every((x, i) => x !== bare[i]) && b.every((x, i) => x !== bare[i])
|
||||
// ② P2 —— 同网必相同(去重还在)
|
||||
leg['P2-同网必相同(去重未丢)'] = same.length === a.length && same.every((x, i) => x === a[i])
|
||||
// ③ P3 —— 装配面:写侧复算 + 读侧复算 + 重组位,三处都要过
|
||||
const rt = rtOf('ops')
|
||||
const put = rt.putContent(bytes)
|
||||
const snap = rt.snapshot()
|
||||
const readBack = rt.store.get(put.plan[0])
|
||||
const parts = new Map()
|
||||
for (const id of put.plan) parts.set(id, rt.store.get(id))
|
||||
let reOk = false
|
||||
try {
|
||||
const back = chunker.reassemble(put.plan, parts, {
|
||||
netKey: rt.netKey,
|
||||
decode: (x) => cipher.decodeBlock(x),
|
||||
})
|
||||
reOk = back.equals(bytes)
|
||||
} catch {
|
||||
reOk = false
|
||||
}
|
||||
leg['P3-装配面无漏改'] =
|
||||
snap.store.putRejected === 0 && snap.store.corruptReads === 0 && readBack !== undefined && reOk
|
||||
detail =
|
||||
`写侧 putRejected=${snap.store.putRejected}(须 0)|读侧 corruptReads=${snap.store.corruptReads}(须 0)` +
|
||||
`|重组位=${reOk ? '逐字节相同 ✓' : '❌'}`
|
||||
// ④ N1 —— 去掉 network 维度(两侧同一 network ⇒ 同一把域密钥)⇒ 谓词必须转假
|
||||
const flatA = idsOf('ops')
|
||||
const flatB = idsOf('ops')
|
||||
leg['N1-flat-key-collapses'] = flatA[0] === flatB[0] && flatA[0] !== bare[0]
|
||||
// ⑤ N2 —— netKey 取空 ⇒ 谓词必须转假,且取空与生效可分
|
||||
const kOn = cmod.deriveBlockIdKey(key, 'ops')
|
||||
leg['N2-empty-key-falls-back-to-bare-hash'] =
|
||||
chunker.blockIdOf(bytes.subarray(0, 64)) === chunker.blockIdOf(bytes.subarray(0, 64)) &&
|
||||
chunker.blockIdOf(bytes.subarray(0, 64), kOn) !== chunker.blockIdOf(bytes.subarray(0, 64))
|
||||
}
|
||||
} catch (err) {
|
||||
hardFail = `❌ 自检抛错:${err.message}`
|
||||
}
|
||||
if (hardFail !== undefined) {
|
||||
add('OBS-29', false, `块 id 域分离(C)真生效:${hardFail}`, true)
|
||||
} else {
|
||||
const missing = LEG_NAMES.filter((n) => leg[n] !== true)
|
||||
const ok = missing.length === 0
|
||||
add(
|
||||
'OBS-29',
|
||||
ok,
|
||||
`块 id 域分离(C · per-network keyed hash)腿数 ${LEG_NAMES.length - missing.length}/${LEG_NAMES.length}` +
|
||||
`${ok ? ' 全绿 ✅' : ` ❌ 缺 ${missing.join(',')}`}|${leg['P1-跨网必不同且都非裸哈希'] ? 'P1 ✅' : 'P1 ❌'}` +
|
||||
` ${leg['P2-同网必相同(去重未丢)'] ? 'P2 ✅' : 'P2 ❌'} ${leg['P3-装配面无漏改'] ? 'P3 ✅' : 'P3 ❌'}` +
|
||||
` ${leg['N1-flat-key-collapses'] ? 'N1 ✅(⛔ 判据有牙)' : 'N1 ❌'}` +
|
||||
` ${leg['N2-empty-key-falls-back-to-bare-hash'] ? 'N2 ✅' : 'N2 ❌'}|${detail}` +
|
||||
`|⏳ 真机腿(content.blockIdKeyed + 两机 blockIdKeyId 逐字相同)待部署后补`,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = fixture ? '⚠️ FIXTURE ' : ''
|
||||
for (const row of rows) { process.stdout.write(`${prefix}${row.skip === true ? 'SKIP' : row.ok ? 'PASS' : 'FAIL'} ${row.id} ${row.text}\n`)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user