空的 `discourse_permalink` 导致静默评论同步失败

环境

  • WP-Discourse 版本: 2.5.7

  • Discourse 版本: 3.5.0.beta5-dev

  • WordPress 版本: 6.9

  • 托管: WP Engine (WordPress 多站点)

  • Discourse 实例: 自托管 / Discourse 托管 (forum.avweb.com)

摘要

从 WordPress 发布的内容成功在 Discourse 上创建了主题,但 discourse_permalink 存储在帖子元数据中时为空字符串。这导致所有后续的评论同步静默失败,因为 discourse-comment.php 中的 sync_comments() 在永久链接为空时会提前退出。Discourse 主题 ID 和 Webhook 同步标志写入正确——只是缺少永久链接。

我们在多站点网络中的一个站点上确定了 174 个受影响的帖子。该问题似乎始于 2025 年 12 月中旬左右。

重现步骤

  1. 发布一个配置为创建 Discourse 主题的 WordPress 帖子

  2. 主题在 Discourse 上成功创建

  3. discourse_topic_id 保存到帖子元数据中 ✓

  4. wpdc_sync_post_comments 设置为 1 (Webhook 正确触发) ✓

  5. discourse_permalink 保存为空字符串 ✗

  6. discourse_comments_raw 从未写入 (同步从未完成)

预期行为

成功创建主题后,discourse_permalink 应包含完整的 Discourse 主题 URL (例如,The China Chickens Come Home - AVweb - News Discussion - AVweb.com Discussion)。

实际行为

discourse_permalink 作为元键存在,但其值为空。每次后续调用 sync_comments() 都会命中 lib/discourse-comment.php 第 209 行的此保护并提前返回:

if ( ! $discourse_permalink ) {
    return 0;
}

由于同步从未完成,discourse_last_sync 从未写入,因此插件会在每次页面加载时重试——并且每次都失败。

诊断

我们通过以下代码路径追踪了问题:

  1. DiscourseCommentFormatter::format() 调用 do_action('wpdc_sync_discourse_comments'),这会触发 DiscourseComment::sync_comments()

  2. sync_comments() 检查 discourse_permalink —— 发现为空,返回 0

  3. format() 然后检查帖子自定义字段中的 discourse_comments_raw —— 发现缺失,返回 bad_response_html()

  4. 评论从未显示在受影响的帖子中

主题创建流程正在写入 discourse_topic_id,但未能持久化永久链接。我们通过查询 Discourse API 在 /t/{topic_id}.json 处重建了正确的永久链接,并将其写回帖子元数据,从而解决了所有 174 个帖子的同步问题。

变通方法

我们编写了一个 WP-CLI 修复脚本,它:

  1. 查找 wpdc_sync_post_comments = 1 但缺少 discourse_comments_raw 的帖子

  2. /t/{topic_id}.json 获取主题 slug

  3. 重建并保存永久链接

  4. /t/{slug}/{topic_id}/wordpress.json 获取并保存评论

我们将其作为临时措施在计划的 cron 任务上运行。


调查期间发现的额外问题

1. 全局 MySQL 锁导致静默同步失败

文件: lib/discourse-comment.php,第 176 行

$got_lock = $wpdb->get_row( "SELECT GET_LOCK( 'discourse_lock', 0 ) got_it" );

sync_comments() 使用一个跨整个安装的所有帖子共享的全局 MySQL 锁 (discourse_lock)。超时设置为 0 (非阻塞),因此如果任何帖子当前正在同步,所有其他帖子都会静默跳过它们的同步——没有日志记录,没有重试。

在每天发布多个帖子的流量大的站点上,这会产生一个竞态条件,导致帖子持续丢失锁而永远无法同步。结合 10 分钟的同步周期,如果一个帖子在最初几次尝试中丢失了锁,并且由于永久链接为空的问题阻止了未来的同步,它可能会被永久卡住。

建议的修复: 使用每个帖子的锁:

$got_lock = $wpdb->get_row( "SELECT GET_LOCK( 'discourse_lock_{$post_id}', 0 ) got_it" );

以及相应的释放:

$wpdb->get_results( "SELECT RELEASE_LOCK( 'discourse_lock_{$post_id}' )" );

2. discourse_comments_raw 的双重转义

文件: lib/discourse-comment.php,第 222 行

update_post_meta( $post_id, 'discourse_comments_raw', esc_sql( $raw_body ) );

update_post_meta() 在内部使用 $wpdb->prepare(),因此用 esc_sql() 包装值会导致双重转义。在多个同步周期中,JSON 主体中的引号会积累转义字符 (\\"\\\\\\"\\\\\\\\\\\\\\"),直到 JSON 变得无法解析。

建议的修复:

update_post_meta( $post_id, 'discourse_comments_raw', $raw_body );

影响

这些问题会累积:空永久链接会阻止初始同步,全局锁会阻止恢复,而双重转义可能会破坏那些设法同步的帖子的数据。在我们的安装中,在我们确定根本原因之前,有 174 个帖子(大约 2 个月的内容)受到了影响。