MySQL 에서 1,000건씩 나눠 처리하는 배치를 돌리는 중이었는데 한 웨이브의 사전 점검 SELECT 에서 8건이 이미 해제 상태로 걸렸다. 이미 해제된 건은 자연스럽게 제외되는 것 아니냐는 반문을 받았고 그럴 것 같았지만 확인했다.
Table of contents
Open Table of contents
원인 — 멱등하지 않은 처리
처리 함수를 다시 읽어 보니 이미 처리된 건인지 확인하는 부분이 없었다.
public function forceDisconnectV2($product)
{
$live = $product->distributionProducts()->whereNull('deleted_at')->first();
$live->delete(); // 소프트 삭제
DistributionProduct::create([...]); // 새로 하나 만듦
}
whereNull('deleted_at') 로 살아 있는 행을 하나 집어 delete() 하고 create() 로 새로 하나 만든다.
이미 해제된 것에 또 돌리면 해제 처리로 만들어진 행이 다시 삭제 표시되고 그 위에 또 하나가 생긴다. 복제본의 복제본이 쌓이는 것이라 무해하지 않았고 여러 번 돌려도 결과가 같아지는 종류가 아니었다. 그런데 나는 무해할 것이라고 근거 없이 짐작하고 있었다. 그 반문을 안 받았으면 확인 없이 그 짐작 위에서 그대로 진행했을 것이다.
명시적으로 빼야 했다
재실행이 무해하지 않은 이상 그 8건은 이번 대상에서 빼야 했다. create() 를 막을 UNIQUE 제약도 ON DUPLICATE KEY UPDATE 도 없어서 우리가 목록에서 직접 빼는 수밖에 없다.
빼기 전에 그 건들이 정말 정상적으로 처리된 것인지도 확인해야 했다. 처리된 것처럼 보이지만 실제로는 중간에 끊긴 것일 수도 있기 때문이다. 그래서 정상적으로 처리된 건이 어떤 모양을 갖는지를 먼저 정의했다.
정상 처리의 시그니처
정상으로 끝난 건 몇 개를 SELECT 로 꺼내서 어떤 컬럼이 어떤 값을 갖는지 정리했고 그것이 정상 처리의 시그니처가 됐다.
걸린 건들을 그 시그니처와 대조하니 대부분이 일치했지만 하나가 안 맞았다. 그 하나는 중간에 끊긴 상태였으므로 제외가 아니라 별도 조사 대상이었다. 시그니처를 안 정의했으면 그것도 함께 제외하고 넘어갔을 것이다.
사유로 태깅해서 제외
제외한 건들은 추적 표의 사유 컬럼을 UPDATE 해 표시했는데 그러면 다음 회차 SELECT 에서도 자동으로 빠지고 왜 빠졌는지도 남는다.
목록에서 그냥 지우면 다음에 다시 대상으로 잡히거나 왜 빠졌는지 알 수 없다. 제외라는 결정을 기록으로 남기는 것이 그 결정을 유효하게 만들었다. 나중에 그 건들을 다시 들여다볼 때도 적어 둔 그 사유가 출발점이 된다.
대상 수를 다시 안 채웠다
제외한 만큼 다른 건을 채워 넣어 천 건을 맞출지도 고민했다. 그렇게 채우면 사전에 점검한 목록과 실제 처리 대상이 서로 달라진다.
점검한 것과 처리한 것이 같아야 결과를 대조할 수 있으므로 채우지 않고 이 회차는 그만큼 적게 처리하고 넘어갔다. 배치를 설계할 때 멱등한지를 먼저 묻는 것이 이 일에서 얻은 순서였다.
정리
- 처리가 멱등하지 않으면 재실행이 무해하지 않다
- 무해할 것이라는 짐작 위에서 진행하지 않는다
UNIQUE제약이 없으면 이미 처리된 것이 안 걸러진다- 배치 대상에서 명시적으로 제외한다
- 제외 전에 정상 처리의 시그니처를 정의해 확인한다
- 시그니처가 안 맞으면 별도 조사 대상이다
- 제외를 사유와 함께 기록으로 남긴다
- 점검한 대상과 처리한 대상을 일치시킨다