環境
-
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 月中旬頃に発生し始めたようです。
再現手順
-
Discourse トピックを作成するように設定された WordPress 投稿を公開する
-
トピックが Discourse 上で正常に作成される
-
discourse_topic_idが投稿メタデータに保存される ✓ -
wpdc_sync_post_commentsが1に設定される (Webhook が正しく発火する) ✓ -
discourse_permalinkが空の文字列として保存される ✗ -
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 が書き込まれず、プラグインはページロードのたびに再試行しますが、毎回失敗します。
診断
以下のコードパスを通じて問題を追跡しました。
-
DiscourseCommentFormatter::format()がdo_action('wpdc_sync_discourse_comments')を呼び出し、これがDiscourseComment::sync_comments()をトリガーする -
sync_comments()がdiscourse_permalinkをチェックする — 空であるため、0 を返す -
format()は投稿カスタムフィールド内のdiscourse_comments_rawをチェックする — 欠落しているため、bad_response_html()を返す -
コメントは該当投稿に対して表示されない
トピック作成フローは discourse_topic_id を書き込んでいますが、パーマリンクの永続化に失敗しています。Discourse API の /t/{topic_id}.json に対してクエリを実行し、パーマリンクを投稿メタデータに書き戻すことで、正しいパーマリンクを再構築でき、これにより 174 件の全投稿の同期が解決しました。
回避策
以下の処理を行う WP-CLI 修復スクリプトを作成しました。
-
wpdc_sync_post_comments = 1だがdiscourse_comments_rawが欠落している投稿を見つける -
/t/{topic_id}.jsonからトピックスラッグを取得する -
パーマリンクを再構築して保存する
-
/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 か月分のコンテンツ) が影響を受けました。