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