재구축 중 오류를 일으키는 플러그인

오늘 저는 Discourse 설치 버전을 2.9.0.beta9에서 2.9.0.beta10으로 업그레이드하려고 시도했습니다. 하지만 결과는 최악이었습니다.

  1. 첫 번째 시도는 콘솔에서 진행했습니다.
cd /var/discourse
sudo git pull
sudo ./launcher rebuild app

결국 로그 중간 어딘가에서 다음과 같은 메시지와 함께 중단되었습니다.

PG::InvalidParameterValue: ERROR:  cannot extract elements from a scalar
/var/www/discourse/vendor/bundle/ruby/2.7.0/gems/rack-mini-profiler-3.0.0/lib/patches/db/pg.rb:110:in `exec'

아직 남아 있던 이전 이미지를 시작하여 포럼을 간단히 복구할 수 있었습니다.

  1. 두 번째 시도는 웹 콘솔(/admin/upgrade)을 통해 진행했습니다. 하지만 결국 같은 유형의 오류(ERROR: cannot extract elements from a scalar)에서 멈췄습니다. 업그레이드 패널로 돌아가 보니 모든 구성 요소가 정상적으로 업데이트되었다고 표시되었습니다. 하지만 컨테이너를 재시작한 후 서버는 이제 HTTP 500 오류를 발생시켰습니다 :frowning_face:

  2. 별도의 컴퓨터에서 처음부터 시작하여 클린 설치를 수행했습니다. beta10 버전은 설치할 수 있었지만, 백업에서 복원하려고 시도하자 정확히 동일한 오류가 발생했습니다!

도움을 주실 수 있는 분이 계신가요?

문제가 되는 쿼리를 찾았습니다.

INSERT INTO question_answer_votes (post_id, user_id, created_at)
	SELECT
	  X.post_id AS post_id,
	  (X.value->>'user_id')::int AS user_id,
	  (X.value->>'created_at')::timestamp AS created_at
	FROM (
	  SELECT
	    post_id,
	    jsonb_array_elements(value::jsonb) AS value
	  FROM post_custom_fields WHERE name = 'vote_history'
	) AS X
	WHERE (X.value->>'action') != 'destroy'
	ORDER BY (X.value->>'created_at')::timestamp DESC
	ON CONFLICT DO NOTHING

제 이해가 맞다면, psql 함수인 jsonb_array_elements는 배열을 기대하지만 NULL 값을 받아들이기 때문에 이런 문제가 발생하는 것입니다.

이와 관련된 보고가 몇 건 있었습니다:

이전에 Pavilion 질문/답변 플러그인이 설치되어 있었던 것과 관련이 있을 것 같습니다.

누군가 더 깊이 살펴볼 수 있도록 알아보겠습니다. :+1:

다음 플러그인을 사용 중입니다: https://github.com/angusmcleod/discourse-question-answer.

  • Pavilion 질문 및 답변(Question and Answer) 플러그인과 관련이 있는 것으로 보입니다.
  • discourse/discourse-upvotes 플러그인의 포크(fork)입니다.

확인 차원에서, app.yml에 어떤 플러그인이 있나요?

내가 확인한 사이트에는 Post Custom Fields에 여러 번 인코딩된 문자열이 대량으로 포함되어 있어 사용할 수 없는 상태였습니다.

복잡도 순서대로 몇 가지 해결 방법을 제시합니다.

  • 플러그인 사용을 중단하기 (새로운 업보트 기능으로 전환하는 것도 고려해 볼 수 있음)
  • 모든 항목 삭제
  • 손상된 항목만 삭제
  • 해당 필드를 편집하여 데이터를 다시 유효한 JSON 문자열로 복구하기

내가 확인한 사이트에는 수년 전부터 존재하던 손상된 데이터가 있었습니다. 이는 수년 전에 존재했던 버그가 이제야 발견된 것으로 보입니다.

손상된 JSON 문자열을 수정하는 코드를 작성하는 것이 가능할 수 있지만, 10분 안에 어떻게 해야 하는지 확인하지는 못했습니다.

예제는 Question Answer Plugin - #301 by pfaffman 를 참고하세요.

저도 이 생각을 했었지만, Angus의 포크를 간단히 확인해 보니 데이터 마이그레이션이 수행된 것으로 보이지 않습니다. https://github.com/angusmcleod/discourse-question-answer/compare/main...discourse:discourse-upvotes:main

말씀하신 대로, 이는 기존 Q&A 게시물로부터의 데이터 오염으로 느껴집니다. 또한 이 마이그레이션이 2021년 11월에 이루어진 것인데도 이제야 이런 문제가 나타나는 것이 흥미롭습니다. 아마도 JayJay가 이제서야 새로운 플러그인으로 전환하기 때문일 것입니다. 어쨌든 해당 마이그레이션을 다시 한번 살펴볼 것입니다.

          - git clone https://github.com/discourse/docker_manager.git
          - git clone https://github.com/discourse/discourse-solved.git
          - git clone https://github.com/unfoldingWord-dev/discourse-mermaid.git
          - git clone https://github.com/angusmcleod/discourse-question-answer.git
          - git clone https://github.com/discourse/discourse-checklist.git
          - git clone https://github.com/discourse/discourse-cakeday.git
          - git clone https://github.com/discourse/discourse-canned-replies.git
          - git clone https://github.com/discourse/discourse-footnote.git
          - git clone https://github.com/discourse/discourse-staff-notes.git
          - git clone https://github.com/discourse/discourse-graphviz.git
          - git clone https://github.com/discourse/discourse-assign.git
          - git clone https://github.com/discourse/discourse-voting.git
          - git clone https://github.com/discourse/discourse-yearly-review.git
          - git clone https://github.com/discourse/discourse-saved-searches.git

@nat 저는 새 플러그인으로 전환할 생각이 전혀 없습니다. 이미 꽤 오랫동안 사용해 온 플러그인 세트로 계속 운영하고 있습니다.

적어도 매달 업데이트를 실행하기 때문에 버전 간에 큰 차이가 있는 것은 아닙니다.

질문-답변 문제와는 별개로, 참고로 canned-replies의 대체 기능이 추가되었습니다:

또한 official Mermaid 테마 컴포넌트도 추가된 것으로 알고 있습니다:

감사합니다. 원래 문제가 해결된 후에 확인해볼게요 :smile:

플러그인을 제거하더라도 백업을 복원하면 문제 있는 쿼리가 여전히 실행됩니다.
따라서 플러그인뿐 아니라 이 플러그인이 사용하는 모든 테이블과 쿼리를 제거해야 합니다.
관련된 테이블을 어떻게 파악할 수 있을까요?

플러그인 관련 행을 주석 처리하고, 재빌드를 시도해 확인해 보셨나요?

지금 바로 시도해 볼게요. 백업의 일부인 dump.sql 파일을 확인해 봤는데, question_answer_* 테이블에 대한 언급이 전혀 없네요. 그래서 희망이 생기는 것 같아요…

스크래치 시스템에서 성공적으로 해결했습니다!

  • 플러그인 비활성화
  • sudo ./launcher rebuild app
  • 백업 복원

이제 라이브 시스템에서 작업을 진행하겠습니다. 진행 상황을 공유하겠습니다.

손상된 데이터는 PostCustmField 테이블에 있습니다. 하지만 해당 플러그인이 없다면, 이 데이터를 새 테이블로 마이그레이션하려고 시도하지 않습니다.

문제는 마이그레이션 자체에 있는 것이 아니라, 과거 어느 시점에 데이터가 손상된 것입니다. 이미 깨져 있었지만, 수년간 아무도 알아채지 못할 정도로 손상된 것입니다. 아마도 새로운 업보트가 발생할 때마다 데이터가 더 손상되었을 것입니다. 합리적인 해결책은 손상된 데이터를 무시하는 것일 수 있습니다. 예를 들어, 이를 손상된 것으로 표시하거나(사용자 정의 필드 이름을 변경하는 등) 향후 마이그레이션에서 이를 무시하도록 하고, 원한다면 누군가가 수동으로 이를 수정할 수 있도록 하는 것입니다.

실서버에서도 성공했습니다. 플러그인을 비활성화한 후 진행했습니다.
왜 이 문제를 이전에 발견하지 못했는지 여전히 궁금합니다. 말씀드렸듯이 매월 정기적으로 모든 시스템을 업데이트하므로, 이 문제를 더 일찍 발견했어야 합니다.

post_custom_fields 테이블에 무엇이 있는지 확인하려면, Discourse DB에 직접 연결하는 방법이 있을까요?

@Jaap-Jan_Swijnenburg 문제가 발생하셨다니 죄송합니다.

그 부분에는 정중히 이견을 표합니다. :slight_smile:

구버전의 QnA 플러그인은 표준 Discourse 커스텀 필드 타입 캐스팅을 사용했는데, 이는 해당 필드를 JSON으로 캐스팅하는 데 사용되도록 설계되었습니다. 하지만 이 방식은 이 경우와 같은 일부 상황에서는 제대로 작동하지 않습니다. 이것이 바로 Discourse 커스텀 필드를 다룰 때(특히 이 사례처럼 상당히 오래된 코드와 데이터를 다룰 때) 형식 검사를 내장하는 것이 이상적인 이유입니다. 즉각적인 오류가 발생하는 곳은 Upvotes 플러그인이며, 이 플러그인이 여기서 수행해야 할 작업이 바로 그 형식 검사라고 생각합니다.

데이터 탐색기(data explorer) 플러그인을 사용하여 해당 테이블에 어떤 데이터가 있는지 확인할 수 있습니다.

음, 이 부분은 대부분 당신을 따르겠습니다. 그리고 우리는 실제로 동의하고 있다고 생각합니다. 데이터가 손상되지 않았다면 마이그레이션이 작동한다는 것이 사실이라고 생각합니다. 데이터가 손상된 경우, 현재보다 더 우아하게 실패해야 한다는 점에도 동의합니다.

네, 하지만 데이터는 몇 년 전에 손상되었을 가능성이 높습니다(제가 잘 아는 사이트의 경우가 그렇습니다). 하지만 재앙적으로 실패하지 않았기 때문에 눈치채지 못했을 뿐입니다. 업보트가 의도대로 관리되지 않았을 것이라고 꽤 확신합니다. 하지만 아무도 알아채지 못했습니다.

rails를 통해 이렇게 하면 됩니다:

./launcher enter app
rails c

그런 다음 이런 식으로:

all_votes=PostCustomField.where(name: "vote_history")
likely_broken_votes=PostCustomField.where(name: "vote_history").where("value like '\"%'")

id와 데이터만 확인하려면:

all_votes.pluck(:id,:value)

pcf 하나만 가져오기:

pcf=PostCustomField.find(1234)

수정하기:

pcf.value='실제로 넣고 싶은 내용'
pcf.save

여기서 일어나고 있는 일은 다음과 같습니다.

  1. 일부 사용자의 app.yml 파일에 저장소 URL로 제 개인 GitHub 사용자명이 포함된 매우 오래된 버전의 질문 답변(Question Answer) 플러그인이 설치되어 있습니다.

  2. 저는 몇 년 전 QnA 플러그인을 paviliondev로 이전했습니다. GitHub는 저장소가 이전되면 URL을 리다이렉트하므로, 제 개인 사용자명이 포함된 기존 URL도 계속 작동했습니다.

  3. 그 후 몇 년이 지나 Pavilion이 질문 답변 플러그인을 Discourse로 이전했습니다. Discourse는 처음에 discourse-question-answer라는 이름을 유지했습니다.

  4. 몇 달 전, 해당 플러그인이 여전히 discourse-question-answer로 불리던 시절에 discourse 조직 내의 플러그인 자체 포크를 생성했습니다.

  5. 제 개인 GitHub 계정으로 연결된 매우 오래된 QnA 플러그인 링크를 가진 사용자들은 이제 discourse에 호스팅된, 상당 부분 업데이트된 discourse-question-answer의 제 새 포크를 클론하게 되었습니다. 다시 말해, 매우 오래된 링크가 이제 새 플러그인의 포크를 가리키고 있었던 것입니다. 그 사이에 수년의 시간이 흘렀음에도 불구하고 이런 상황을 예상하지 못한 점에 대해 사과드립니다. 해당 포크는 삭제했습니다.

  6. Discourse는 discourse-question-answer의 이름을 discourse-upvotes로 변경했습니다. 이 이름 변경은 @Jaap-Jan_Swijnenburg 님의 사례에 실질적인 영향을 미치지 않았지만, 이것이 바로 (예상치 못하게) 해당 저장소의 포크를 클론하게 된 이유입니다.

  7. discourse/discourse-upvotes(구 discourse-question-answer)의 마이그레이션은 post_custom_fields의 value 컬럼이 JSON으로 타입 안전하다고 가정합니다.

  8. 플러그인이 angusmcleod/discourse-question-answer이던 시절(몇 년 전)에 생성된 오래된 데이터는 discourse/discourse의 HasCustomFields concern에 의해 유효한 JSON으로 저장되지 않았습니다. 추측컨대, 이 데이터는 아마도 JSON 타입 확인이 추가되기 4년 전에 추가되었을 것입니다(JSON으로 등록된 커스텀 필드에서도 엣지 케이스로 인해 유효하지 않은 JSON이 생성될 수 있습니다).

따라서

  1. app.yml에 (수년 전의) angusmcleod/discourse-question-answer URL을 가진 사용자가 Discourse를 업데이트할 때, 새 버전의 플러그인에서 마이그레이션이 클론되고 실행되며, 이로 인해 잠재적으로 이 오류가 발생할 수 있습니다.

이 문제를 해결하는 몇 가지 방법이 있습니다.

  1. @Jaap-Jan_Swijnenburg 님의 경우, 제 오래된 QnA 플러그인에 대한 참조를 제거하면 사이트를 다시 빌드할 수 있습니다. 그것뿐입니다; 그 이상은 없습니다. 이미 그렇게 하신 것으로 보입니다 :+1:

  2. discourse/discourse-upvotes 플러그인의 마이그레이션을 업데이트하여 post_custom_fields의 value 컬럼에 있는 비-JSON 값을 처리하도록 할 수 있습니다.

2번 방안은 실제로 오래된 버전의 QnA 플러그인에서 discourse-upvotes로 전환하려는 사용자들의 추가적인 경우를 처리할 수도 있음을 언급하고 싶습니다. 그 경우 마이그레이션이 실행되며, post_custom_fields의 value 컬럼에 유효하지 않은 JSON 항목이 하나라도 있으면 실패하게 됩니다.