"커널에 박힌 스레드는 timeout 못 걸어. 근데 프로세스는 죽일 수 있어."
중단할 수 없는 hang
Recall 의 executor 는 네트워크로 마운트한 아카이브에서 영상을 읽어. 여기서 얌전한 로컬 디스크만 다뤄본 사람이 놀랄 사실이 하나 있어. 네트워크 파일시스템에서는 파일 open 이 무한정 block 될 수 있어. 느린 게 아니라 무한정이야. 마운트가 응답을 멈추면 open() 을 부른 스레드는 네 Python 코드가 닿을 수 없는 상태로 커널 안에 주차돼. 널 구해줄 timeout 파라미터도 없어. 중단도 못 해. KeyboardInterrupt 도 안 먹혀. 파일시스템이 답하기로 결정할 때까지 스레드는 그냥 사라진 거고, 그건 영영일 수도 있어.
이게 worker 한테 뭘 하는지 따라가 보자. 메인 프로세스가 그 open 에서 hang 돼. heartbeat 가 멈추고, lease 가 만료돼. control plane 은 옳게도 worker 가 죽었다고 판단하고 job 을 다른 worker 한테 넘겨. 그 worker 도 같은 막힌 마운트에 손을 뻗어서 똑같이 hang 되고. 응답 없는 네트워크 경로 하나가 queue 전체를 인질로 잡은 거야. hang 된 프로세스 안에 아무리 영리한 재시도 로직을 넣어도 소용없어. 프로세스 자체가 얼어 있으니까.
중단 못 하면, 죽일 수 있는 데 격리해
답은 구조에 있고, 그게 이 레슨의 교훈이야. 위험한 작업은 별도의 자식 프로세스에서 돌려. 부모는 오디오 proxy 준비, 그러니까 네트워크 파일시스템을 건드리는 부분을 맡을 자식을 spawn 하고 지켜보기만 해. 부모는 계속 응답할 수 있는 상태로 남아. lease 도 계속 갱신하고. 자기가 통제하는 wall-clock timeout 을 강제하되 넉넉하게 잡아. 최소 30분쯤에서 시작해서 source 크기에 따라 늘리고 몇 시간에서 끊는 식으로. 느리긴 해도 제대로 되고 있는 전송이 억울하게 죽지 않게. 그리고 자식이 그 예산을 넘기면 부모가 죽여. 안에 있는 스레드는 중단 못 해도 프로세스는 언제든 죽일 수 있거든. syscall 에서 못 얻은 timeout 을 프로세스 경계가 대신 주는 거야.
이게 깔끔하게 맞아떨어지는 건 위치 덕분이야. 전부 유료 경계 앞쪽이거든. 여기서 난 timeout 은 provider 이전 단계의 실패로 기록돼. 전사 credit 을 한 푼도 안 썼으니까, 재시도해도 안전하다는 게 증명되는 실패 범주(트랙 4 의 safe-only 재시도) 에 들어가. daemon 은 멈추는 대신 다음 durable job 으로 넘어가고. 무한 hang 이 범위가 정해지고, 공짜고, 복구 가능한 실패로 바뀌는 거야.
그리고 느린 게 준비된 작업을 막게 두지 마
같은 본능에서 나온 짝 규칙이 하나 더 있어. executor 는 새 영상을 찾으려고 아카이브를 주기적으로 스캔하기도 하는데, 그 스캔도 느려질 수 있는 같은 파일시스템을 건드리거든. 그래서 daemon 은 이미 있는 durable job queue 를 먼저 비우고 그다음에야 인벤토리를 시도해. 상태가 나빠진 네트워크 마운트가 새 영상 발견을 느리게 만드는 건 어쩔 수 없지만, 이미 등록돼서 돌 준비가 끝난 작업까지 인질로 잡게 두면 안 되니까. 옷만 갈아입은 같은 원리야. 네가 지연을 통제할 수 없는 작업이, 그냥 뒀으면 진행됐을 작업의 critical path 에 앉아 있게 두지 마.