여러 채널에 순차로 restore:channel 을 돌리는 중이었고 첫 채널이 끝나 다음을 골라야 했다. 기준은 작은 것부터였고 작으면 실패해도 영향이 적기 때문이다.
Table of contents
Open Table of contents
문제 — 제외 조건 없이 센 규모
GROUP BY channel_id 로 채널별 규모를 세어 작은 순서를 뽑았다. 백 건대 채널 셋이 나왔고 이 정도면 시작하기 좋아 보였다.
그런데 이 작업에는 건드리면 안 되는 EXCLUDED_SUPPLIERS 목록이 따로 있었다. 처음 센 쿼리에는 supplier_id NOT IN (...) 조건이 안 들어가 있었다.
원인 — 소형으로 보이던 것의 실체
제외 조건을 넣고 다시 세니 그 세 채널이 각각 0건과 1건과 0건이 됐다. 소형으로 보이던 채널들이 전부 제외 대상 고객의 독점 채널이었던 것이다.
제외를 적용하면 그 세 채널에는 처리할 것이 없다. 작은 채널이 아니라 우리가 건드릴 것이 없는 채널이었고 두 문장은 완전히 다른 뜻이다. NOT IN 조건 하나를 빼먹었을 뿐인데 후보 목록의 앞자리가 통째로 바뀌었다.
주의 — 연속이 아닌 규모
제외를 적용한 뒤 순서를 다시 보니 규모가 연속이 아니었다. 직전에 끝낸 채널이 천 건 남짓이고 다음 후보 둘이 이천 건대인데 그다음이 수만 건대로 뛴다.
조금씩 늘려 가는 방식이 안 된다는 뜻이다. 다음 단계가 십수 배이므로 이천 건대를 끝낸 뒤 바로 수만 건대에 들어가게 된다. 미리 알고 있으면 그 앞에서 준비를 할 수 있고 모르면 규모에 맞는 준비 없이 들어간다.
판단 기준 — 두 지표를 함께 보는 기준
이천 건대 후보 둘의 성격도 달랐다. 두 가지 방법이 있었는데 개수가 더 작은 쪽을 먼저 하는 것과 registered_count 가 0 인 쪽을 먼저 하는 것이다. 뒤쪽을 골랐다. 이유는 등록이 0이면 실패해도 밖에 티가 안 나서다. 대신 그 채널에서는 외부 전송 경로가 검증되지 않는 것을 감수했다. 다른 하나는 사내 독립 배포 서비스로 전송하는 특수 채널이었다.
등록이 0이면 갱신 작업이 구조적으로 발생할 수 없고 외부로 나가는 전송도 없다. 복구 로직은 똑같이 도는데 무엇이 잘못돼도 우리 DB 안에서 끝난다. 실패해도 밖에 티가 안 나는 대상이 검증에는 가장 좋은 대상이었다.
여기서 규칙이 하나 나왔다. 확대 순서는 제외 조건을 적용한 개수와 registered_count 를 함께 보고 정한다. 개수만 보면 위험한 것을 고를 수 있고 registered_count 만 보면 규모를 모른다.
조치 — 병렬 실행 전의 준비
실행을 준비하다가 하나 더 걸렸다. 여러 채널을 병렬로 돌리면 같은 restore_log.sqlite 에 동시 쓰기가 발생하는데 SQLite 는 그것에 약해 database is locked 가 난다.
sqlite3.connect(path, timeout=60.0)
conn.execute('PRAGMA busy_timeout=60000')
SQLite 공식 문서대로 PRAGMA busy_timeout 은 연결마다 잠금 대기 시간을 정하므로 connect(timeout=) 과 둘 다에 넣어야 한다. 이걸 실행 전에 넣은 것이 중요했다. 돌리다가 잠금 오류를 만나면 그 실행분이 날아가고 어디까지 갔는지도 기록에 안 남는다.
정리
- 규모를 셀 때
NOT IN제외 조건을 적용하고 센다 - 안 넣으면 실제 작업 대상과 크게 어긋난다
- 소형으로 보이던 것이 전부 제외 대상 독점일 수 있다
- 작은 채널과 건드릴 것이 없는 채널은 다른 뜻이다
- 규모가 연속이 아니라 뛰면 조금씩 늘리는 방식이 안 된다
- 확대 순서는 개수와 외부 노출 위험을 함께 보고 정한다
- 등록이 0인 대상은 실패해도 밖에 안 나가서 검증에 좋다
- 병렬 실행 전에 SQLite 의
busy_timeout을 넣는다