환경
-
WP-Discourse 버전: 2.5.7
-
Discourse 버전: 3.5.0.beta5-dev
-
WordPress 버전: 6.9
-
호스팅: WP Engine (WordPress 멀티사이트)
-
Discourse 인스턴스: 셀프 호스팅 / Discourse 호스팅 (forum.avweb.com)
요약
WordPress에서 게시된 게시물이 Discourse에서 주제(Topic)를 성공적으로 생성하지만, discourse_permalink이 게시물 메타(post meta)에 빈 문자열로 저장됩니다. 이로 인해 discourse-comment.php의 sync_comments() 함수가 퍼마링크가 비어 있으면 조기에 종료(bail early)하도록 되어 있어, 모든 후속 댓글 동기화가 조용히 실패합니다. Discourse 주제 ID와 웹훅 동기화 플래그는 올바르게 기록되지만, 퍼마링크만 누락됩니다.
멀티사이트 네트워크의 단일 사이트에서 174개의 영향받는 게시물을 확인했습니다. 이 문제는 2025년 12월 중순경부터 발생하기 시작한 것으로 보입니다.
재현 단계
-
Discourse 주제 생성이 구성된 WordPress 게시물 게시
-
Discourse에서 주제가 성공적으로 생성됨
-
discourse_topic_id가 게시물 메타에 저장됨 ✓ -
wpdc_sync_post_comments가1로 설정됨 (웹훅이 올바르게 발화) ✓ -
discourse_permalink이 빈 문자열로 저장됨 ✗ -
discourse_comments_raw가 기록되지 않음 (동기화가 완료되지 않음)
예상 동작
주제 생성이 성공적으로 완료된 후, discourse_permalink에는 전체 Discourse 주제 URL(예: The China Chickens Come Home - AVweb - News Discussion - AVweb.com Discussion )이 포함되어야 합니다.
실제 동작
discourse_permalink은 메타 키로 존재하지만 그 값은 비어 있습니다. lib/discourse-comment.php의 209번째 줄의 이 가드(guard)를 만나 모든 후속 sync_comments() 호출이 조기에 반환됩니다:
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()은 게시물 커스텀(post custom)에서discourse_comments_raw를 확인합니다 — 누락됨을 발견하고bad_response_html()을 반환합니다. -
영향받는 게시물에 대해 댓글이 표시되지 않습니다.
주제 생성 플로우가 discourse_topic_id를 기록하는 것은 성공하지만 퍼마링크를 영구적으로 저장하는 데 실패하고 있습니다. /t/{topic_id}.json에 대한 Discourse API를 쿼리하여 올바른 퍼마링크를 재구성하고 게시물 메타에 다시 기록함으로써 174개 게시물 모두에 대한 동기화 문제를 해결할 수 있었습니다.
임시 해결책 (Workaround)
다음과 같은 WP-CLI 복구 스크립트를 작성했습니다:
-
wpdc_sync_post_comments = 1이지만discourse_comments_raw가 누락된 게시물 찾기 -
/t/{topic_id}.json에서 주제 슬러그(slug) 가져오기 -
퍼마링크 재구성 및 저장
-
/t/{slug}/{topic_id}/wordpress.json에서 댓글 가져오기 및 저장
이것을 임시 조치로 예약된 크론(scheduled cron)으로 실행하고 있습니다.
조사 중 발견된 추가 문제
1. 전역 MySQL 잠금(Lock)으로 인한 조용한 동기화 실패
파일: lib/discourse-comment.php, 176번째 줄
$got_lock = $wpdb->get_row( "SELECT GET_LOCK( 'discourse_lock', 0 ) got_it" );
sync_comments()는 전체 설치 환경의 모든 게시물에서 공유되는 단일 전역 MySQL 잠금(discourse_lock)을 사용합니다. 타임아웃은 0(비차단식)이므로, 현재 어떤 게시물이 동기화 중이라면 다른 모든 게시물은 조용히 동기화를 건너뜁니다 — 로깅도, 재시도도 없습니다.
하루에 여러 게시물을 게시하는 대량 트래픽 사이트에서는, 게시물이 일관되게 잠금을 잃고 동기화되지 않는 레이스 컨디션(race condition)이 발생합니다. 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의 이중 이스케이프(Double-Escaping)
파일: 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개월 분량의 콘텐츠)이 영향을 받았습니다.