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
@@ -343,6 +343,28 @@ export class LocalSpawner implements Spawner {
|
||||
notes: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖网络线 序 ㊽:**本轮认领的探活全部落定**之后的回调。
|
||||
*
|
||||
* ## 为什么需要它(这正是一个实测缺陷的修法)
|
||||
* 认领来的存量实例**刻意不进 `mains`**(文件头 序 ㉕ 的边界:`launchToken` 不可恢复),
|
||||
* 而 `listUserInstances()` 的口径**就是 `mains`** ⇒ 上一进程遗留的实例监听端口**没有任何人**
|
||||
* 会向 relay 重新声明一遍。实测症状(2026-09-19):106 的实例 `:21001` 进程健在、
|
||||
* `[rehydrate] probe OK` 也打了,但两台中继的端点表里都没有它(47 侧只留一条
|
||||
* `w-106:19000 online=false` 的**孤儿**条目)⇒ Manager 侧 `(hostId, port)` 翻译不出来。
|
||||
*
|
||||
* ⇒ 由 **worker agent** 接这个回调,把 `adoptedInstancePorts()` 并进对账口径(⛔ 不改认领语义、
|
||||
* ⛔ 不把认领实例写进 `mains`):端口一落定就登记,**不必等 20 s 对账节拍**。
|
||||
* ⚠️ 缺省 `undefined` ⇒ 行为与改造前**逐字一致**(其他装配点不受影响)。
|
||||
*/
|
||||
onRehydrateSettled: (() => void) | undefined
|
||||
|
||||
/** 序 ㊽:本轮仍在途的探活条数。探活是**异步**的(socket 事件)⇒ ⛔ 不能只看 `schedule` 的基例。 */
|
||||
private pendingProbes = 0
|
||||
|
||||
/** 序 ㊽:本轮 `schedule` 已走完(不会再产生新探活)。`true` + `pendingProbes === 0` ⇒ 落定。 */
|
||||
private rehydrateScheduled = false
|
||||
|
||||
constructor(
|
||||
private readonly config: ServerConfig,
|
||||
/** Resolve the user's own API key (decrypted); null = user has none. */
|
||||
@@ -1411,16 +1433,62 @@ export class LocalSpawner implements Spawner {
|
||||
return { ...this.rehydrate, notes: [...this.rehydrate.notes] }
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖网络线 序 ㊽:**已认领且探活通过**的实例监听端口(worker agent 的对账口径之一)。
|
||||
*
|
||||
* 与 {@link listUserInstances} 的关系 = **互补,不合并**:那个的口径是"本进程 launch 过的"
|
||||
* (`mains`),这个是"本进程**接管**的"(`adopted`)。认领来的实例没有 `launchToken`、
|
||||
* 也进不了 `status(userId)`(文件头 序 ㉕ 的客观边界,⛔ 未改),但"**这个端口在听、且归本
|
||||
* worker 管**"与实例身份无关 ⇒ 该登记给 relay 就得登记(否则跨机代理查不到落点)。
|
||||
*
|
||||
* ⚠️ 只报 `alive === true` 的:探活没过的那条已被 `probeAdopted` 按旧行为停掉了,
|
||||
* 报出去只会让上游去登记一个死口。⛔ 未出生的探活(`alive === undefined`)同样不报。
|
||||
*/
|
||||
adoptedInstancePorts(): number[] {
|
||||
const out: number[] = []
|
||||
for (const rec of this.adopted.values()) {
|
||||
if (rec.alive === true && rec.info.port !== undefined) out.push(rec.info.port)
|
||||
}
|
||||
return out.sort((a, b) => a - b)
|
||||
}
|
||||
|
||||
/**
|
||||
* 序 ㊽:本轮 `schedule` 收尾 —— ⚠️ **探活可能还没落定**(异步)⇒ 只置标志,
|
||||
* 由 {@link probeAdopted} 的最后一条补射(见 {@link maybeRehydrateSettled})。
|
||||
*/
|
||||
private settleRehydrate(): void {
|
||||
this.rehydrateScheduled = true
|
||||
this.maybeRehydrateSettled()
|
||||
}
|
||||
|
||||
/** 序 ㊽:`schedule` 走完 + 在途探活归零 ⇒ **恰好通知一次**(幂等;回调抛错⛔不许拖垮编排器)。 */
|
||||
private maybeRehydrateSettled(): void {
|
||||
if (!this.rehydrateScheduled || this.pendingProbes > 0) return
|
||||
this.rehydrateScheduled = false
|
||||
try {
|
||||
this.onRehydrateSettled?.()
|
||||
} catch (err) {
|
||||
process.stderr.write(
|
||||
`[rehydrate] ⚠️ 落定回调抛错(已吞,不阻断):${err instanceof Error ? err.message : String(err)}\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private rehydrateAdoptedScopes(): void {
|
||||
// 序 ㊽:新一轮认领起算 —— 上一轮的落定标志必须清掉(否则残留的 `true` 会让本轮提前通知)。
|
||||
this.pendingProbes = 0
|
||||
this.rehydrateScheduled = false
|
||||
// ① 非 account 形态不产生 scope ⇒ 保持旧语义(此处是空操作)。
|
||||
if (this.config.isolationMode !== 'account') {
|
||||
this.cleanAllStaleScopes()
|
||||
this.settleRehydrate()
|
||||
return
|
||||
}
|
||||
const found = this.scanExistingScopes()
|
||||
this.rehydrate.scanned = found.length
|
||||
if (found.length === 0) {
|
||||
process.stderr.write('[rehydrate] 无既有实例 scope ⇒ 不动作(与旧行为等价)\n')
|
||||
this.settleRehydrate()
|
||||
return
|
||||
}
|
||||
const perUid = new Map<number, number>()
|
||||
@@ -1429,6 +1497,7 @@ export class LocalSpawner implements Spawner {
|
||||
const schedule = (i: number): void => {
|
||||
if (i >= found.length) {
|
||||
process.stderr.write(`[rehydrate] summary ${JSON.stringify(this.rehydrateReport())}\n`)
|
||||
this.settleRehydrate()
|
||||
return
|
||||
}
|
||||
const t = setTimeout(() => {
|
||||
@@ -1505,9 +1574,13 @@ export class LocalSpawner implements Spawner {
|
||||
this.rehydrate.stopped += 1
|
||||
this.adopted.delete(rec.unit)
|
||||
this.stopUnit(rec.unit)
|
||||
// 序 ㊽:这一条**不产生**探活 ⇒ 若此刻已是本轮最后一条,落定通知不能等它。
|
||||
this.maybeRehydrateSettled()
|
||||
return
|
||||
}
|
||||
let settled = false
|
||||
// 序 ㊽:在途计数 —— 探活由 socket 事件驱动,`schedule` 的基例可能先于它跑完。
|
||||
this.pendingProbes += 1
|
||||
const sock = connect({ host: '127.0.0.1', port })
|
||||
const done = (ok: boolean): void => {
|
||||
if (settled) return
|
||||
@@ -1521,15 +1594,17 @@ export class LocalSpawner implements Spawner {
|
||||
if (ok) {
|
||||
this.rehydrate.probeOk += 1
|
||||
process.stderr.write(`[rehydrate] probe OK ${rec.unit} :${port}\n`)
|
||||
return
|
||||
} else {
|
||||
this.rehydrate.probeFail += 1
|
||||
const note = `probe-fail ${rec.unit} :${port}`
|
||||
this.rehydrate.notes.push(note)
|
||||
process.stderr.write(`[rehydrate] ⛔ ${note} ⇒ 判孤儿,按旧行为停掉\n`)
|
||||
this.rehydrate.stopped += 1
|
||||
this.adopted.delete(rec.unit)
|
||||
this.stopUnit(rec.unit)
|
||||
}
|
||||
this.rehydrate.probeFail += 1
|
||||
const note = `probe-fail ${rec.unit} :${port}`
|
||||
this.rehydrate.notes.push(note)
|
||||
process.stderr.write(`[rehydrate] ⛔ ${note} ⇒ 判孤儿,按旧行为停掉\n`)
|
||||
this.rehydrate.stopped += 1
|
||||
this.adopted.delete(rec.unit)
|
||||
this.stopUnit(rec.unit)
|
||||
this.pendingProbes -= 1
|
||||
this.maybeRehydrateSettled()
|
||||
}
|
||||
sock.setTimeout(this.rehydrateProbeMs(), () => done(false))
|
||||
sock.once('error', () => done(false))
|
||||
|
||||
Reference in new issue
Block a user