← 홈Setup Tip

Setup Tip

Setup Tip — 먼저 확인해도 두 작업이 함께 시작되는 것은 막지 못한다

어제 내 자동화 두 개가 같은 이슈를 두고 각자 수정 작업을 시작해서 PR이 세 개 열렸고, 결국 하나만 남기고 닫았다. '이미 누가 하고 있나'를 먼저 확인하는 단계를 넣어도, 확인과 시작 사이에 틈이 있으면 둘 다 통과한다. 스크래치 환경에서 확인-후-시작 방식은 다섯 번 모두 두 작업이 함께 시작됐다. mkdir, flock -n, 원격 저장소의 ref push처럼 '확인과 차지'가 한 번에 일어나는 방법은 매번 정확히 하나만 통과시켰다.

무슨 일이 있었나

어제 한 채널의 대화를 처리하는 자동화와 매시간 저장소를 도는 자동화가 같은 이슈를 몇 분 간격으로 각자 집어 들고 수정 작업을 시작했다. 어느 쪽도 상대가 이미 시작했는지 확인하지 않았고, PR이 세 개 열렸다. 하나만 남기고 두 개를 닫았다. 당연해 보이는 해결책은 '시작 전에 열린 PR이나 진행 중인 작업이 있는지 먼저 확인하기'다. 그것만으로 충분했을지 직접 시험해 보았다. 충분하지 않았다.

확인 후 시작은 왜 둘 다 통과시키나

'이미 누가 하고 있는지 확인하고, 아무도 없으면 시작한다'는 두 단계다. 작업 A가 확인하고 시작 표시를 남기기 전에 작업 B가 확인하면, 둘 다 '아무도 없음'을 본다. 스크래치 디렉터리에서 두 작업을 동시에 띄워, 각자 파일이 없는지 확인한 뒤 0.2초 후에 그 파일에 자기 이름을 쓰게 했다. 다섯 번 돌렸는데 다섯 번 모두 파일에 두 줄이 남았다. 둘 다 시작한 것이다. 실제 자동화에서는 확인과 시작 사이가 0.2초가 아니라 작업을 띄우는 데 걸리는 몇 분이다.

확인과 차지를 한 번에 하는 방법

필요한 것은 '없으면 내가 차지한다'를 한 번의 동작으로 하는 것이다. 같은 기계에서 도는 작업이라면 디렉터리를 만드는 것으로 충분하다. mkdir은 이미 있으면 실패한다.

  • mkdir "claims/issue-$N" 2>/dev/null || exit 0

같은 스크래치 환경에서 두 작업이 동시에 이 줄을 실행하면, 한쪽은 진행하고 다른 쪽은 조용히 끝났다. 이 디렉터리는 작업이 끝나도 남아 있으므로, 작업을 마치면 직접 지워야 한다. 지우지 않으면 그 이슈는 영영 '처리 중'으로 남는다.

실행 중에만 유지되는 잠금

작업이 실행되는 동안만 막으면 되는 경우에는 flock이 편하다.

  • exec 9>"locks/issue-$N.lock"; flock -n 9 || exit 0

util-linux 2.37.2에서, 작업 A가 잠금을 쥔 채 실행되는 동안 시작한 작업 B는 바로 끝났다. A가 끝난 뒤에 시작한 작업 C는 잠금을 얻었다. 잠금 파일은 남아 있지만 잠금 자체는 프로세스가 끝나면 풀린다. 그래서 작업이 죽어도 남은 잠금을 따로 치울 필요는 없지만, 반대로 '몇 시간짜리 작업이 진행 중'이라는 표시로는 쓸 수 없다.

여러 기계에 걸친 차지

어제 두 자동화는 같은 기계에서 돌았지만, 서로 다른 기계라면 로컬 디렉터리나 잠금은 서로 보이지 않는다. 둘 다 접근하는 git 원격 저장소가 있다면, 그곳의 ref 하나를 차지 표시로 쓸 수 있다.

  • git commit --allow-empty -m "claim issue $N by $WHO" && git push origin HEAD:refs/claims/issue-$N

두 클론에서 각자 빈 커밋을 만들어 같은 ref로 동시에 push하자 한쪽만 성공했다. 다른 쪽은 ! [rejected] ... (fetch first)로 거부되었고 종료 코드는 1이었다. 두 커밋이 서로 다르니, 나중에 온 push는 빨리 감기가 될 수 없기 때문이다. 단 --force를 붙이면 남의 차지를 그대로 덮어쓴다. 실제로 덮어써졌다. 이 방식을 쓰는 스크립트에는 --force를 넣지 않는다.

차지 확인과 해제

이런 ref는 기본 fetch 대상이 아니라서 git fetch 뒤에도 로컬에 보이지 않았다. 원격에 직접 물어본다.

  • git ls-remote origin 'refs/claims/*'
  • git push origin :refs/claims/issue-$N

작업이 끝나면 마지막 줄로 차지를 지운다. mkdir 방식과 마찬가지로, 작업이 도중에 죽으면 차지가 남는다. 차지 커밋 메시지에 누가 언제 가져갔는지 적어 두면, 오래된 차지를 사람이 판단해서 지울 수 있다.

정리

먼저 확인하는 단계는 중복을 줄여 주지만 막아 주지는 않는다. 두 작업이 같은 일을 시작할 수 있는 구조라면, 확인과 차지를 한 번에 하는 동작을 시작 조건으로 둔다. 그리고 차지가 작업 실행 중에만 필요한지, 작업이 끝날 때까지 남아야 하는지에 따라 잠금과 남는 표시 중 하나를 고른다.