# Chat Pitchfork 超时：回复静默创建线程，自动追踪随时间膨胀

**URL:** https://meta.discourse.org/t/chat-pitchfork-timeouts-replies-silently-create-threads-and-auto-tracking-bloats-over-time/401067
**Category:** Bug
**Tags:** chat
**Created:** [2026年四月20日 18:47 UTC](https://meta.discourse.org/t/chat-pitchfork-timeouts-replies-silently-create-threads-and-auto-tracking-bloats-over-time/401067 "2026-04-20T18:47:10Z")
**Posts on this page:** 1
**Showing post:** 1

<div class="post-metadata">

### Author: ![piffy](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/piffy/32/254198_2.png) [@piffy](https://meta.discourse.org/u/piffy)
#### Post date: [2026年四月20日 18:47 UTC](https://meta.discourse.org/t/chat-pitchfork-timeouts-replies-silently-create-threads-and-auto-tracking-bloats-over-time/401067/1 "2026-04-20T18:47:10Z")

</div>

我们的网站保存了完整的聊天记录，这些记录已经积累了很长时间。最近，我遇到了一个问题：用户抱怨聊天按钮加载时间过长，甚至有时根本不显示！而且，不同用户之间的表现似乎也不一致。

在 Claude Code 的帮助下，我认为我已经找到了问题所在，这个问题可能会影响所有站点。以下是关于该问题的报告汇总。

**简而言之：聊天中的回复会生成追踪事件，这些事件会随着时间推移不断膨胀（即使已关闭线程功能）。**

以下是经过我轻微编辑的 AI 生成报告：

* * *

## 问题描述

活跃用户报告称，聊天功能会一直加载转圈，永远无法完成。在管理员日志中，我们看到了 Pitchfork 工作进程超时并抛出回溯堆栈，例如：

```plaintext
Pitchfork worker is about to timeout, dumping backtrace for main thread
...
lib/mini_sql/postgres/connection.rb:... MiniSql::Postgres::Connection#query
plugins/chat/app/queries/chat/thread_unreads_query.rb:132 Chat::ThreadUnreadsQuery.call
plugins/chat/app/queries/chat/tracking_state_report_query.rb:71 Chat::TrackingStateReportQuery.call
plugins/chat/app/services/chat/list_user_channels...

```

Web 工作进程被终止，因为单个 SQL 查询（`ThreadUnreadsQuery`）未在工作进程超时时间内返回。同一条回溯堆栈在一天内重复出现了数百次。

在客户端，该错误表现为 `/chat/api/me/channels` 返回 500 错误：

```plaintext
/chat/api/me/channels Failed to load resource: the server responded with a status of 500

```

这就是浏览器端所呈现的同一个 Pitchfork 超时问题：触发 `ThreadUnreadsQuery` 的端点永远无法完成，工作进程被回收，UI 无法获取渲染聊天所需的频道列表。

* * *

## 根本原因（通俗解释）

每当用户在特定聊天消息上点击“回复”时，Discourse 都会创建（或重用）一个 **线程** 来保存该回复——即使该频道已禁用线程功能。在群组私信中，系统还会自动将所有私信参与者添加到新线程的成员列表中。随着时间的推移，每个活跃用户都会在 `user_chat_thread_memberships` 表中积累数千条成员记录，而他们从未有意选择追踪任何内容。

当这些用户中的任何一人打开聊天时，核心追踪查询会遍历他们追踪的 **所有** 线程，并对每个线程运行三个相关子查询。拥有数千条成员记录的用户会导致查询超出 Postgres + Pitchfork 的等待时间，导致工作进程在聊天加载完成前被终止。

目前没有任何清理机制：成员记录从未被修剪，当前插件代码中也从未为自动创建的记录设置 `notification_level = muted/normal`。

* * *

## 数据

调查时我们生产数据库的数据如下：

- 闲置 180 天以上的过时追踪级别成员记录（`notification_level = 2`）： **29,309** 条
- 闲置 15 天以上的同类记录： **41,139** 条
- 拥有超过 1,500 条线程成员记录的用户： **11** 人
- 单个用户最高记录： **3,738** 条成员记录 —— 超过了 `ThreadUnreadsQuery` 中的 `MAX_THREADS = 3000` 上限
- 在这 11 位重度用户中， **100%** 的成员记录均为 `notification_level = 2`（自动追踪），没有任何一条是用户主动选择的“关注”（级别 3）

重度用户的追踪成员记录分布

 ![image](https://global.discourse-cdn.com/meta/original/4X/0/8/8/088a11b4697603e7a8daba42796587341f4ccd6e.png)

* * *

## 受影响的频道类型

两条不同的路径导致了相同的结果：

1. **群组私信（`chatable_type = DirectMessage`, `threading_enabled = false`）**  
每次在群组私信中回复时，系统都会创建一个线程，并自动将所有私信参与者添加进去。这在 `create_message.rb` 中有明确体现（见下方代码）。

2. **分类/公共频道，且 `threading_enabled = false`**  
我们发现某个分类频道中存在数千条线程，该频道的 `threading_enabled` 标志已设置为 `false` 并持续超过一年——但今天仍然在创建新线程（查询运行后几分钟内就出现了新的线程 ID）。`create_message.rb` 中的 `fetch_thread` 路径在回复时静默构建线程，而并未检查频道的线程设置。

因此，“关闭线程功能”并不奏效：该设置仅隐藏了专用线程界面，但当用户使用“回复消息”功能时，后台仍会创建线程。

* * *

## 相关代码

**静默创建线程的回复路径** — `plugins/chat/app/services/chat/create_message.rb:131`

```ruby
def fetch_thread(params:, reply:, channel:, options:)
  return Chat::Thread.find_by(id: params.thread_id) if params.thread_id.present?

  reply.thread ||
    reply.build_thread(
      ...
      force: options.force_thread,
      ...
    )
end

```

代码中未检查 `channel.threading_enabled?`。在非私信的分类频道中，无论设置如何都会构建线程。

**私信中自动将所有参与者加入线程** — 同一文件，约第 203 行

```ruby
if channel.direct_message_channel? && !channel.threading_enabled
  # 将所有私信参与者添加到线程，以便实现未读追踪和标记已读功能
  channel.chatable.users.each { |user| thread.add(user) }
  thread.membership_for(guardian.user).update!(last_read_message: message_instance)

```

每次在群组私信中回复，成员记录数量就会增加该私信的参与者人数，而这些记录从未被清理。

**性能瓶颈查询** — `plugins/chat/app/queries/chat/thread_unreads_query.rb:14-54`

```ruby
class ThreadUnreadsQuery
  MAX_THREADS = 3000
  ...
  # 每行 user_chat_thread_memberships 对应三个相关子查询：
  # SELECT (SELECT COUNT(*) ... unread) AS unread_count,
  # (SELECT COUNT(*) ... mentions) AS mention_count,
  # (SELECT COUNT(*) ... watched) AS watched_threads_unread_count,
  # chat_threads.channel_id, memberships.thread_id
  # FROM user_chat_thread_memberships AS memberships
  # ...
  # WHERE memberships.user_id = :user_id
  # ...
  # LIMIT :limit

```

对于 N 条成员记录，该查询会执行 3 × N 次相关子查询，每次子查询需连接 5 个以上的表。一旦用户的成员记录超过几千条，查询就会可靠地超出 Pitchfork 超时限制。`MAX_THREADS = 3000` 的设置本身已承认了扩展性问题，但用户确实可能并已经超过了该限制。

`user_chat_thread_memberships` 表上已存在 `(user_id, thread_id) UNIQUE` 和 `(thread_id, user_id)` 索引，因此这不是缺失索引的问题。

* * *

## 临时解决方案

作为即时缓解措施，我们删除了底层线程在过去 15 天内无任何活动的追踪级别成员记录：

```sql
DELETE FROM user_chat_thread_memberships uctm
USING chat_threads t
WHERE t.id = uctm.thread_id
  AND uctm.notification_level = 2
  AND NOT EXISTS (
    SELECT 1 FROM chat_messages m
    WHERE m.thread_id = t.id
      AND m.created_at > NOW() - INTERVAL '15 days'
  )

```

清理后，那位重度用户的成员记录从 3,738 条降至 77 条，聊天瞬间加载完成。

 ![image](https://global.discourse-cdn.com/meta/original/4X/c/b/e/cbe12ef800ff4f929214cc225af7bc18247c0917.png)

此举是安全的：这些记录均不对应用户的明确选择（全部为自动追踪，级别 2），如果用户再次与已清理的线程互动，下次回复或阅读时会自动创建新的成员记录。

重度用户的成员记录从 1,500–3,700 条降至 150 条以下，聊天功能恢复正常。我们计划将此操作安排为定期任务，直到上游行为得到根本性修复。

* * *

## 建议的修复方向

1. 在合理的范围内限制或清理自动追踪成员记录（例如，通过定期任务删除闲置超过 N 天的线程中 `notification_level = 2` 的记录）。
2. 考虑群组私信中“自动将所有参与者添加到每个线程”的行为是否可以使用比为每个参与者每个线程创建完整 `user_chat_thread_memberships` 记录更轻量级的记账机制。

---

_[View the full topic](https://meta.discourse.org/t/chat-pitchfork-timeouts-replies-silently-create-threads-and-auto-tracking-bloats-over-time/401067)._
