fix(preview): 옛 serve 가 살아남아 낡은 빌드를 서빙하던 문제 - #170
Merged
Conversation
pkill 패턴이 실제 프로세스를 한 번도 잡지 못했다. 컨테이너 안 cmdline 은
npm exec serve -s /workspace/app/dist -l 3000
node /root/.npm/_npx/<hash>/node_modules/.bin/serve -s /workspace/app/dist -l 3000
이라 리터럴 'npx serve' 가 없다. 대신 그 pkill 을 실행하는 sh -c 자신의 cmdline 에는
패턴 문자열이 들어 있어 스스로에게 SIGTERM 을 보냈다.
2026-08-25 컨테이너 실측(node:20-alpine, 운영과 같은 cap-drop/pids/memory):
pkill -f 'npx serve' → exec 종료코드 143, 살아남은 serve 2개
pkill -f '[s]erve -s' → exec 종료코드 0, 남은 serve 0개
옛 serve 가 포트 3000 을 쥐고 있으면 새 serve 는 EADDRINUSE 로 죽지 않고 랜덤 포트로
조용히 옮겨 붙는다("Accepting connections at http://localhost:46113"). 게이트웨이는
3000 만 프록시하므로 옛 serve 가, 즉 그 serve 가 붙들고 있는 옛 디렉터리가 계속
응답한다. 빌드 산출물 경로가 바뀌면 낡은 화면이 그대로 보인다 — 같은 디렉터리면
serve 가 요청마다 디스크를 읽어 최신이 나오므로, 이 증상은 경로가 바뀔 때만 터진다.
이미 들어가 있는 리스닝 확인이 오히려 이것을 가렸다. 옛 serve 가 3000 에서 응답하니
probe 가 통과해 "준비 완료"가 된다.
함께 고친 것 — exec 이 종료 코드를 읽지 않던 문제.
DockerContainerService.exec 은 stdout·stderr 만 모으고 종료 코드를 확인하지 않았다.
그래서 프리뷰 빌드의 set -o pipefail 이 아무 의미가 없었고, 빌드가 깨져도 호출자는
성공으로 알았다. 컨테이너를 재사용하는 프리뷰에서는 이전 빌드의 dist 가 남아 있어
detectBuildOutputDir 이 그것을 잡았고, 실패한 빌드가 옛 화면으로 성공처럼 보였다.
exec 의 의미는 바꾸지 않았다. 호출부가 48곳인데 상당수는 실패해도 되는 명령이라
(없는 파일 cat, 이미 있는 remote 를 git remote add, [ -d ... ] 존재 확인) 전부
예외로 바꾸면 멀쩡한 흐름이 깨진다. 종료 코드를 함께 돌려주는 execWithExitCode 를
따로 두고, 실패가 반드시 전달돼야 하는 빌드에서만 쓴다. 호출자는 이미 예외를 받아
세션을 FAILED 로 닫는다.
Closes #115
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #115
검증부터
이슈 주장을 컨테이너에서 직접 재현했다(2026-08-25,
node:20-alpine, 운영과 같은--cap-drop ALL --pids-limit 256 -m 512m).1. 패턴이 아무것도 안 잡는다 — 실행 중 프로세스의 cmdline 에 리터럴
npx serve가 없다.2. 대신 자기 자신을 죽인다 — pkill 을 실행하는
sh -c의 cmdline 에 패턴 문자열이 들어 있다.3. 그래서 낡은 것이 서빙된다 — 새 serve 는 EADDRINUSE 로 죽지 않고 랜덤 포트로 조용히 옮겨 붙는다.
이슈보다 조건이 좁다
빌드 산출물 경로가 바뀔 때만 터진다. 같은 디렉터리에 덮어쓴 경우에는
serve가 요청마다 디스크를 읽으므로 최신 내용이 나온다(실측 확인).dist→build처럼 경로가 달라질 때 옛 serve 가 옛 디렉터리에 고정돼 낡은 화면이 남는다.그리고 지금은 더 안 보인다
이슈에 적힌 "리스닝 확인 없음"은 그 사이 이미 해소됐다(
startPreviewServer가 포트 3000 을 30초까지 폴링). 그런데 그것이 이 버그를 가린다 — 옛 serve 가 3000 에서 응답하니 probe 가 통과해 "준비 완료"가 된다. 새 serve 가 밀려나도 아무도 눈치채지 못한다.수정
1. pkill 패턴을 실제 cmdline 에 맞춘다
[s]erve는 pkill 자신의 cmdline 에[s]erve -s로 남아 정규식serve -s와 매치되지 않는다. 자기 매치를 피하면서 실제 프로세스는 잡는다.2. 빌드 실패를 호출자에게 전달한다
DockerContainerService.exec이 종료 코드를 읽지 않아set -o pipefail이 무의미했다. 빌드가 깨져도 성공으로 반환됐고, 컨테이너를 재사용하는 프리뷰에서는 이전 빌드의dist가 남아 있어detectBuildOutputDir이 그것을 잡았다 — 실패한 빌드가 옛 화면으로 성공처럼 보였다.exec의 의미는 바꾸지 않았다. 호출부가 48곳인데 상당수는 실패해도 되는 명령이다 — 없는 파일cat, 이미 있는 remote 를git remote add,[ -d ... ] && echo yes || echo no로 존재 확인. 전부 예외로 바꾸면 멀쩡히 돌던 흐름이 깨진다. 그래서 종료 코드를 함께 돌려주는execWithExitCode를 따로 두고 빌드에서만 쓴다. 호출자(ProjectPreviewProvisioner:44-58)는 이미 예외를 받아 세션을FAILED로 닫는다.검증
./gradlew test전체 통과남는 것
execWithExitCode를 쓰는 곳은 지금 빌드 하나다. 다른 호출부도 실패를 전달해야 하는 곳이 있을 수 있지만, 48곳을 한 번에 판단하는 것은 이 PR 범위를 넘는다 — 필요해지는 곳부터 하나씩 옮기면 된다.🤖 Generated with Claude Code