dev.to

One `rm -f` Silently Disabled My Crash Detection

`rm -f` 한 줄이 크래시 감지를 조용히 꺼뜨렸습니다

크래시 후 데몬을 재시작해도 기록 공백이 남지 않는 버그의 원인은 복구 로직이 확인하는 PID 파일을 테스트 정리 단계에서 삭제한 것이었습니다. 테스트 하네스가 실행 조건을 재현하고 데몬 실행 여부가 아닌 복구 결과를 검증해야 한다는 교훈을 agentwatch 사례로 설명합니다.

에디터 노트

이 사례에서 테스트가 증명해야 할 대상은 프로세스 재시작이 아니라 복구 기록의 생성입니다. 글은 PID 파일이 사라지면 첫 실행으로 처리하는 설계와, 테스트가 그 파일을 삭제해 복구 경로를 건너뛴 과정을 구체적으로 보여줍니다. FT-14가 공백 기록의 존재와 지속 시간을 확인하도록 고친 점도 문제와 해법이 맞닿아 있습니다. 댓글은 PID 파일을 상태의 진실로 취급할 때 생기는 다른 문제를 제기합니다. `pgrep`으로 PID 파일을 쓰는 방식에는 시작 순서 경쟁이 있을 수 있다는 지적과, 정상 종료 때 파일을 지우지 않으면 다음 시작에서 가짜 공백이 생길 수 있다는 지적입니다. 이는 본문에서 검증한 수정의 효과와 별개로 남는 구현 조건입니다. 파일을 보존하는 것만으로 모든 복구 상태가 올바르게 관리된다고 볼 수는 없으며, 생성과 삭제의 책임 및 종료 방식까지 테스트 경계에 포함해야 합니다.

AI 요약

agentwatch 데몬은 크래시 뒤 재시작하면 마지막 기록 시각과 현재 시각을 비교해 녹화 공백(recording gap)을 기록합니다. 첫 실행과 크래시 복구를 구분하는 신호는 daemon.pid 파일입니다. 런처가 데몬을 시작할 때 이 파일을 쓰고, 데몬이 비정상 종료해도 파일은 남습니다. 다음 실행에서 파일이 있으면 이전에 데몬이 실행 중이었다고 판단합니다.

테스트가 지운 복구 신호

문제는 테스트가 데몬을 강제 종료한 뒤 daemon.pid까지 rm -f로 삭제한 데서 생겼습니다. 테스트는 이 파일을 오래된 상태로 취급했지만, 복구 로직에는 크래시가 있었다는 증거였습니다. 재시작한 데몬은 파일이 없으니 첫 실행이라고 판단하고 함수 초반에 반환했습니다. 예외나 로그는 발생하지 않았고, 기록 공백도 남지 않았습니다.

처음에는 공백 감지 함수가 잘못됐다고 의심했지만, 함수는 입력된 조건에 맞게 동작했습니다. 깨끗한 첫 실행에서 가짜 공백을 만들지 않으려면 PID 파일이 없을 때 반환하는 동작이 필요합니다. 잘못된 입력은 테스트 정리 단계가 만들었습니다. 수정안은 소켓 파일만 지우고 PID 파일은 보존하는 것입니다.

테스트 하네스가 재현해야 하는 조건

테스트는 런처가 아니라 데몬을 직접 실행합니다. 따라서 런처가 맡는 PID 파일 작성 작업도 하네스가 재현해야 합니다. 글에서는 백그라운드 데몬을 실행한 뒤 pgrep으로 PID를 찾아 파일에 기록하는 방법을 제시합니다. pgrep 패턴의 [a]는 pgrep 프로세스 자신이 검색 결과에 포함되는 일을 막습니다. 그렇지 않으면 곧 종료할 pgrep의 PID가 파일에 기록될 수 있습니다.

수정 후 FT-14 테스트는 크래시와 재시작 사이의 시간에 맞는 duration_ms를 가진 recording-gap 이상 기록이 생기는지 확인합니다. agentwatch v0.1.0에 이 수정이 반영됐으며, 테스트는 데몬이 실행됐는지만 보는 대신 복구 결과가 기록됐는지를 검증합니다.

파일 신호와 복구 검증

글은 PID 파일 외에도 마이그레이션 완료 표시 파일, 상태 파일, .env 파일처럼 존재 자체가 상태를 나타내는 파일을 예로 듭니다. 정리 작업이 파일의 역할을 구분하지 않고 삭제하거나 비우면, 이를 읽는 쪽은 오류 없이 기능을 건너뛸 수 있습니다. 따라서 파일을 신호로 쓰는 복구 로직에서는 테스트 정리가 그 신호를 보존해야 하며, 테스트도 복구 동작을 직접 확인해야 한다고 설명합니다.

특히 프로세스가 다시 시작됐다는 사실과 중단 기록이 저장됐다는 사실은 서로 다른 검증 대상입니다. 테스트가 시작 성공만 확인하면 복구 경로가 실행되지 않아도 통과할 수 있습니다. 크래시 복구나 마이그레이션 테스트는 복구 전제인 PID 파일, 마커, 잠금 상태를 준비하고 기대한 상태 변화까지 검사해야 합니다.

dev.to 반응

  • @annavi11arrea1 — 자세히 공유해 주셔서 감사합니다. 크래시 뒤 모든 파일을 "정리"하면 안 되고, 깨끗한 복구의 출발점이 되는 파일도 있다는 설명이 좋았습니다. 놓치기 쉬운 흥미로운 내용입니다. AI에서 새롭고 흥미로운 버그가 많이 생기는 것도 봤습니다. 😂✨️
    • @yuhehe — 두 번째 문단의 일반화가 핵심입니다. 버그 유형은 "검사가 대리 신호를 읽는데, 그 신호를 관리하는 코드는 검사의 존재를 모른다"는 것입니다. rm -f는 PID 파일을 지웠고 복구 로직은 그 파일만을 세계의 전부로 봤습니다. 각 구성 요소가 자기 관점에서는 제대로 동작했으므로 예외가 발생하지 않았습니다. 오늘 아침 저도 같은 유형의 문제를 반대편에서 겪었습니다. 게시 스크립트가 응답 필드 하나를 자체 파싱해 게시물을 확인했는데, 플랫폼 쪽에서 그 필드가 하루 전부터 실제 상태를 반영하지 않아 이미 공개된 게시물을 실패로 표시했습니다. 살아남은 수정은 존재 여부나 상태를 판정하는 기준을 그 기준에 거짓 정보를 줄 이유가 없는 코드가 관리하도록 하는 것이었습니다. 제 경우에는 쿠키 없는 GET 요청이었고, 여기서는 정리 작업이 없애면 F1 감지가 "첫 실행, 채울 내용 없음"으로 조용히 낮아졌다는 사실을 드러내도록 복구 프로브가 센티널을 쓰고 다시 읽는 방법이 있을 것 같습니다. 기록에서 섬뜩한 부분은 "조용히"라는 말이 두 번 나온다는 점입니다. 데몬도 조용히 죽었고 감지도 조용히 사라졌습니다. 이런 조용한 실패가 겹치면 시스템은 기능이 사라진 줄도 모른 채 몇 주씩 돌아갑니다.
  • @innokentyb — "데몬이 재시작됐다"와 "공백이 기록됐다"를 구분한 점이 실제 테스트 경계를 잘 짚었습니다. 첫 번째는 프로세스 복구를 증명하고 두 번째는 기대한 운영 결과를 증명합니다. 마커 파일의 소유권도 명시하고 싶습니다. 누가 만들고 누가 삭제할 수 있는지, 사라지면 어떤 불변 조건이 실패해야 하는지를 정해야 합니다. 각 마커를 제거해 복구 검증기가 이를 잡는지 확인하는 변이 테스트(mutation test)를 고려했나요?
  • @reidmarlow — pgrep 결과를 daemon.pid에 쓰는 서브셸 실행에는 경쟁 조건이 생길 수 있습니다. 백그라운드 데몬은 pgrep이 파일 쓰기를 마치기 전에 실행을 시작할 수 있습니다. 빠른 머신에서는 _record_gap_if_needed()가 self.pid_path.exists()를 먼저 확인할 수 있습니다. Bash 테스트 하네스를 제어한다면 서브셸에서 exec agentwatch-daemon 직전에 $$를 파일에 써서 Python 인터프리터가 모듈을 불러오기 전 PID 파일을 만들 수 있습니다. Linux에서는 정상 종료 후 재시작할 때도 daemon.pid가 문제가 됩니다. SIGKILL 뒤 남은 파일은 원하는 크래시 신호지만, 정상적인 SIGTERM 종료에서는 다음 시작이 가짜 크래시 공백을 만들지 않도록 종료 핸들러가 파일을 직접 지워야 합니다.
  • @zrycsbpuss5d — 이런 조용한 실패는 잠을 설치게 합니다. 프로세스가 실제로 끝났는지 확인하지 않고 PID 파일이나 잠금 파일에 rm -f를 실행하는 일은 흔한 위험 요소입니다. 데몬이 재시작해 잠금이 없다고 보고 첫 실행이라고 판단할 수 있지만, 이전 프로세스가 버퍼를 쓰거나 소켓을 잡고 있을 수 있습니다. 저도 kill -HUP $(cat pidfile) 2>/dev/null || true 뒤에 rm -f pidfile을 실행하는 logrotate의 postrotate 스크립트에서 문제를 겪었습니다. HUP 처리가 연결을 정리하는 데 예상보다 오래 걸렸고 PID 파일은 사라졌습니다. systemd가 서비스를 재시작하면서 로그 파일을 두 프로세스가 함께 쓰게 됐고, 디버깅에 필요할 로그가 손상됐습니다. 저는 이제 PID 파일을 진실이 아니라 신호로 봅니다. 실제 기준은 /proc/<pid>/exe나 TTL이 있는 etcd 또는 Consul의 임대입니다. 파일을 써야 한다면 flock(1)의 비차단 확인 뒤 삭제하거나 최소한 kill -0 $pid를 실행해야 합니다. 크래시 감지기는 파일 대신 프로세스를 감시해야 하며 Linux 5.3 이상에서는 pidfd_open으로 특정 PID를 경쟁 조건 없이 감시할 수 있습니다.
  • @kyisaiah47 — 오래된 파일이 충분히 오래 남아 있으면 PID 재사용 때문에 재시작 테스트가 공백을 다른 프로세스의 것으로 판단할 수도 있나요?

원문: dev.to / 번역·요약: Trawling