`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_comments1 に設定される (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 からトピックスラッグを取得する

  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 (非ブロッキング) のため、いずれかの投稿が現在同期中の場合、他のすべての投稿はサイレントに同期をスキップします — ログ記録も再試行もありません。

1 日に複数の投稿を公開する高トラフィックサイトでは、投稿がロックを失い、決して同期されない競合状態が発生します。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 か月分のコンテンツ) が影響を受けました。