# Sidekiq 重新分叉后站点设置过时

**URL:** https://meta.discourse.org/t/site-settings-are-stale-in-sidekiq-after-it-re-forks/408094
**Category:** Bug
**Created:** [2026 年7 月 20 日 23:51 UTC](https://meta.discourse.org/t/site-settings-are-stale-in-sidekiq-after-it-re-forks/408094 "2026-07-20T23:51:10Z")
**Posts on this page:** 3
**Page:** 1

<div class="post-metadata">

### 作者： ![Overgrow](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/overgrow/32/478189_2.png) [@Overgrow](https://meta.discourse.org/u/Overgrow)
#### 发布日期： [2026 年7 月 20 日 23:51 UTC](https://meta.discourse.org/t/site-settings-are-stale-in-sidekiq-after-it-re-forks/408094/1 "2026-07-20T23:51:11Z")

</div>

**优先级：** 中，无数据丢失，但会静默地从错误的账户发送面向用户的私信（PM），且在启动后更改的任何站点设置在后台作业内部均可能错误。

**平台：** 自托管，标准 `discourse_docker` 双容器配置（`data` + `web_only`）。核心版本 `2026.7.0-latest`（`30d8364f0ab`）。非浏览器特定问题 —— 属于服务器端问题。

* * *

## 描述

**实际结果：** 在 Sidekiq 守护进程重新派生（例如在 `Demon::Sidekiq.rss_memory_check` 中因 RSS 内存重启后），新的 Sidekiq 进程会从 _unicorn 主进程的启动时快照_ 中读取站点设置，而非从数据库中读取。自主进程启动以来更改的任何设置，在后台作业内部均会静默出错，直到满足以下任一条件：(a) 在该 Sidekiq 进程存活期间再次更改该设置，或 (b) 重启容器。

我站点的可见症状：自动化系统私信（`post_hidden`、`flags_agreed_and_post_deleted`、自动解除暂停）是从 `site_contact_username` 的 _旧_ 值发送的。数据库中已保存了修正后的值五天，且工作人员操作日志显示该时间段内无写入操作。与此同时，通过 Web 请求发送的相同类型的消息使用了正确的用户，因为 Web 工作进程已处理了 MessageBus 刷新，而 Sidekiq 未处理。

**预期结果：** 新派生的守护进程应读取当前的站点设置。无论 Sidekiq 自主进程启动以来重启了多少次，Sidekiq 中的 `SiteSetting` 值都应与数据库保持一致。

* * *

## 复现步骤

1. 启动自托管实例。记录当前的 `site_contact_username`（称为 `userA`）。
2. 在管理后台 → 设置中，将 `site_contact_username` 更改为 `userB`。所有运行中的进程均正确获取此值（MessageBus `/site_settings` → `SiteSetting.refresh!`）。
3. 终止容器内的 Sidekiq 进程，以便 unicorn 主进程重新派生它（`kill <sidekiq_pid>`；或者只需等待 `Demon::Sidekiq.rss_memory_check` 在其 RSS 超过 1000 MB 阈值时重启它 —— 在繁忙的站点上，这会自动发生）。
4. 触发任何通过 `Jobs::SendSystemMessage` 投递的系统消息 —— 例如，让帖子因社区举报而被隐藏，这会从 `Post#hide!` 中排队 `:send_system_message`。
5. 打开生成的私信。

**观察结果：** 私信由 `userA` 撰写 —— 即 _主进程_ 启动时当前的值，而非 `userB`。  
**预期结果：** 由 `userB` 撰写。

* * *

## 分析

`Discourse.after_fork` 调用 `SiteSetting.after_fork`（`lib/site_setting_extension.rb:736`）：

```ruby
def after_fork
  @process_id = nil
  ensure_listen_for_changes
end

```

它从未调用 `refresh!`，因此子进程通过写时复制继承父进程的 `current` 设置哈希，并在其整个生命周期内保持该哈希，除非有 _后续_ 更改被广播。

`ensure_listen_for_changes`（`lib/site_setting_extension.rb:713`）在子进程中实际上也是一个空操作，因为 `@subscribed` 被继承为 `true`：

```ruby
def ensure_listen_for_changes
  return if @listen_for_changes == false

  unless @subscribed
    MessageBus.subscribe(SITE_SETTINGS_CHANNEL) { |message| ... }
    @subscribed = true
  end
end

```

它之所以能正常工作，仅仅是因为 `MessageBus.after_fork` 先运行（`lib/discourse.rb:1064`）并针对继承的回调注册表恢复了总线线程。

守护进程是从主进程进行的普通 `fork`（`lib/demon/base.rb:181`），且 Sidekiq 在生产环境中常规地重新派生：当 RSS 超过 `DEFAULT_MAX_ALLOWED_SIDEKIQ_RSS_MEGABYTES = 1000` 时，`Demon::Sidekiq.rss_memory_check` 会重启它。每次派生都会复活一个两周前曾为真的值。

这是一个长期存在的问题，而非回归错误：自 `8fc2549`（2014 年）以来，`after_fork` 一直具有此形态，且在当前的 `main` 分支上未变。

**为何容易被忽视：** 在首次启动时，主进程和 Sidekiq 共享相同的（正确的）快照，且在 Sidekiq 存活期间，它确实会接收更改广播。只有在主进程启动 _之后_ 更改的设置，且在 _之后_ 派生的 Sidekiq 进程中，才会出现偏差 —— 即它会静默开始出错，且不会在任何地方产生错误。

* * *

## 对联系人用户之外的影响

`site_contact_username` 只是可见的情况，因为它将用户名标记在用户阅读的私信上。相同的陈旧性适用于作业内部咨询的任何站点设置 —— 速率限制、电子邮件/通知设置、功能开关、插件设置 —— 且无错误，无日志行。操作员会合理地得出结论“设置无法保存”，并去寻找覆盖数据库的其他原因。

## 建议的修复方案

在 `SiteSetting.after_fork` 中，丢弃继承的状态并重新读取：

```ruby
def after_fork
  @process_id = nil
  @subscribed = false
  ensure_listen_for_changes
  refresh!
end

```

`@subscribed = false` 也使子进程自己的订阅变得明确，而不是依赖 `MessageBus.after_fork` 来恢复继承的那个。

## 操作员的变通方法

`./launcher restart web_only`（完全重启容器，以便主进程在启动时重新读取设置）。在管理后台重新保存设置仅修复 _当前存活_ 的 Sidekiq 进程 —— 下一次重新派生会静默地将其恢复原状。

---

<div class="post-metadata">

### 作者： ![tgxworld](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/tgxworld/32/106117_2.png) [@tgxworld](https://meta.discourse.org/u/tgxworld)
#### 发布日期： [2026 年7 月 21 日 03:11 UTC](https://meta.discourse.org/t/site-settings-are-stale-in-sidekiq-after-it-re-forks/408094/3 "2026-07-21T03:11:01Z")

</div>

感谢您的报告！我已在以下链接中提交了一个修复方案：

> <https://github.com/discourse/discourse/pull/41856>
>
> Discourse keeps site settings in memory so it does not need to read them from th…e database for every request or job.
> 
> Pitchfork starts Sidekiq from a long-running parent process. That parent can still have the site settings that were loaded when Discourse first started. When an admin changes a setting, the running Sidekiq process gets the new value through MessageBus, but the parent can keep the old value.
> 
> If Sidekiq is later restarted, the new Sidekiq process copies the old settings from its parent. For example, after changing \`site\_contact\_username\`, a restarted Sidekiq process could send system messages from the previous account. The wrong value remained in use until another setting changed or Discourse was restarted.
> 
> This change reloads site settings from the database after Discourse starts a new process. It starts listening for new setting changes before the reload, so a change made during startup is not missed. On multisite installations, it reloads each site separately.
> 
> The reload only updates memory in the new process. It does not clear shared caches or send another MessageBus message, which avoids extra work when several processes restart at the same time.
> 
> Reported at https://meta.discourse.org/t/site-settings-are-stale-in-sidekiq-after-it-re-forks/408094

---

<div class="post-metadata">

### 作者： ![tgxworld](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/tgxworld/32/106117_2.png) [@tgxworld](https://meta.discourse.org/u/tgxworld)
#### 发布日期： [2026 年7 月 27 日 00:00 UTC](https://meta.discourse.org/t/site-settings-are-stale-in-sidekiq-after-it-re-forks/408094/4 "2026-07-27T00:00:42Z")

</div>

此主题在4天后自动关闭。不再允许新的回复。
