장기 미처리 취소 대기 건을 분석하다가 일부의 상태가 이상한 것을 봤는데, channel_claims.status 는 success 인데 order_cancels.status 는 REQUESTED 로 남아 있었다.
Table of contents
Open Table of contents
조사 — 흐름의 단계 정의
코드를 뒤지기 전에 취소 요청이 들어와서 처리되기까지의 흐름을 단계로 먼저 그렸다.
① 수집이 클레임을 대기 상태로 저장
② 스케줄러가 대기 건을 큐로 전송
③ 컨슈머가 외부 API 호출
④ 성공하면 클레임을 success 로 마킹
클레임이 success 라는 것은 네 번째 단계까지 정상적으로 갔다는 뜻이다. 단계를 먼저 정의해 두니 어디까지 갔는지가 상태값 하나로 확인됐다.
원인 — 빠진 마지막 단계
여기에 다섯 번째 단계가 있어야 했는데 order_cancels.status 를 REQUESTED 에서 COMPLETED 로 전환하는 것이다.
ClaimConsumer 코드를 보니 클레임만 success 로 바꾸고 order_cancels 는 건드리지 않은 채 끝나고 있었다. 그래서 외부 처리는 완료이고 클레임도 성공인데 우리 취소만 요청 상태로 영구히 남는다.
그 상태로 화면에는 취소 대기로 뜨고 입점사는 취소가 안 됐다며 문의를 넣는다. 실제로는 끝난 일인데 우리 기록만 안 끝난 것이라 어느 쪽 말도 틀리지 않았다.
재수집이 안 통하는 이유
재수집하면 상태가 갱신되지 않을까 싶어 수집 코드를 봤다.
if ($existingClaim) {
// $claim->status = 'ready_v2'; ← 주석 처리
return;
}
기존 레코드가 있으면 수정 분기로 들어가는데 거기서 상태를 되돌리는 로직이 주석 처리돼 있어 아무 효과가 없다. 게다가 클레임이 이미 success 면 스케줄러가 ready 만 집으므로 그 건을 아예 안 집는다.
즉 재수집으로는 어떤 경우에도 안 풀리는 구조였다. 이걸 미리 확인하지 않았으면 재수집을 몇 번 돌려 보고 왜 안 되는지를 다시 조사했을 것이다.
조치 — 수동 복구와 근본 수정
두 가지 방법이 있었는데 밀린 건들의 클레임을 ready 로 되돌려 컨슈머를 다시 태우는 것과 우리 쪽 기록만 손으로 맞추는 것이다. 뒤쪽을 골랐다. 이유는 외부는 이미 끝났으므로 다시 태우면 취소 요청이 중복으로 나가서다. 대신 status 와 cancelled_at 과 order_items.status 세 값을 UPDATE 로 맞추는 스크립트를 따로 검토받아야 하는 것을 감수했다.
근본 수정은 ClaimConsumer 에 다섯 번째 단계를 추가하는 것이다. 수동 복구는 밀린 것만 치우고 그 뒤로 또 쌓이므로 컨슈머를 안 고치면 같은 복구를 계속 반복하게 된다.
판단 기준 — 책임 경계의 공백
왜 빠졌는지 짐작해 보면 책임 경계가 애매했기 때문인 것 같다. ClaimConsumer 는 외부 호출과 channel_claims 관리가 자기 일이고 order_cancels 는 다른 쪽 일이라고 봤을 수 있다.
문제는 그 다른 쪽이 존재하지 않았다는 점이고 결국 아무도 그 값을 안 바꾼다. 두 시스템에 걸친 흐름에서 마지막 단계가 누구 것인지 정해지지 않으면 그 단계가 통째로 빠지고 양쪽 다 자기 일은 다 했다고 본다.
흐름을 먼저 그린 것이 이 진단에서 유효했다. 단계를 정의하고 각 단계의 증거를 상태값에서 찾으면 빠진 데가 한눈에 드러난다. 코드부터 뒤졌으면 여기도 없고 저기도 없다는 식으로 한참을 헤맸을 것이다.
정리
- 외부 처리는 끝났는데 우리 상태만 안 바뀌는 경우가 있다
ClaimConsumer가 자기 레코드만 바꾸고order_cancels를 안 건드리면 그렇게 된다- 재수집은 기존 레코드를 만나면 다른 분기로 가서 효과가 없다
- 상태가 이미 최종이면 스케줄러가 그 건을 안 집는다
- 복구는 우리 기록만 맞추는 것이고 외부는 보낼 것이 없다
- 근본은 컨슈머에 전환 단계를 추가하는 것이다
- 마지막 단계의 주인이 안 정해지면 그 단계가 통째로 빠진다
- 단계를 정의하고 각 단계의 증거를 찾으면 빠진 데가 드러난다