fix: 큰 blob이 섞인 diff가 45초 뒤 503으로 떨어지던 것을 고친다 - #74
Merged
Merged
Conversation
showBytes가 Bun `$`로 `git show`를 부르고 있었는데, Bun 1.3.x의 `$`는 64KB 파이프 버퍼를 넘는 stdout을 받는 호출이 겹치면 자식이 이미 끝났는데도 promise가 영영 settle하지 않는다. getDiffFiles의 8-way 버스트가 정확히 그 모양이라, 큰 파일이 섞인 diff는 매번 45초 flight 타임아웃 → 503이 되고 클라이언트는 1회 재시도 후 "Failed to load diff."에서 멈췄다(실측: raycast-extensions의 556파일 diff가 매번 503/45s). - showBytes를 Bun.spawn(argv, stdout pipe, stderr ignore)으로 바꾼다. 동작은 `$ … 2>/dev/null` + .nothrow()와 같다 — 종료 코드를 보지 않고 stdout만 읽고, 스폰 자체가 실패하면(cwd 삭제) 둘 다 throw한다. - 트리거 실측: 200KB 파일 12개를 8-way로 읽으면 첫 라운드에 죽고, 60KB는 27,000 스폰에도 멀쩡하다. 외부 프로세스 경합은 필요 없다(예전 진단 정정). 1.3.12·1.3.14 재현, 업스트림은 1.4.0에서 수정. Bun.spawn은 1.3.12에서 수천 번 돌려도 행 0. - 자가 치유 3부품은 그대로 둔다: 64KB를 넘을 수 있는 단발 `$`가 남아 있다 (큰 diff의 name-status, ls-files --others, 지문의 status -uall). 회귀망: diff-large-blob.test.ts — 200KB × 16파일을 3회 연속 읽어 settle과 바이트 동일성을 단언한다. 행업 판별력은 1.3.x에서만 있다(CI의 setup-bun은 버전 미지정이라 최신을 쓰고, 거기선 `$`도 통과). 내용 단언은 버전 무관 (첫 청크만 읽는 뮤테이션 → 200009 기대, 98304 수신으로 실패). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XAe9zh6siLHSR4nPpqm8QK
- singleFlight.ts 머리 주석이 여전히 "외부 프로세스 생성 경합과 겹치면"이라는 기각된 원인을 말하고 있었다. 이 모듈의 존재 이유가 그 행이라, CLAUDE.md만 고치면 두 곳이 서로 다른 원인을 주장한다. - "호출이 겹치면"은 트리거를 너무 좁게 말했다. 완전 순차(limit=1)도 200KB × 32를 14라운드째에 걸었다 — 겹침은 확률만 올린다. 좁게 적어 두면 남은 단발 `$`를 안전하다고 읽고 자가 치유를 걷어낼 근거가 된다. diff.ts 주석·테스트 docblock·CLAUDE.md 세 곳을 같은 서술로 맞춘다. - CLAUDE.md의 남은 `$` 목록에 refs.ts의 for-each-ref와 flight 밖에서 도는 summary.ts의 diff --name-only·ls-files --others를 더하고 예시임을 밝힌다. 주석·문서만 바뀌고 동작 변경은 없다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XAe9zh6siLHSR4nPpqm8QK
- 직전 커밋이 "세 부품이 남은 `$`를 지킨다"며 summary.ts까지 목록에 넣었는데, summary.ts는 flight 밖이라 셋 다 닿지 않는다 — 같은 문단의 "덮지 않는 곳"과 모순이었다. 닿는 범위를 호출별로 가른다: diffFlight 안(①②③), refsFlight 안(①②, ③ 재시도는 /api/diff 전용), flight 밖(없음). - "새 git 호출의 출력이 64KB를 넘을 수 있으면 `$`가 아니라 Bun.spawn"이라는 규칙을 리포에 적는다. 지금까지 개인 메모에만 있어 다른 기여자는 showBytes를 되돌리지 말라는 것만 알 수 있었다. - "덮지 않는 곳"의 /api/blob 서술에 단서를 단다: getFileBytes의 blob 읽기는 이제 showBytes라 이미지 크기로는 걸리지 않고, 남은 `$`는 출력이 작은 merge-base뿐이다. 문서만 바뀐다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XAe9zh6siLHSR4nPpqm8QK
This was referenced Sep 21, 2026
say8425
added a commit
that referenced
this pull request
Sep 21, 2026
* fix: 출력이 64KB를 넘을 수 있는 나머지 git 호출도 Bun.spawn 헬퍼로 읽는다 Bun 1.3.x의 `$`는 64KB를 넘는 stdout을 받는 호출에서 promise가 영영 settle하지 않을 수 있다(업스트림은 1.4.0에서 수정). #74·#77이 `showBytes`와 `summary.ts`를 `gitOutput.ts` 헬퍼로 옮긴 뒤에도 큰 출력을 받는 `$`가 넷 남아 있었다: - 지문의 `status --porcelain -z -uall` — watch가 2초마다 부른다 - `getDiffFiles`의 `diff --name-status` (큰 diff) - `getDiffFiles`의 `ls-files --others` (untracked 켜짐) - `refs.ts`의 `for-each-ref` (원격 브랜치가 많은 리포) 넷 다 `gitText`로 옮긴다. 이로써 서버에 남은 `$`는 `rev-parse`· `merge-base`·`gh pr view`·`worktree list`처럼 출력이 작은 호출뿐이다. 자가 치유 3부품(flight 타임아웃·503·재시도)은 원인을 가리지 않는 안전망이라 그대로 둔다. 회귀망 `git-large-output.test.ts`는 호출처마다 그 함수를 8번 동시에 불러 출력을 겹치게 만든다(동시 호출은 실제로 일어난다 — 선택이 다른 요청, prewarm, watch 폴). Bun 1.3.12에서 수정 전 코드는 4/4 실패, 수정 후 4/4 통과. 호출을 하나씩 되돌리는 뮤테이션으로 판별력을 쟀다: 지문· name-status·ls-files는 각 3/3 잡히지만 for-each-ref는 좁은 구간에서만 확률적으로 멈춰 20라운드로도 5번 중 3번만 잡힌다 — 테스트와 CLAUDE.md에 그대로 적었다. 행업 단언은 1.3.x에서만 갈리므로 CI `test-bun13` 잡에 이 파일을 더한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XAe9zh6siLHSR4nPpqm8QK * fix: 리뷰 반영 — for-each-ref 회귀망을 16-way로, worktree list도 헬퍼로 - for-each-ref 케이스는 8-way에선 되돌린 코드를 20라운드로도 5번 중 3번만 잡았다. 그 케이스만 16-way로 올리자 첫 라운드에 11번 중 11번 잡는다 (1.3.12, macOS — 리뷰어 6/6 + 재현 5/5). 라운드는 20 → 5. - beforeAll에 60초 상한을 준다. 픽스처가 평소 1.2~1.8초인데 부하가 걸린 뮤테이션 실행에서 훅 기본 상한 5초를 넘어 무관한 이유로 실패한 것을 실측했다 — CI 러너에서도 날 수 있는 결함이다. - 테스트 상한을 라운드 수에 맞춰 케이스별로 계산한다. - refs.ts의 worktree list도 gitText로 옮긴다. 출력이 등록된 워크트리 수에 비례하고, 디렉토리가 지워져도 prunable로 등록이 남아 약 300개면 64KB를 넘는다. 다만 죽은 워크트리 400개(110KB)로 `$`를 8·16-way 10라운드씩 8번 돌려도 한 번도 안 멈춰 판별할 모양을 못 찾았으므로 회귀망은 두지 않고 그 사실을 적는다. 크기는 필요조건일 뿐이라는 것도 적는다. - 남은 `$`는 rev-parse·merge-base·gh pr view — 출력 크기가 리포 규모와 무관한 호출뿐이다. - CI 잡 주석의 "10초"(새 파일은 15초), git-output.test.ts docblock의 오해 소지 있는 문장을 고친다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XAe9zh6siLHSR4nPpqm8QK * test: [임시·되돌릴 것] 다섯 호출을 $로 되돌려 test-bun13이 Linux에서 빨개지는지 본다 * Revert "test: [임시·되돌릴 것] 다섯 호출을 $로 되돌려 test-bun13이 Linux에서 빨개지는지 본다" Linux 러너에서 되돌린 코드로 test-bun13이 빨개지는 것을 확인했으므로 되돌린다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XAe9zh6siLHSR4nPpqm8QK --------- 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.
왜
큰 파일이 섞인 diff를 열면 매번 45초 뒤 503이 나고, 클라이언트는 1회 재시도한 뒤
Failed to load diff.에서 멈췄다. 실측:raycast-extensions(91k 파일 리포)의HEAD~50대비 556파일 diff가 3/3503 / 45.01s.그런데 구성 요소는 전부 빨랐다 — merge-base 10ms,
diff --name-status230ms,git show8ms/회. 같은getDiffFiles를 서버 밖에서 한 번 돌리면 449ms에 끝났다. 느린 게 아니라 안 끝나고 있었다.원인은 메모리·CLAUDE.md에 "재현이 어렵다"고 적혀 있던 그 Bun
$never-settle 행이고, 이번에 트리거가 확정됐다:getDiffFiles의 8-wayshowBytes버스트가 정확히 그 모양이다. 예전 진단(7/24)은 헤드리스 Chromium 기동 같은 외부 프로세스 경합이 필요하다고 봤는데 틀렸다 — 큰 blob만 있으면 서버 없이도, 첫 호출에서 죽는다. 작은 파일만 쓰는 픽스처로는 우연히만 밟혀서 재현에 42회 실행이 필요했던 것이다.무엇을
showBytes한 함수만 바꾼다:$→Bun.spawn(argv, { stdout: "pipe", stderr: "ignore" }).동작은
$ … 2>/dev/null+.nothrow()와 같다(둘 다 실측):rev:path→ 둘 다 빈 바이트(종료 코드 무시)daemon-cwd.e2e.ts통과)getDiffFiles·buildFile·payloadCache·etag·지문은 하나도 안 바뀐다.부수 효과:
/api/blob(이미지 카드)의 old 쪽과 head 뷰의 new 쪽도 같은showBytes를 탄다. 이미지는 흔히 64KB를 넘고 이 라우트는 클라이언트 재시도가 없으므로 같은 위험이 거기서도 사라진다. 다만 이미지 경로의 행업은 따로 재현하지 않았다.실측 근거
트리거 격리 (Bun 1.3.12, 8-way,
$):배제한 가설: 부분 클론 지연 페치(blob이 따뜻해도 45초), fd 고갈(31개/한도 1,048,576, 누수 0),
Bun.serve핸들러(서버 없이도 행), 리포 크기. 행 중 자식 0·좀비 0.버전 매트릭스 (
$, 행업 픽스처):→ 1.3.x 전 계열 영향, 업스트림은 1.4.0에서 수정(해당 이슈는 업스트림에 없다).
bunx는 사용자의 Bun을 쓰므로 1.3.x 사용자는 여전히 있다 — 이 머신도 1.3.12다.Bun.spawn: 같은 행업 픽스처 전부에서 1.3.12 기준 clean 30/30 × 3 (수천 번 스폰, 행 0). 속도는$와 같다(556파일 ~730ms).실제 리포에서 (수정 빌드, 한 서버 프로세스에서 base를 바꿔 매번 콜드 버스트): 363
819파일 10회 전부 200. 556파일은60(819파일)이 첫 회만 14초였는데, 부분 클론이 처음 보는 blob을 원격에서 당겨온 1회성 비용이다 — blob이 로컬에 있는 상태로 새 서버에서 다시 재면 1.5초(2회).503 / 45s→200 / 0.59s. HEAD그대로 두는 것
자가 치유 3부품(singleFlight 타임아웃 race + 503/
Retry-After+ 1회 재시도, #54)은 남긴다. 64KB를 넘을 수 있는 단발$가 아직 있다 — 큰 diff의git diff --name-status -z, untracked를 켰을 때의ls-files --others, 지문의status --porcelain -z -uall(watch 폴마다). 완전 순차도 결국 죽으므로 확률은 낮아도 0이 아니다. 이 PR에선 손대지 않았다.문서·주석
CLAUDE.md "Loading…" 자가 치유 항목의 원인 서술과
singleFlight.ts머리 주석을 고쳤다(둘 다 기각된 원인인 외부 경합을 말하고 있었다). 그대로 두면 리포가 틀린 원인을 말하고,showBytes의Bun.spawn을 누군가$로 "정리"해도 막을 근거가 없다. 남은$는 예시로 적되 자가 치유가 어디까지 닿는지 호출별로 갈랐다(diffFlight안 ①②③ /refsFlight안 ①② / flight 밖summary.ts는 없음). 새 git 호출의 출력이 64KB를 넘을 수 있으면Bun.spawn으로 쓴다는 규칙도 CLAUDE.md에 적었다.테스트 플랜
회귀망:
diff-large-blob.test.ts— 200KB × 16파일을getDiffFiles로 3회 연속 읽고, 매번 10초 안에 settle하는지와 old/new 내용이 바이트 단위로 같은지 단언한다.$)getDiffFiles가 10000ms 안에 settle하지 않았다(3/3)Bun.spawn)Expected 200009, Received 98304oven-sh/setup-bun@v2는bun-version이 없어 최신(1.4.x)을 쓰므로,showBytes를$로 되돌려도 CI는 초록이다. 그 한계는 테스트 주석과 CLAUDE.md에 적어 두었다. 내용 단언(마지막 행)은 버전과 무관하게 잡는다.보안:
/api/blob의path와 URL의 base/head가 흘러드는 서브프로세스 인자 전달 방식이 바뀌므로 security review를 돌렸다 — 신규 취약점 0건. 인자는 여전히${rev}:${path}한 덩어리이고 항상verifyBaseRef를 통과한rev로 시작하므로 옵션으로 읽히지 않으며, 셸을 아예 거치지 않아 같거나 더 안전하다. NUL 바이트는 두 방식 모두 인자가 git에 닿기 전에 throw한다.🤖 Generated with Claude Code
https://claude.ai/code/session_01XAe9zh6siLHSR4nPpqm8QK