오늘 저는 Discourse 설치 버전을 2.9.0.beta9에서 2.9.0.beta10으로 업그레이드하려고 시도했습니다. 하지만 결과는 최악이었습니다.
첫 번째 시도는 콘솔에서 진행했습니다.
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'
아직 남아 있던 이전 이미지를 시작하여 포럼을 간단히 복구할 수 있었습니다.
두 번째 시도는 웹 콘솔(/admin/upgrade)을 통해 진행했습니다. 하지만 결국 같은 유형의 오류(ERROR: cannot extract elements from a scalar)에서 멈췄습니다. 업그레이드 패널로 돌아가 보니 모든 구성 요소가 정상적으로 업데이트되었다고 표시되었습니다. 하지만 컨테이너를 재시작한 후 서버는 이제 HTTP 500 오류를 발생시켰습니다
별도의 컴퓨터에서 처음부터 시작하여 클린 설치를 수행했습니다. 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 값을 받아들이기 때문에 이런 문제가 발생하는 것입니다.
말씀하신 대로, 이는 기존 Q&A 게시물로부터의 데이터 오염으로 느껴집니다. 또한 이 마이그레이션이 2021년 11월에 이루어진 것인데도 이제야 이런 문제가 나타나는 것이 흥미롭습니다. 아마도 JayJay가 이제서야 새로운 플러그인으로 전환하기 때문일 것입니다. 어쨌든 해당 마이그레이션을 다시 한번 살펴볼 것입니다.
손상된 데이터는 PostCustmField 테이블에 있습니다. 하지만 해당 플러그인이 없다면, 이 데이터를 새 테이블로 마이그레이션하려고 시도하지 않습니다.
문제는 마이그레이션 자체에 있는 것이 아니라, 과거 어느 시점에 데이터가 손상된 것입니다. 이미 깨져 있었지만, 수년간 아무도 알아채지 못할 정도로 손상된 것입니다. 아마도 새로운 업보트가 발생할 때마다 데이터가 더 손상되었을 것입니다. 합리적인 해결책은 손상된 데이터를 무시하는 것일 수 있습니다. 예를 들어, 이를 손상된 것으로 표시하거나(사용자 정의 필드 이름을 변경하는 등) 향후 마이그레이션에서 이를 무시하도록 하고, 원한다면 누군가가 수동으로 이를 수정할 수 있도록 하는 것입니다.
구버전의 QnA 플러그인은 표준 Discourse 커스텀 필드 타입 캐스팅을 사용했는데, 이는 해당 필드를 JSON으로 캐스팅하는 데 사용되도록 설계되었습니다. 하지만 이 방식은 이 경우와 같은 일부 상황에서는 제대로 작동하지 않습니다. 이것이 바로 Discourse 커스텀 필드를 다룰 때(특히 이 사례처럼 상당히 오래된 코드와 데이터를 다룰 때) 형식 검사를 내장하는 것이 이상적인 이유입니다. 즉각적인 오류가 발생하는 곳은 Upvotes 플러그인이며, 이 플러그인이 여기서 수행해야 할 작업이 바로 그 형식 검사라고 생각합니다.
데이터 탐색기(data explorer) 플러그인을 사용하여 해당 테이블에 어떤 데이터가 있는지 확인할 수 있습니다.
일부 사용자의 app.yml 파일에 저장소 URL로 제 개인 GitHub 사용자명이 포함된 매우 오래된 버전의 질문 답변(Question Answer) 플러그인이 설치되어 있습니다.
저는 몇 년 전 QnA 플러그인을 paviliondev로 이전했습니다. GitHub는 저장소가 이전되면 URL을 리다이렉트하므로, 제 개인 사용자명이 포함된 기존 URL도 계속 작동했습니다.
그 후 몇 년이 지나 Pavilion이 질문 답변 플러그인을 Discourse로 이전했습니다. Discourse는 처음에 discourse-question-answer라는 이름을 유지했습니다.
몇 달 전, 해당 플러그인이 여전히 discourse-question-answer로 불리던 시절에 discourse 조직 내의 플러그인 자체 포크를 생성했습니다.
제 개인 GitHub 계정으로 연결된 매우 오래된 QnA 플러그인 링크를 가진 사용자들은 이제 discourse에 호스팅된, 상당 부분 업데이트된 discourse-question-answer의 제 새 포크를 클론하게 되었습니다. 다시 말해, 매우 오래된 링크가 이제 새 플러그인의 포크를 가리키고 있었던 것입니다. 그 사이에 수년의 시간이 흘렀음에도 불구하고 이런 상황을 예상하지 못한 점에 대해 사과드립니다. 해당 포크는 삭제했습니다.
Discourse는 discourse-question-answer의 이름을 discourse-upvotes로 변경했습니다. 이 이름 변경은 @Jaap-Jan_Swijnenburg 님의 사례에 실질적인 영향을 미치지 않았지만, 이것이 바로 (예상치 못하게) 해당 저장소의 포크를 클론하게 된 이유입니다.
discourse/discourse-upvotes(구 discourse-question-answer)의 마이그레이션은 post_custom_fields의 value 컬럼이 JSON으로 타입 안전하다고 가정합니다.
플러그인이 angusmcleod/discourse-question-answer이던 시절(몇 년 전)에 생성된 오래된 데이터는 discourse/discourse의 HasCustomFields concern에 의해 유효한 JSON으로 저장되지 않았습니다. 추측컨대, 이 데이터는 아마도 JSON 타입 확인이 추가되기 4년 전에 추가되었을 것입니다(JSON으로 등록된 커스텀 필드에서도 엣지 케이스로 인해 유효하지 않은 JSON이 생성될 수 있습니다).
따라서
app.yml에 (수년 전의) angusmcleod/discourse-question-answer URL을 가진 사용자가 Discourse를 업데이트할 때, 새 버전의 플러그인에서 마이그레이션이 클론되고 실행되며, 이로 인해 잠재적으로 이 오류가 발생할 수 있습니다.
이 문제를 해결하는 몇 가지 방법이 있습니다.
@Jaap-Jan_Swijnenburg 님의 경우, 제 오래된 QnA 플러그인에 대한 참조를 제거하면 사이트를 다시 빌드할 수 있습니다. 그것뿐입니다; 그 이상은 없습니다. 이미 그렇게 하신 것으로 보입니다
discourse/discourse-upvotes 플러그인의 마이그레이션을 업데이트하여 post_custom_fields의 value 컬럼에 있는 비-JSON 값을 처리하도록 할 수 있습니다.
2번 방안은 실제로 오래된 버전의 QnA 플러그인에서 discourse-upvotes로 전환하려는 사용자들의 추가적인 경우를 처리할 수도 있음을 언급하고 싶습니다. 그 경우 마이그레이션이 실행되며, post_custom_fields의 value 컬럼에 유효하지 않은 JSON 항목이 하나라도 있으면 실패하게 됩니다.