외부에서 영구 중단된 항목 천여 건을 정리해야 했다. 정식 경로는 외부 호출이 성공해야 후처리가 도는데 이미 없는 것들이라 호출이 실패한다.
Table of contents
Open Table of contents
상황 — 정식 경로를 못 썼다
정식 경로는 외부에 중지를 보내고 그 호출이 성공하면 우리 쪽 status 와 이력이 함께 정리되는 순서였다. 호출이 실패하면 뒤쪽이 아예 안 돈다.
이번 건들은 외부에 이미 없으므로 그 호출이 성공할 수 없고 정식 경로를 태우면 영영 정리가 안 되는 상태였다.
그래서 MySQL 에서 직접 고치는 수동 정리로 가기로 했다.
조치 — 삭제 시각을 채웠다
distribution_products 의 연동 레코드를 소프트 삭제했다.
UPDATE distribution_products SET deleted_at = NOW() WHERE id IN (...);
목록 화면의 조건이 deleted_at 을 보고 있으니 화면에서는 사라진다.
여기서 끝난 줄 알았는데 화면에서 안 보이는 것을 정리가 끝난 것으로 읽은 셈이다.
지적 — 상태와 이력
첫 지적은 status 쪽이었다.
상태도 중지 성공 상태로 해 놔야 하지 않아.
status 를 확인해 보니 그대로였다.
SELECT status FROM distribution_products WHERE id IN (...);
-- update_success
정식 경로를 탔으면 중지 성공이 됐어야 하는데 update_success 로 남아 있었다.
두 번째 지적은 stop 이력 쪽이었다.
중지 작업 이력을 완료 상태로 만들어 놔야 하는 거 아닐까.
정식 경로였으면 stop 작업이 생기고 완료됐을 텐데 이력이 아예 없었다.
두 지적이 status 와 이력이라는 서로 다른 층을 가리키고 있었고 둘 다 맞았다.
원인 — 왜 셋이 다 필요한가
정리하면 deleted_at 과 status 와 이력 셋을 맞춰야 했다.
① 삭제 시각 설정 ← 했다
② 상태를 중지 성공으로 ← 안 했다
③ 중지 작업 이력 생성 ← 안 했다
status 를 안 바꾸면 상태 기반 집계와 재처리가 어긋난다.
update_success 인 것들을 다시 확인하는 처리에 삭제된 것까지 걸리고 화면에서는 안 보이는데 그 조건에는 계속 잡힌다.
stop 이력이 없으면 왜 빠졌는지 추적이 안 되고 몇 달 뒤에 물으면 아무 기록이 없다.
셋이 각각 화면과 집계와 추적을 나눠 담당하고 있어서 하나만 하고 끝내면 나머지 층에서만 어긋난 채로 남는다.
내가 놓친 이유는 삭제하면 되는 것으로 봤기 때문인데 목적은 삭제가 아니라 정식 경로가 만드는 상태의 재현이었다.
변경 내용 — 완료 상태로 만든 작업
stop 이력을 만들려면 각 필드를 어떻게 채울지 알아야 했다.
SELECT * FROM distribution_product_jobs
WHERE job = 'stop' AND completed_at IS NOT NULL LIMIT 5;
completed_at 이 채워진 표본을 꺼내 필드 패턴을 역으로 확인했다.
job 'stop'
fail_status NULL
attempt_count 0
executed_at (시각)
completed_at (시각)
event NULL
last_log (결과 메시지)
여기서 판단이 하나 있었는데 그 행을 대기로 만들지 완료로 만들지였다.
대기로 두면 스케줄러가 집어서 실제로 외부를 부르는데 이미 없는 것이라 그 호출은 반드시 실패한다. 그래서 처음부터 completed_at 을 채워 만들었다.
INSERT INTO distribution_product_jobs
(product_id, channel, job, fail_status, attempt_count,
executed_at, completed_at, event, last_log)
VALUES
(?, ?, 'stop', NULL, 0, NOW(), NOW(), NULL, '수동 처리: 외부 영구중단 확인');
last_log 에 수동 처리 사유를 남긴 것이 나중에 자동 처리와 구분해 준다.
조치 — 한 묶음으로 취급
세 문장을 따로 실행하면 중간에 끊겼을 때 일부만 반영된다.
BEGIN;
UPDATE ... SET deleted_at = NOW(), status = 'stop_success' WHERE ...;
INSERT INTO ... (...) VALUES (...);
COMMIT;
BEGIN 과 COMMIT 으로 묶으니 전부 되거나 전부 안 된다.
일부만 반영된 상태는 정리하기 전보다 다루기 어렵다. 어디까지 됐는지 다시 가려내야 하고 그 가려내는 일이 원래 작업보다 커진다.
이것을 정리 스크립트의 규칙으로 적어 둬서 앞으로 비슷한 수동 정리를 할 때 셋을 다 하는지 확인하게 된다.
이 작업의 성질은 정식 경로가 하는 일을 MySQL 에서 재현하는 것이다. 그러려면 정식 경로의 코드를 읽고 무엇을 바꾸는지 목록으로 만든 뒤 그 목록을 다 재현해야 한다.
정리
- 수동 정리는 삭제 시각만 채우면 반쪽이 된다
- 화면에서 안 보이는 것을 정리가 끝난 것으로 읽기 쉽다
- 삭제 시각과 상태와 작업 이력 셋을 함께 맞춘다
- 상태가 안 바뀌면 집계와 재처리가 어긋난다
- 이력이 없으면 왜 빠졌는지 추적이 안 된다
- 셋이 각각 화면과 집계와 추적을 나눠 담당한다
- 목적은 삭제가 아니라 정식 경로가 만드는 상태의 재현이다
- 필드 패턴은 정상 완료 표본에서 역으로 확인한다
- 외부에 이미 없으면 작업을 대기가 아니라 완료로 만든다
- 로그에 수동 처리 사유를 남겨 자동과 구분한다
- 셋을 한 트랜잭션으로 묶는다
- 정식 경로가 무엇을 바꾸는지 목록으로 만들고 다 재현한다