mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-15 12:14:37 +00:00
feat(cluster,webapi): 集群页可切换负载均衡模式(每节点一个开关)
## 为什么之前没有开关(以及为什么现在这么加) 模式原本只读一次环境变量 `W4F_BALANCE_MODE`,UI 只显示「观察模式 / 已启用」。 把开关加进来时有两个坑必须一起解决,否则开关是个摆设: 1. **不持久化就等于骗人**:只用 env,页面上翻一下、重启一次就回到默认, 而页面已经显示成功了。 2. **不能做成集群级开关**:每个成员只释放自己的转发,根本没有一个动作能把它们 一起翻过来。做成集群开关就是撒谎。 所以:**每节点一个开关**,写的是本节点自己的设置。 ## 持久化与优先级 - 新增 `settings` 表的 `balance_mode` 列 + `Store.BalanceMode()`。**单独一列**, 不是塞进 settings JSON:那个 JSON 是整份重写的,而这是会被各节点同时写、必须 各自收敛的状态,跟 `nodeKey`/`settings` 同一类处理。 - **存了就以存的为准**,env 只作首次启动的默认值;`Settings()` 读回时带上它, 但 `UpdateSettings` 只在非空时镜像到列,所以一次无关的设置保存不会把它清掉。 - `SetModeFromUI` **先落盘再应用**:写失败就什么都不改,否则节点会运行一个重启后 复现不出来的模式——开关就成了谎言。 - 认不出的模式一律拒绝(400),不猜。 ## 报告新增两个字段 - `modeSource`: `env` | `stored` —— 说清当前模式是启动参数给的还是有人在这里选的。 - `canControl`: 由**调用者的权限**决定,只读身份不会看到一个点了会 403 的开关。 `PUT /cluster/balance/mode` 走 **write** 级鉴权:释放会停 worker、断活连接。 ## 两处我自己抓到并修掉的错 1. **`ModeSource()` 一开始写成「能存就算 stored」**(判据是 `persist != nil`), 于是**每个有 store 的节点都自称 stored**,哪怕从没人设过。canary 第一轮就露了: `mode=dry-run source=stored`。改成看「是否真的存过值」。 2. **启动日志打的是 env 变量而不是生效值**,存过 `on` 之后仍打 `DRY-RUN`—— 上一行的 `loaded from settings` 还在,两行自相矛盾,正好会让人以为释放是关的 而它其实开着。 ## 测试 新增 6 条:存的模式压过 env、没存就回落 env、落盘失败则模式不变、乱值被拒、 UI 切换同时落盘并生效、无持久化能力时不宣称可控制。 canary 实跑 6 步:初始 `dry-run/env` → 设 `on` → 复查 `on/stored` → 乱值 400 → 乱值后模式不变 → **重启后仍 `on/stored`**,启动日志 `load balancing ON (source=stored …)`。 vue-tsc 干净;go build / vet / go test ./... 全绿。
This commit is contained in:
@ -128,6 +128,11 @@ type Settings struct {
|
||||
// key). Cleared on explicit detach (detachAsStandalone) so a node that
|
||||
// intentionally left does NOT auto-rejoin.
|
||||
ClusterPeers string `json:"clusterPeers,omitempty"`
|
||||
// BalanceMode is the load-balancing mode chosen in the UI: "", "off",
|
||||
// "dry-run" or "on". Empty means "never configured here" so the environment
|
||||
// variable stays the bootstrap default; a non-empty value wins over it,
|
||||
// otherwise flipping the switch in the UI would silently revert on restart.
|
||||
BalanceMode string `json:"balanceMode,omitempty"`
|
||||
}
|
||||
|
||||
// Forward is a rendered link row attached to a remote.
|
||||
@ -748,6 +753,13 @@ func (s *Store) Settings() (Settings, error) {
|
||||
if err := json.Unmarshal([]byte(raw), &st); err != nil {
|
||||
return Settings{AutoStartProfiles: true, RestartOnExit: true, RestartIntervalSeconds: 5}, nil
|
||||
}
|
||||
// BalanceMode lives in its own column (see UpdateSettings). Surface it here
|
||||
// too so callers reading Settings get the effective value, but never let it
|
||||
// overwrite the column on a write-back: UpdateSettings only mirrors a
|
||||
// non-empty value, so an unrelated settings PUT cannot clear a UI choice.
|
||||
if mode, ok, err := s.BalanceMode(); err == nil && ok {
|
||||
st.BalanceMode = mode
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
@ -760,7 +772,36 @@ func (s *Store) UpdateSettings(st Settings) error {
|
||||
"INSERT INTO settings(key, value) VALUES('settings', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||||
string(raw),
|
||||
)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mode := st.BalanceMode; mode != "" {
|
||||
// Mirror into its own column for the same reason the other flagged
|
||||
// fields have one: the settings JSON is rewritten wholesale, so a
|
||||
// clustered field that rides that JSON would flip back and forth between
|
||||
// nodes instead of converging.
|
||||
if _, err := s.db.Exec(
|
||||
"INSERT INTO settings(key, value) VALUES('balance_mode', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||||
mode,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BalanceMode returns the stored load-balancing mode, and whether one was ever
|
||||
// stored (false = never configured here; the environment default applies).
|
||||
func (s *Store) BalanceMode() (string, bool, error) {
|
||||
var v string
|
||||
err := s.db.QueryRow("SELECT value FROM settings WHERE key = 'balance_mode'").Scan(&v)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return v, v != "", nil
|
||||
}
|
||||
|
||||
// SetNodeKey persists just the cluster admission key, preserving all other
|
||||
|
||||
Reference in New Issue
Block a user