Skip to content

페이지 fetch 클라이언트의 라이브러리 자동 재시도를 끈다 - #50

Open
m-a-king wants to merge 1 commit into
mainfrom
fix/disable-fetch-auto-retry
Open

페이지 fetch 클라이언트의 라이브러리 자동 재시도를 끈다#50
m-a-king wants to merge 1 commit into
mainfrom
fix/disable-fetch-auto-retry

Conversation

@m-a-king

Copy link
Copy Markdown
Collaborator

Situation

  • prod 에서 에이블리 상품 등록 1건이 실패했다(2026-08-23). 정적 fetch 한 번이 57초를 먹어 core 의 55초 예산을 통째로 소진했고, 7초 만에 성공한 브라우저 렌더가 도착할 자리가 없었다.
12:26:31  fetch 시작
12:27:04  SocketException → 0ms 뒤 자동 재실행 (exec count 2)   ← 33초
12:27:28  403 FORBIDDEN                                        ← 다시 24초
12:27:29  브라우저로 승격
12:27:45  렌더 성공 (7.2초)  ← 이미 늦었다
12:28:14  core: 502, retry_exhausted → FAILED
  • 그 57초의 절반이 우리가 켠 적 없는 재시도였다. HttpClient5 는 I/O 오류를 기본으로 한 번 더 실행한다(maxRetries=1, 대기 0ms). 아무도 의도하지 않았고 아무도 보고 있지 않았다.

Task

  • 재시도를 방침대로 한 곳(core 작업 큐)에만 남긴다.

Action

PageFetchHttpClientConfig.disableAutomaticRetries() 한 줄.

방침은 이미 코드에 적혀 있었다. application.ymlgemini.retry 주석이다.

max-attempts 는 총 시도 횟수. 기본 1 = 재시도 없음 — URL 파싱 재시도는 호출자의 recover 한 곳으로 모여 있다. 여기서 내부 재시도를 켜면 재시도가 이중으로 겹친다.

Gemini 쪽은 이 방침대로 껐는데 fetch 쪽은 라이브러리 기본값이 살아 있었다. 바로 윗줄 disableRedirectHandling() 과 같은 성격이다 — 우리가 상위에서 관리하는 동작을 라이브러리가 몰래 또 하는 것인데, 리다이렉트는 껐고 재시도는 놓쳤다.

겹치는 값이 아니라 성격이 다르다

core 작업 큐 HttpClient5
기록 attemptCount 가 DB 에 남음 없음
간격 다음 tick (1초) 0ms
상한 MAX_ATTEMPTS=2, 초과 시 FAILED hop 마다 새로 2회
관측 메트릭·로그·트레이스 로그 한 줄
제어 우리 라이브러리 기본값

방금 우리를 끊어낸 호스트를 0ms 만에 다시 두드리는 동작이다.

검증

실제 소켓으로 못 박았다. MockRestServiceServer 는 RestClient 위에 붙어 HttpClient 를 아예 안 타므로 이 설정을 못 잡는다. 연결을 받자마자 RST 로 끊는 로컬 소켓을 두고 도착한 연결 수가 1인지 센다.

고친 줄을 빼면 이 테스트가 실패하는 것도 확인했다(negative control). 357 tests 통과.

Result

  • fetch 의 hop 당 최악 시간이 절반이 된다. 이번 건 기준 57초 → 30초대로, core 의 55초 예산 안에 렌더가 들어올 여지가 생긴다.
  • 재시도가 없어지는 게 아니라 제자리로 돌아간다. 이 실패는 escalatable=true 라 브라우저 승격이 받고, core 의 attempt 2회도 그대로다.
  • 부작용: 간헐적 연결 오류가 바로 fetch 실패로 떨어진다. 지금은 라이브러리가 한 번 더 시도해 넘어가던 것들이다. 위 두 안전망이 받으므로 실질 recall 손실은 작다고 본다.

남은 것

이번 사고의 원인은 셋인데 그중 하나만 고친다.

  • 정적 fetch 에 총 소요시간 상한이 없다. read-timeout=15s 는 전체 마감이 아니라 read 1회 간격(SO_TIMEOUT)이다. 여기에 redirect hop(최대 6회)과 A 레코드 개수가 곱해져 이론 최악이 300초를 넘는다. 벽시계 마감이 필요한데 상한 값은 fetch 성공 latency 분포 실측 후에 정해야 한다 — 지금 정하면 느리지만 결국 성공하던 몰을 자른다.
  • 확정 차단 몰의 정적 fetch 스킵 수단이 없다. headlessFirst요청 계약을 authorized 하나로 정리하고 헤드리스 게이트를 걷어낸다 #46 에서 제거됐고, 삭제 전 주석이 정확히 이 증상을 목적으로 적혀 있었다("plain 이 항상 차단되는 host 의 느린-실패 낭비를 없앤다"). 되살리려면 SSRF 가드 경계를 함께 세워야 해 부작용이 크다. 위 둘을 하고도 낭비가 측정되면 그때 판단.
  • infra contracts/extraction-api.md §4 의 예산표가 코드와 어긋나 있다(hop 상한 3 vs 실제 5, 자동 재시도 미반영). 이 PR 로 재시도 항목은 사실이 되지만 hop 수는 여전히 다르다.

연관 이슈

- HttpClient5 는 I/O 오류를 기본으로 한 번 더 실행한다(maxRetries=1, 대기 0ms). 우리가 켠 적 없고 아무도 보고 있지 않던 재시도다
- 방침은 이미 코드에 적혀 있었다. application.yml 의 gemini.retry 주석이 "URL 파싱 재시도는 호출자의 recover 한 곳으로 모여 있다. 여기서 내부 재시도를 켜면 재시도가 이중으로 겹친다" 고 못박고 Gemini 쪽은 max-attempts=1 로 껐는데, fetch 쪽은 라이브러리 기본값이 살아 있었다
- 바로 윗줄 disableRedirectHandling() 과 같은 성격이다. 우리가 상위에서 관리하는 동작을 라이브러리가 몰래 또 하는 것인데, 리다이렉트는 껐고 재시도는 놓쳤다
- 성격이 다르다는 게 핵심이다. core 큐 재시도는 attempt 가 DB 에 남고 상한(MAX_ATTEMPTS=2)·메트릭·트레이스가 붙는데, 이쪽은 기록도 간격도 없이 방금 우리를 끊어낸 호스트를 즉시 다시 두드린다

prod 사고(2026-08-23)에서 드러났다. 에이블리 등록 1건이 실패했는데, 정적 fetch 한 번이 57초를 먹어 core 의 55초 예산을 통째로 소진했고 7초 만에 성공한 브라우저 렌더가 도착할 자리가 없었다. 그 57초의 절반이 이 재시도였다(12:27:04 SocketException -> 0ms 뒤 재실행 -> 24초 더 쓰고 403).

검증: 실제 소켓으로 못 박는 테스트를 넣었다. MockRestServiceServer 는 RestClient 위에 붙어 HttpClient 를 아예 안 타므로 이 설정을 못 잡는다. 연결을 받자마자 RST 로 끊는 로컬 소켓에 붙여 도착한 연결 수가 1인지 센다. 고친 줄을 빼면 이 테스트가 실패하는 것도 확인했다(negative control).
@m-a-king m-a-king added the fix 외부 가시적 결함 수정 label Aug 23, 2026
@m-a-king m-a-king self-assigned this Aug 23, 2026
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c3d4ffff-4fe3-4816-8088-ac692bac85e9


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix 외부 가시적 결함 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant