job 하나가 실패해서 멈춰 있었고 그것을 다시 돌려야 했다. 시도 횟수가 한도에 닿으면 더 안 돈다는 것은 알고 있었으므로 그 값을 손대는 것부터 생각했다.
Table of contents
Open Table of contents
시도 횟수와 한도
MySQL 의 jobs 표를 SELECT 해 보니 시도 횟수와 실패 상태와 마지막 로그가 한 행에 함께 있었다.
SELECT attempt_count, fail_status, last_log FROM jobs WHERE id = ?;
attempt_count 가 3이었고 한도를 찾아보니 코드에 상수로 박혀 있었다.
const MAX_ATTEMPTS = 3;
값이 한도와 같으면 그 job 은 대상에서 빠진다. 멈춘 것은 정확히 그 상태였다.
그러면 attempt_count 를 UPDATE 로 내려 주면 다시 대상에 들어온다는 뜻이 된다. 다만 얼마로 내릴지와 내리기 전에 무엇을 해야 하는지가 남았다.
원인을 안 없애면 또 실패한다
값만 내려서 다시 돌리면 실패하게 만든 조건이 그대로 있으므로 같은 이유로 또 실패한다. 시도 횟수만 다시 소진되고 로그만 쌓인다.
그래서 grep 으로 실패 로그를 먼저 읽어서 무엇 때문에 멈췄는지를 확인해야 했다. 원인이 우리 쪽 자료 문제인지 상대 쪽 응답 문제인지에 따라 없애는 방법이 달라진다.
0이 아니라 한도에서 하나를 뺀다
값을 0으로 되돌리면 한도만큼의 기회가 한꺼번에 생긴다. 원인을 잘못 짚었을 경우 같은 실패 로그가 그 횟수만큼 쌓이게 된다.
그래서 MAX_ATTEMPTS 에서 하나를 뺀 2로 넣어 딱 한 번의 기회만 만들었다.
UPDATE jobs SET attempt_count = 2, updated_at = NOW() WHERE id = ?;
한 번 돌려 보고 또 실패하면 last_log 를 다시 읽는 것이 순서였다.
소프트 삭제와 갱신 시각
이 건의 실패 원인은 충돌한 옵션 행이었고 그것을 정리할 때는 DELETE 하지 않고 deleted_at 을 채우는 방식을 썼다.
UPDATE product_options
SET deleted_at = NOW(), updated_at = NOW()
WHERE id IN (...);
나중에 무슨 일이 있었는지를 되짚을 자료가 필요하기 때문이다.
이때 deleted_at 과 함께 updated_at 도 같이 넣었다. 그 시각이 있어야 나중에 어떤 작업으로 정리된 건인지를 시간으로 묶어 볼 수 있다.
단건 실행의 스코프
다시 돌릴 때는 cron 배치 전체를 부르지 않고 단건 실행 경로로 그 건만 돌렸다.
<단건 실행 명령> --job-id=<id>
배치를 부르면 다른 건까지 함께 도는데 그럴 이유가 없었다.
다만 단건 실행 경로는 배치가 거치는 스코프 설정을 안 거치므로 상태 복원 없이 곧바로 돈다. 그것이 이 경우에는 오히려 맞았지만 그 차이는 알고 써야 했다.
표준 절차의 가변 구간
정리하면 절차는 셋으로 고정된다.
1. 실패 원인 행을 deleted_at 으로 소프트 삭제
2. attempt_count 를 MAX_ATTEMPTS - 1 로 UPDATE
3. 단건 실행 경로로 재실행
1을 빼면 같은 이유로 다시 실패하고 2를 빼면 한도를 넘어 안 돌며 3을 빼면 다음 주기까지 기다리게 된다. 뒤의 둘은 어느 작업이든 같은 명령으로 처리된다.
바뀌는 것은 첫 번째뿐이고 그것을 무엇으로 채울지는 last_log 가 정한다. 절차를 적을 때 고정 구간과 가변 구간을 갈라 두는 것이 다음 사람에게 쓸모가 있었다.
정리
- 시도 횟수가 한도에 닿으면
job이 대상에서 빠진다 - 한도에서 하나를 뺀 값으로 리셋해 한 번의 기회만 만든다
- 0으로 되돌리면 같은 로그가 한도만큼 쌓인다
- 원인을 먼저 없애지 않으면 같은 이유로 다시 실패한다
- 이력은
DELETE하지 않고deleted_at으로 남긴다 deleted_at과updated_at을 함께 넣는다- 단건 실행은 스코프를 안 거쳐 상태 복원 없이 돈다
- 절차의 고정 구간과 가변 구간을 갈라 적는다