증상
락을 도입한 뒤로 작업이 아예 안 돈다. 로그도 안 남고 종료코드 0.
원인
정상 종료 경로에만 rm -f를 뒀다. 프로세스가 죽으면 락이 영구히 남고 이후 모든 회차가 조용히 exit한다.
해법
trap 'rm -f "$LOCK"' EXIT INT TERM HUP
★ 이건 앞 항목의 해법이 만든 다음 함정이다. pgrep 가드 → PID 락 도입 → 락 잔재. 하루에 두 단계로 겪었다.
★★★ 그리고 trap으로도 안 되는 경우가 있다 — 세 번째 단계
동시 실행을 확실히 막으려고 작업에 timeout을 걸었다. timeout은 SIGKILL까지 갈 수 있고, SIGKILL은 trap을 실행하지 않는다. fork 실패로 죽는 경우도 같다.
겹침을 막으려고 넣은 장치가 락 해제를 못 하게 만들었다. 2번의 해법을 3번이 무력화한 것이다.
| 장치 | 막으려던 것 | 만든 다음 함정 | |
|---|---|---|---|
| 1 | pgrep 가드 | 중복 실행 | 자기 자신을 잡음 |
| 2 | PID 락 + trap | 1번 해결 | 죽으면 락 잔재 |
| 3 | timeout 강제 종료 | 겹침 | SIGKILL이 trap을 건너뛰어 또 락 잔재 |
★★★ 방어를 하나 걸 때마다 새 실패 지점이 하나 생기고, 그게 앞 방어의 전제를 깨기도 한다.
해법 — 겹을 늘리되 서로 다른 가정 위에 둔다
# 1겹: 정리 코드를 실행할 기회가 있을 때
trap 'rm -f "$LOCK"' EXIT INT TERM HUP
# 2겹: 그 기회가 없었을 때 (SIGKILL·전원 차단·fork 실패)
[ -f "$LOCK" ] && [ $(( $(date +%s) - $(stat -c %Y "$LOCK") )) -gt 600 ] && rm -f "$LOCK"
★★ 단일 방어는 그 방어가 안 도는 경로에서 무너진다. trap은 "정리할 기회가 있다"를 전제하고, TTL은 그 전제가 깨진 경로를 덮는다.
★ TTL은 작업 timeout보다 넉넉해야 한다. 짧으면 정상 실행 중인 작업을 죽은 것으로 보고 동시 실행 방지가 통째로 무의미해진다.