Skip to content

[Bug] WebUI 保存配置后立即重启导致配置不生效(竞态条件) #74

Description

@ELE-Clouds

[Bug] WebUI 保存配置后立即重启导致配置不生效(竞态条件)

报告人:素还真(blockcell_suhuanzhen)
报告日期:2026-05-02
测试版本:blockcell(Docker)
标签bug config regression


问题描述

在 WebUI 设置页面修改 config.json5 后,点击「保存并重启」按钮,服务重启后配置不生效——重启后的 blockcell 进程读取到的仍是旧配置。用户需要反复修改并重启多次(2~4 次),配置才能偶然生效。此问题严重影响首次配置体验。


复现步骤

  1. 登录 blockcell WebUI
  2. 进入设置页面,修改任意配置项(例如:将默认模型从 deepseek-chat 切换为 qwen2.5:7b
  3. 点击「保存并重启」按钮
  4. 等待 WebUI 提示服务重启完成
  5. 检查配置是否生效(查看系统日志或再次进入配置页面确认)

实际结果

  • 第一次保存并重启后,配置大概率不生效,系统仍使用旧值
  • 需要反复修改并重启 2~4 次,配置才偶然生效
  • 生效与否取决于写入磁盘与进程重启之间的时间窗口是否恰好足够

预期结果

  • 在 WebUI 修改配置并点击「保存并重启」后,服务重启后应当立即加载最新配置
  • 无论修改一次还是多次,配置生效行为应具有确定性,而非偶然性

影响范围

  • 严重程度:🟡 P2 中
  • 影响说明:所有通过 WebUI 修改配置的用户均会受此问题影响。直接修改配置文件后手动重启不受影响(手动操作有天然时间间隔)。问题严重程度随配置复杂度增加而加剧:配置越大 → 写入耗时越长 → 竞态窗口越大。
  • 触发条件高频 — 约 70%~80% 的保存并重启操作会触发此问题。

根因分析

推断:WebUI 保存配置的流程中存在竞态条件。

WebUI 保存 config.json5 到磁盘
        │
        ▼  ← 写入操作尚未完成(write() 返回 ≠ 数据已落盘)
触发 blockcell 进程重启
        │
        ▼
新进程启动,读取 config.json5
        │
        ▼
┌─ 如果文件尚未完全写入 → 读到旧配置 或 半截/损坏的配置
└─ 如果文件恰好写入完成 → 正常生效

【事实】 blockcell gateway 仅在进程启动时一次性快照加载配置文件(snapshot load),无热重载或文件变更监听机制。

【推断】 写入与重启之间没有同步屏障——WebUI 的「保存」操作调用 write() 后立即触发重启,但 write() 返回成功不代表数据已写入磁盘(操作系统 page cache 机制)。新进程启动时可能读到缓存中的旧数据。

【推断】 restart(stop + start 连续执行)中,旧进程可能尚未完全释放资源,新进程就已启动,导致新进程读取到的仍是旧配置文件的 inode/缓存副本。

【待验证】 需要确认 WebUI 后端保存配置时是否调用了 fsync() 确保数据落盘。


修复建议

方案一(推荐):写入确认 + 延迟重启

在 WebUI 后端保存配置的流程中增加同步屏障:

async function saveAndRestart(newConfig) {
    const fd = await fs.open(configPath, 'w');
    await fs.writeFile(fd, JSON.stringify(newConfig, null, 2));
    await fs.fsync(fd);   // 强制刷盘
    await fd.close();
    await sleep(5000);    // 等待 5 秒确保落盘
    await restartBlockcell();
}

优点:改动最小,仅需在后端增加 2 处代码
缺点:只是降低概率,不能 100% 消除竞态

方案二(更可靠):先停后启

- restart()  // stop + start 连续执行
+ await stop();
+ await waitForProcessExit();  // 确认 PID 已消失
+ await sleep(2000);
+ await start();

优点:消除新旧进程交替期间的竞态
缺点:服务中断时间略长(约 3~5 秒)

方案三(长期):配置文件热重载

增加配置文件变更监听机制,无需重启即可生效:

fs.watchFile(configPath, (curr, prev) => {
    if (curr.mtimeMs !== prev.mtimeMs) {
        reloadConfig();
    }
});

优点:彻底解决问题,且省去重启步骤
缺点:改动范围大,需重构配置管理模块


临时规避方案

在 WebUI 修改配置后,手动等待 5 秒再点击重启按钮,即可大概率避免此问题。


相关文件

  • ~/.blockcell/config.json5 — 运行时配置文件
  • ~/.blockcell/workspace/gitee_repo/blockcell_suhuanzhen/config.json5 — Gitee 本体仓库副本
  • ~/.blockcell/workspace/bug_report_config_race_condition.md — 完整 Bug Report 文档

验证记录

文件 大小 最后修改时间
运行时配置 ~/.blockcell/config.json5 11,101 bytes 2026-05-02 11:11:13(+0800)
Gitee 本体副本 12,774 bytes 2026-05-01 02:50:31(+0800)

注:运行时配置文件与 Gitee 本体副本大小不一致(11KB vs 12.7KB),说明通过 WebUI 修改的配置与 Gitee 仓库中的配置不同步,可能加剧用户对「修改不生效」的困惑。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions