Skip to content

fix(preview): 옛 serve 가 살아남아 낡은 빌드를 서빙하던 문제 - #170

Merged
dldnsgkr merged 1 commit into
developfrom
unhak/preview-stale-serve
Aug 25, 2026
Merged

fix(preview): 옛 serve 가 살아남아 낡은 빌드를 서빙하던 문제#170
dldnsgkr merged 1 commit into
developfrom
unhak/preview-stale-serve

Conversation

@dldnsgkr

Copy link
Copy Markdown
Collaborator

Closes #115

검증부터

이슈 주장을 컨테이너에서 직접 재현했다(2026-08-25, node:20-alpine, 운영과 같은 --cap-drop ALL --pids-limit 256 -m 512m).

1. 패턴이 아무것도 안 잡는다 — 실행 중 프로세스의 cmdline 에 리터럴 npx serve 가 없다.

npm exec serve -s /workspace/app/dist -l 3000
node /root/.npm/_npx/aab42732f01924e5/node_modules/.bin/serve -s /workspace/app/dist -l 3000

2. 대신 자기 자신을 죽인다 — pkill 을 실행하는 sh -c 의 cmdline 에 패턴 문자열이 들어 있다.

pkill -f 'npx serve'   → exec 종료코드 143,  살아남은 serve 2개
pkill -f '[s]erve -s'  → exec 종료코드 0,    남은 serve 0개

3. 그래서 낡은 것이 서빙된다 — 새 serve 는 EADDRINUSE 로 죽지 않고 랜덤 포트로 조용히 옮겨 붙는다.

새 serve 로그:  INFO  Accepting connections at http://localhost:46113
포트 3000 응답: <h1>OLD-DIST</h1>          ← 게이트웨이가 프록시하는 그 포트

이슈보다 조건이 좁다

빌드 산출물 경로가 바뀔 때만 터진다. 같은 디렉터리에 덮어쓴 경우에는 serve 가 요청마다 디스크를 읽으므로 최신 내용이 나온다(실측 확인). distbuild 처럼 경로가 달라질 때 옛 serve 가 옛 디렉터리에 고정돼 낡은 화면이 남는다.

그리고 지금은 더 안 보인다

이슈에 적힌 "리스닝 확인 없음"은 그 사이 이미 해소됐다(startPreviewServer 가 포트 3000 을 30초까지 폴링). 그런데 그것이 이 버그를 가린다 — 옛 serve 가 3000 에서 응답하니 probe 가 통과해 "준비 완료"가 된다. 새 serve 가 밀려나도 아무도 눈치채지 못한다.

수정

1. pkill 패턴을 실제 cmdline 에 맞춘다

dockerService.exec(containerId, "pkill -f '[s]erve -s' 2>/dev/null || true");

[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 로 닫는다.

검증

  • 마이그레이션 없음
  • 테스트 4개 추가. pkill 테스트는 문자열 모양이 아니라 성질을 본다 — 추출한 패턴을 정규식으로 컴파일해 ① 실측 cmdline 두 개를 잡는지 ② 자기 자신은 안 잡는지 확인한다. 옛 패턴으로 되돌리면 둘 다 깨진다
  • 빌드 테스트 3개: 실패가 호출자에게 도달하는지 / 성공이면 그냥 반환하는지 / build 스크립트가 없으면 빌드를 건너뛰는지
  • ./gradlew test 전체 통과

남는 것

execWithExitCode 를 쓰는 곳은 지금 빌드 하나다. 다른 호출부도 실패를 전달해야 하는 곳이 있을 수 있지만, 48곳을 한 번에 판단하는 것은 이 PR 범위를 넘는다 — 필요해지는 곳부터 하나씩 옮기면 된다.

🤖 Generated with Claude Code

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>
@dldnsgkr
dldnsgkr merged commit f83e762 into develop Aug 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant