"손으로 돌리면 돼. 서비스 매니저가 돌리면 hang 돼. 같은 경로, 같은 코드, 같은 기계."
재현을 거부하는 버그
진짜 사람 미치게 하는 게 여기 있어. Recall 의 코드는 fleet 이 관례로 쓰는 경로에 있는데, 어떤 기계에서는 그 경로를 파일 동기화 provider 가 받치고 있어. 파일을 '클라우드에' 두고 필요할 때 materialize 하는 종류 말이야. SSH 로 들어가서 손으로 코드를 돌리면 전부 완벽하게 돌아. 그런데 서비스 매니저가 같은 코드를, 같은 경로에서, 같은 기계에서 시작하면 백그라운드 프로세스가 hang 돼. 때로는 의미 있는 줄 하나 실행하기도 전에, 자기 작업 디렉터리를 해석하다가.
여기선 디버깅 본능이 전부 무너져. 본능은 일단 재현해보라고 하는데 재현이 안 되거든. 네 인터랙티브 셸은 동기화 provider 를 밀어붙여서 필요한 파일을 materialize 시킬 수 있어. 반면 다른 세션 컨텍스트에서 서비스 매니저가 띄운 프로세스는 영영 materialize 되지 않는 파일을 기다리며 그냥 앉아 있을 수 있고. 코드는 멀쩡해. 경로가 '같다' 는 건 문자열이 똑같다는 뜻일 뿐이고. 그 문자열을 해석하는 컨텍스트는 전혀 같지 않아.
고침: 편집하는 곳과 도는 곳을 분리해
프로그램 안에서는 못 고쳐. 프로그램이 돌 기회조차 못 얻으니까. 답은 구조에 있어. 서비스를 동기화 경로에서 아예 돌리지 마. Recall 의 서비스 컨트롤러는 평범한 로컬 application-support 디렉터리에 runtime snapshot 을 atomic 하게 만들어. git 메타데이터, 캐시, 의존성 폴더, 빌드 출력은 빼고 런타임 환경 파일은 넣어서. 그다음 서비스 매니저가 그 snapshot 에서, 동기화 provider 가 끼어들 데 없는 평범한 로컬 경로에서 프로세스를 시작해. 멈춤이라는 문제가 설계 단계에서 사라지는 거지.
여기서 외워둘 만한 건 이걸 안전하게 유지하는 규율이야. snapshot 은 두 번째 repository 가 아냐. 생성되는 거고, 버려도 되는 거고, 절대 편집하면 안 되는 거야. 코드의 유일한 진실은 관례적인 checkout 과 git 이고. repo 에서 편집하고, git 으로 push 하고, 배포할 때 snapshot 을 다시 만들어. 누가 snapshot 에서 '빨리 뭐 좀 고치는' 순간, 진실이 갈라지고 어떤 commit 으로도 설명 안 되는 동작을 하는 기계가 생겨. 이 아키텍처 전체가 피하려고 지어진 유령이 정확히 그거고.
동반 디테일: 멈추기 전에 빌드해
같은 명령 안에 어렵게 얻은 순서가 하나 더 들어 있어. control plane 호스트에서 restart 는 지금 도는 프로세스를 멈추기 전에 프로덕션 프론트엔드를 빌드해. 사소한 순서 선택처럼 보이지만, 빌드 실패가 아무 일도 아닌 것과 빌드 실패가 장애가 되는 것의 차이야. 먼저 빌드하면, 빌드가 깨져도 멀쩡한 옛 서비스가 계속 서빙해. 먼저 멈추면, 빌드가 깨졌을 때 아무것도 안 도는 상태에서 압박 속에 깨진 트리를 고쳐야 해. 돌아가는 걸 내리기 전에, 실패할 수 있는 걸 먼저 해.