24시간 지난 원화 입금 신청이 자동 만료되지 않는다는 문의를 받고 원인을 추적했다. 옛 시스템의 crontab 을 열어 그 명령이 걸린 줄을 찾았다.
Table of contents
Open Table of contents
증상 — 주석 처리된 스케줄
그 줄은 이렇게 돼 있었다.
# 0 * * * * /usr/bin/php /app/batch/expire_deposit_requests.php
expire_deposit_requests.php 가 주석 처리돼 있으니 기능을 껐구나 싶었다.
자동 만료 기능이 비활성화돼 있다고 그대로 답할 뻔했다. 증상과 그 # 한 줄이 딱 맞아떨어져 더 볼 이유가 없어 보였다.
그대로 답할 뻔했다
그런데 그 답에는 설명되지 않는 구석이 하나 남아 있었다.
왜 껐는지가 설명되지 않았고 껐으면 대체가 있거나 요구사항이 없어졌어야 하는데 둘 다 확인하지 않았다.
# 한 줄이 그 자리에서 답을 주는 것처럼 보였다. 그것이 왜 그렇게 됐는지는 아무것도 말해 주지 않았다.
껐다는 사실은 상태이고 왜 껐는지는 이유다. 상태만 보고 답하면 그 # 가 나온 맥락을 통째로 건너뛴다. 껐다는 것만 알고 이유를 모르면 되살려도 되는지조차 못 정한다.
원인 — 신 시스템에 있었다
이 시스템은 옛 PHP 배치의 crontab 과 리뉴얼한 Node.js 워커의 cron.schedule 이 함께 도는 기간이었다.
cron.schedule('*/5 * * * *', function () {
repo.findByStatus('deposit_request', 'pending', function (rows) {
rows.forEach(function (r) { queue.push({ type: 'expire', id: r.id }); });
});
});
cron.schedule 이 5분 주기로 같은 일을 하고 있었다.
queue.push 만 하고 끝나는지도 봤다.
class ExpireHandler {
handle(msg) {
switch (msg.status) {
case 'pending': return this.expire(msg); // 만료
case 'matched': return this.skip(msg); // 이미 입금 확인됨
case 'canceled': return this.skip(msg); // 사용자가 취소함
}
}
}
ExpireHandler 가 status 별로 처리하고 있었다.
queue.push 만 있고 handle 이 없으면 큐가 쌓이기만 하는데 양쪽이 다 있었다. 여기까지 보고 나니 그 # 가 폐지가 아니라 이관이라는 것이 분명해졌다.
주기도 달랐는데 crontab 쪽은 한 시간이고 cron.schedule 쪽은 5분이다. 옮기면서 주기까지 다시 정한 흔적으로 읽힌다. 그대로 옮긴 것이 아니라 옮기면서 값을 다시 정한 셈이다.
제약 — 확정하지 못한 부분
다만 이것을 확정할 근거가 약했다.
이관을 명시한 주석 없음
이관 커밋 메시지 확인 못 함
crontab 줄과 cron.schedule 이 둘 다 있다는 것으로 판단한 것이다.
그래서 기록에는 이관을 명시한 주석과 커밋을 확인하지 못했고 두 저장소의 코드 병존으로 판단했다고 적었다.
# 옆에 이관을 적은 한 줄만 있었어도 확정이었을 텐데 없었다. 확정이라고 쓸 근거가 없을 때는 그것을 판단이라고 적는 편이 맞았다.
다음 사람이 그 강도를 알고 읽어야 잘못된 확신이 안 생긴다. 판단을 확정처럼 적으면 그 뒤의 결정이 전부 그 위에 쌓인다.
대응 — 바뀐 조사 방향
이것으로 문의 조사의 방향이 바뀌었다. 옛 것을 켜는 것과 새 쪽을 고치는 것 두 가지 길 중 뒤쪽이다.
[처음] 옛 스케줄러가 꺼져 있다 → 켜면 되나?
[실제] 신 시스템이 도는데 안 된다 → 거기를 봐야 한다
crontab 쪽을 켜면 cron.schedule 과 같은 대상을 처리해 이중 처리가 된다.
주석을 폐지로 읽고 되살리는 것이 그 상황에서 가장 나쁜 선택이었고 되살리는 대신 다른 쪽을 확인하는 것이 순서였다. 고치기 전에 지금 무엇이 실제로 도는지부터 보는 것이 먼저였다.
미처리 원인은 새 쪽의 수집과 소비 구간에서 찾아야 했고 방향이 바뀐 뒤에야 볼 곳이 정해졌다.
교훈 — 주석에 적혀 있어야 할 것
이 조사가 길어진 이유는 그 줄에 아무 이유도 없었기 때문이다.
# v2 워커의 expireDepositRequests 로 이관 (2018-xx)
# 0 * * * * /usr/bin/php /app/batch/expire_deposit_requests.php
expireDepositRequests 를 적은 한 줄이면 됐고 그 한 줄이 없어서 두 저장소를 다 뒤졌다.
주석 처리한 코드에는 왜 껐는지와 옮긴 곳까지 적어야 하고 껐다는 사실만 남으면 다음 사람이 같은 조사를 다시 한다.
병존 구조에서는 한쪽만 봐서는 판단이 안 된다.
옛 것에 없다 → 없는 게 아니라 새 것에 있을 수 있다
새 것에 없다 → 아직 안 옮겨져 옛 것에 있을 수 있다
항상 양쪽 스케줄러를 보고 어느 쪽이 실제로 도는지도 확인해야 한다. 둘 다 돌고 있으면 그것대로 이중 처리이고 둘 다 꺼져 있으면 그때가 진짜 폐지다.
병존 기간에는 한쪽 저장소의 코드가 그 기능의 전부가 아니고 어디에 있느냐보다 어느 쪽이 도느냐가 답이었다.
정리
- 주석 처리가 기능 폐지를 뜻하지 않는다
- 증상과 코드가 맞아떨어져 보여도 왜 껐는지가 설명돼야 한다
- 병존 구조에서는 다른 쪽으로 이관됐을 수 있다
- 주석을 발견하면 다른 쪽의 스케줄러와 소비 쪽을 먼저 확인한다
- 넣는 쪽과 꺼내는 쪽이 다 있는지 본다
- 옛 것을 다시 켜면 이중 처리가 된다
- 되살리기 전에 다른 쪽을 확인한다
- 확정 근거가 없으면 병존으로 판단했다고 적는다
- 주석 처리한 코드에는 왜 껐는지와 어디로 갔는지를 적는다
- 한쪽만 보고 내린 결론은 한 번은 틀린다
- 둘 다 돌고 있으면 그것대로 이중 처리다