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
+38
-2
@@ -253,16 +253,42 @@ export function buildWorkerAgent(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* **本 worker 此刻真的在管的实例端口**(对账口径的**唯一**来源)。
|
||||
*
|
||||
* 🔴 覆盖网络线 序 ㊽:必须是**两条腿的并集** ——
|
||||
* ① `listUserInstances()` = 本进程 `launch` 过的(`LocalSpawner.mains`);
|
||||
* ② `adoptedInstancePorts()` = 本进程**认领**来的存量实例(⛔ 不进 `mains`,见其文件头 序 ㉕)。
|
||||
*
|
||||
* 只取 ① 就是本次实测的缺陷:worker 重启后 `mains` 空 ⇒ 上一进程遗留的实例端口**没人重新声明**,
|
||||
* relay 端点表里只留一条 `online=false` 的孤儿条目(实测 47 侧 `w-106:19000`)⇒ Manager 侧
|
||||
* `(hostId, port)` 翻译不出来。并进 ② 之后,`forward(port)` 会把那条孤儿**就地覆盖**成在线
|
||||
* (relay 侧 `ensureEndpoint` 复用既有条目、只换绑定会话,⛔ 不新开口、⛔ 不动白名单)。
|
||||
*/
|
||||
const liveInstancePorts = async (): Promise<Set<number>> => {
|
||||
const live = new Set(
|
||||
(await spawner.listUserInstances()).map((i) => i.port).filter((p): p is number => p !== undefined),
|
||||
)
|
||||
for (const port of spawner.adoptedInstancePorts()) live.add(port)
|
||||
return live
|
||||
}
|
||||
|
||||
/** 把活着的实例端口补齐、把已消失的撤掉(崩溃退出也走这里收敛,不必逐个挂 exit 钩子)。 */
|
||||
const reconcileTunnel = async (): Promise<void> => {
|
||||
if (tunnel === undefined || !tunnelReady) return
|
||||
const live = new Set((await spawner.listUserInstances()).map((i) => i.port).filter((p): p is number => p !== undefined))
|
||||
const live = await liveInstancePorts()
|
||||
for (const port of live) await tunnel.forward(port)
|
||||
for (const port of tunnel.ports) {
|
||||
if (!live.has(port) && !staticPorts.includes(port)) await tunnel.cancel(port)
|
||||
}
|
||||
}
|
||||
|
||||
/** 隧道一拍 = 自愈 + 对账(序 ㊽:定时器与"认领落定"回调**共用同一份语义**,⛔ 不写第二套)。 */
|
||||
const tunnelTick = async (): Promise<void> => {
|
||||
await healTunnel()
|
||||
await reconcileTunnel()
|
||||
}
|
||||
|
||||
let tunnelTimer: NodeJS.Timeout | undefined
|
||||
if (tunnel !== undefined) {
|
||||
void tunnel
|
||||
@@ -274,9 +300,19 @@ export function buildWorkerAgent(
|
||||
console.error('[tunnel] 建立失败(跨机代理将不可用,本机功能不受影响):', err instanceof Error ? err.message : err)
|
||||
})
|
||||
tunnelTimer = setInterval(() => {
|
||||
void healTunnel().then(reconcileTunnel)
|
||||
void tunnelTick()
|
||||
}, 20_000)
|
||||
tunnelTimer.unref?.()
|
||||
/**
|
||||
* 序 ㊽:**认领(rehydrate)落定即登记** —— ⛔ 不等 20 s 对账节拍。
|
||||
*
|
||||
* 认领来的存量实例端口**只有这一条路**会被声明给 relay(它们不进 `mains`)⇒ 若只靠 20 s
|
||||
* 定时器,重启后有一段"实例活着但中继查不到落点"的窗口;本回调把它压到探活落定的那一刻。
|
||||
* ⚠️ 定时器**保留**:它仍是断链自愈 / 端口漂移的兜底(回调只负责"起步那一拍")。
|
||||
*/
|
||||
spawner.onRehydrateSettled = () => {
|
||||
void tunnelTick()
|
||||
}
|
||||
}
|
||||
const app = Fastify({ logger: { level: options.logLevel ?? 'info' }, bodyLimit: MAX_PATCH_BYTES + 4096 })
|
||||
const cache: OpCache = { order: [], results: new Map() }
|
||||
|
||||
Reference in new issue
Block a user