JJianFeeeeefix(cluster): 停用状态以「环」为权威,消除长期离线导致的永久分歧
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix(cluster): 停用状态以「环」为权威,消除长期离线导致的永久分歧 承接用户指出的遗留:links 表是每节点本地副本,靠「store→环上行 + 环→store 下行」双向 sync 收敛,但两边都可能被覆盖。其中「本地 store 权威」这条规则有 硬伤,本次改掉。 ## 先复现,再动手 写探针验证「入环 token 会不会冲掉本地未传播的决定」,结果比预期严重: after adopting a stale token: known=true disabled=false LOCAL DISABLE WAS WIPED by an incoming token OnToken/AdoptState 是 e.state = tk.State **整体替换**。两次 token 之间做出的 停用决定,只要下一轮到达的 token 是「决定之前」捕获的,就会被整个洗掉 —— 决定永远传不出去,转发照旧运行。Group 之所以看起来没这个问题,是因为它每次 adoption 都被 SetTopologySync 从 store 重新推上去,而 disabled 没有对应的 「尚未传播」保护。 ## 改法:环权威 + 本地决定带「未确认」标记 1. **环权威**:adoption 时把环上的 disabled 写穿本地 store(write-through, 不是监听器),任何 peer 的决定都在一个 token 周期内落地。这终结了旧规则 「各人信自己那份」造成的永久分歧。顺序上只有单向要求:引擎先重新断言本地 未确认决定,再跑 host sync,所以读环绝不会覆盖用户刚做的操作。 2. **未确认决定受保护**(RingEngine.localDisabled):UpdateTopologyDisabled 记下决定,adoption 后由 reconcileLocalDisabled 重新断言到刚采纳的 state 上, 于是它会随下一轮 token 传出去。环报回同值时删除该键(全cluster已一致); 转发从 topology 消失时一并清扫,map 不会无限增长。**false 同样受保护** —— 重新启用也需要传播,丢掉它会把转发永久留在停用态。 3. TopologyDisabled() 对未确认决定短路返回本地值,避免状态页与用户刚做的 操作相反。 ## 测试(又抓到一个假绿) 新增 4 条:停用/启用跨 adoption 存活、确认后停止断言(让位给 peer 的后续决定)、 追踪表不累积。 ★ TestLocalDisableSurvivesAdoption **第一版是假绿**:它断言 TopologyDisabled(),而该访问器会短路到本地决定,于是「即便即将转发的 state 仍是 enabled」它也报 true。改成断言**下游节点会看到什么**(用一个 peer 引擎 AdoptState 本引擎的 state)后,去掉重新断言如期变红: the forwarded token carries disabled=false, want true 双向验证通过,这个教训要记:测「声明」而不是测「实际传播的状态」,等于没测。 go build / go vet / go test ./... 全绿,gofmt 干净。 | 1 天前 | |
feat(cluster): 停用改为「标记」语义,让 disabled 真正随令牌环跨节点传播 承接用户提问「设计上停用不是本来就会跨节点传输吗」——核实结论:结构上确实 如此(TopoEntry.Link 是完整 store.Link,整个 State 随 token 每轮广播),但 实际路径断了。断点正是「撤销会删掉 topology 条目」:条目是 flag 的载体, 删了就无处传播,于是停用只能靠一次性 revoke 任务投递给 owner,**owner 当时 不在线就收不到**(实测 .60 记 disabled=1 / .106 记 0,就是这么来的)。 ## 改为标记而非移除 撤销不再 RemoveTopology,而是 UpdateTopologyDisabled(true),条目保留、 Link.Disabled=true、Active=false。Active 正是为此存在:OfflineReassign() 只处理 Active 条目,所以停用的转发在 owner 掉线时不会被重新排队。 - 新增 UpdateTopologyDisabled / TopologyDisabled(照 UpdateTopologyGroup 的桥) - 新增 store.ReconcileLinkDisabled 作接收端:adoption 时把环上的 flag 落进 本地 store;本节点没有该转发时补一条 disabled 占位行(否则日后在本节点被 claim 会复活),enable 则不建行 - SetTopologySync 由单向(store→环)扩为双向:群组仍上行,disabled 下行 - AddTopology 的 Active 跟随 Link.Disabled(原本硬编码 true,认领一个停用 转发就会复活它) - 审计日志细分 forward.stop / forward.start,与 forward.remove 区分 ## 语义变更带出的两个新问题(都已修) 1. **「启动」这条路断了**。条目保留 ⇒ SubmitTask 被去重挡下,而认领路径的 duplicate-claim 防御又会丢弃「已有 owner」的任务 ⇒ 重启任务发不出去,owner 永远收不到,转发**能停不能起**。 修:新增 Task.Restart 这一独立任务类型 + SubmitRestart + Handler.RestartFn, 显式绕过 duplicate-claim 防御并原地复活(不重复建条目、不重跑 claim 簿记)。 SubmitTask 的守卫同时从 HasTask 收窄为新的 HasActiveTask(跳过 disabled 条目 与撤销任务);saveCanvas 的判断相应改用 HasActiveTask,避免每次保存都对 已标记的转发重复发撤销。 2. 原本两处 RemoveTopologyEntry 调用(ClaimFn/RevokeFn 的 disabled 分支)在 新语义下会把本该保留的条目删掉,改为 UpdateTopologyDisabled。 ## 测试(每个都做了「回退修复行→必须变红→还原变绿」双向验证) - TestStoppedTopologyEntrySurvivesAdoption —— 离线成员也能学到停用, 一次性 revoke 任务永远做不到这一点 - TestStoppedForwardNotRequeuedOnNodeDeparture / TestAddTopologyRespectsDisabledFlag —— 标记而非删除为何安全 - TestSubmitTaskNotBlockedByStoppedEntry / TestSubmitTaskStillDedupesActiveForward - TestRestartTaskBypassesDuplicateClaimGuard / TestRestartFlagSurvivesTokenSerialization - TestStopThenStartPublishesRestartTask(HTTP 端到端,断言**任务真的发出**) - TestReconcileLinkDisabled*(store 侧三条) ★ 两次踩到**假绿**:第一版只断言 store 层(newTestHandler 的 Ring 为 nil, 坏掉的路根本没执行);第二版在 re-enable **之后**才调 SubmitTask,此时新旧 谓词结果相同,测不出差异。都是靠「回退修复行看是否变红」抓出来的 —— 这个 双向验证已经是本项目的固定动作。 go build / go vet / go test ./... 全绿,gofmt 干净。 | 1 天前 | |
webui4frpc: 独立可用的可视化 frpc 控制器 (M0) - 零 frp 源码依赖,单二进制 (Go + Vue3 + VueFlow + Element Plus) - 画布多对多连线,渲染 tcp/udp/http/https frpc 配置 - worker 进程管理:自愈、日志轮转、崩溃退避重启 - frpc 一键安装 (GitHub Releases) + 手动指定路径 - 三页 UI:状态(默认)/连接配置/设置 - 状态页实时节点/转发状态,节点可增删改启停 - 画布冲突检查:端口/域名冲突标红 + 弹窗拦截保存 - backend 单测覆盖 store/render/process/httpapi/install - plan.md + FRPC_FEATURES_AUDIT.md 文档 | 1 个月前 | |
feat: per-forward worker 模型 — 每条转发独立 frpc 配置+独立进程 核心改动(ring 协议不变,Task 本来就是 {Local,Remote,Link} 三元组): - process.WorkerKey: worker 键改为 'local~remote~port' 三元组 - renderForward: 每条 forward 渲染只含 1 个 proxy 的独立 frpc 配置 (替代 renderRemote 把该 remote 全部 forwards 塞进一个进程) - ClaimFn/RevokeFn: 认领/撤销只操作这一条 forward 自己的进程 - SyncWorkers/auto-start: 只拉起 localOnly 转发的独立进程, 集群转发由 ring 认领节点拉起 (消除三台争抢 proxy already exists) - RemoteStatus/StartRemote/StopRemote/RestartRemote: remote 级聚合辅助, 保持 /profiles API 形状不变, 前端零改动 - handleStatus/logs: 按 worker key 解析真实 remote 名 效果: 三台节点的 frpc 各自只注册自己拥有的 proxy, 单条转发故障不再波及兄弟 | 1 个月前 | |
feat: M3 load balancing + health check (model/render/admin API status/UI) | 1 个月前 | |
feat(cluster): 停用改为「标记」语义,让 disabled 真正随令牌环跨节点传播 承接用户提问「设计上停用不是本来就会跨节点传输吗」——核实结论:结构上确实 如此(TopoEntry.Link 是完整 store.Link,整个 State 随 token 每轮广播),但 实际路径断了。断点正是「撤销会删掉 topology 条目」:条目是 flag 的载体, 删了就无处传播,于是停用只能靠一次性 revoke 任务投递给 owner,**owner 当时 不在线就收不到**(实测 .60 记 disabled=1 / .106 记 0,就是这么来的)。 ## 改为标记而非移除 撤销不再 RemoveTopology,而是 UpdateTopologyDisabled(true),条目保留、 Link.Disabled=true、Active=false。Active 正是为此存在:OfflineReassign() 只处理 Active 条目,所以停用的转发在 owner 掉线时不会被重新排队。 - 新增 UpdateTopologyDisabled / TopologyDisabled(照 UpdateTopologyGroup 的桥) - 新增 store.ReconcileLinkDisabled 作接收端:adoption 时把环上的 flag 落进 本地 store;本节点没有该转发时补一条 disabled 占位行(否则日后在本节点被 claim 会复活),enable 则不建行 - SetTopologySync 由单向(store→环)扩为双向:群组仍上行,disabled 下行 - AddTopology 的 Active 跟随 Link.Disabled(原本硬编码 true,认领一个停用 转发就会复活它) - 审计日志细分 forward.stop / forward.start,与 forward.remove 区分 ## 语义变更带出的两个新问题(都已修) 1. **「启动」这条路断了**。条目保留 ⇒ SubmitTask 被去重挡下,而认领路径的 duplicate-claim 防御又会丢弃「已有 owner」的任务 ⇒ 重启任务发不出去,owner 永远收不到,转发**能停不能起**。 修:新增 Task.Restart 这一独立任务类型 + SubmitRestart + Handler.RestartFn, 显式绕过 duplicate-claim 防御并原地复活(不重复建条目、不重跑 claim 簿记)。 SubmitTask 的守卫同时从 HasTask 收窄为新的 HasActiveTask(跳过 disabled 条目 与撤销任务);saveCanvas 的判断相应改用 HasActiveTask,避免每次保存都对 已标记的转发重复发撤销。 2. 原本两处 RemoveTopologyEntry 调用(ClaimFn/RevokeFn 的 disabled 分支)在 新语义下会把本该保留的条目删掉,改为 UpdateTopologyDisabled。 ## 测试(每个都做了「回退修复行→必须变红→还原变绿」双向验证) - TestStoppedTopologyEntrySurvivesAdoption —— 离线成员也能学到停用, 一次性 revoke 任务永远做不到这一点 - TestStoppedForwardNotRequeuedOnNodeDeparture / TestAddTopologyRespectsDisabledFlag —— 标记而非删除为何安全 - TestSubmitTaskNotBlockedByStoppedEntry / TestSubmitTaskStillDedupesActiveForward - TestRestartTaskBypassesDuplicateClaimGuard / TestRestartFlagSurvivesTokenSerialization - TestStopThenStartPublishesRestartTask(HTTP 端到端,断言**任务真的发出**) - TestReconcileLinkDisabled*(store 侧三条) ★ 两次踩到**假绿**:第一版只断言 store 层(newTestHandler 的 Ring 为 nil, 坏掉的路根本没执行);第二版在 re-enable **之后**才调 SubmitTask,此时新旧 谓词结果相同,测不出差异。都是靠「回退修复行看是否变红」抓出来的 —— 这个 双向验证已经是本项目的固定动作。 go build / go vet / go test ./... 全绿,gofmt 干净。 | 1 天前 | |
fix(web): 窄屏底部导航栏错位到顶部 — 顶栏 backdrop-filter 创建了包含块 .sb-nav 用 position:fixed;bottom:0 想贴视口底部,但父元素 .sidebar 带 backdrop-filter。backdrop-filter 与 transform/filter 同理,会为后代的 position:fixed 创建【包含块】—— 于是 bottom:0 变成相对顶栏下沿定位而不是 视口,tab bar 贴在顶栏正下方,看起来就是固定在屏幕顶部。 窄屏下关掉顶栏的 backdrop-filter,改用近实色底 color-mix(var(--w4f-card-solid) 92%, transparent);玻璃模糊保留在 .sb-nav 自身(元素自己的 backdrop-filter 不影响自己的定位)。 顺带 bump 到 0.1.2(version 包 / release.sh / build_installers.sh / rpm spec + changelog / README 安装示例文件名)。 验证:tsc --noEmit 通过、npm run build 通过;产物 CSS 中确认 720px 段的 .sidebar 已含 backdrop-filter:none;三节点部署后 /status 报告 0.1.2, 环正常(cycle 推进、pending=0、5 条转发 frpc 进程与 topology 归属一致)。 | 24 天前 |