Skip to main content

a-timeout-names-the-waiter-not-the-cause-read-the-other-end

타임아웃은 기다린 쪽의 경험이지 사건의 원인이 아니다. 보고 홉(worker→backend, client→API, job→queue)이 보내는 쪽에서 응답을 안 보고 받는 쪽에서 거절을 200 으로 돌려주면, 실패한 쓰기가 성공 로그로 세탁된다 — 그리고 한쪽 끝만 읽어 만든 진단표에는 진실이 들어갈 칸이 아예 없다. 두 끝을 같은 타임스탬프로 나란히 읽어라: 모순 자체가 진단이다. 트리거 - '왜 자꾸 타임아웃이지', 재현 안 되는 간헐 행(hang), 같은 증상을 두 번 이상 오진한 이력, 종결되지 않고 쌓이는 in-flight 행.

Aller à l'installation

Informations de source

Dépôt
blas1n/claude-skills
Dernière activité de la source
11 septembre 2026 à 11:28
Langue détectée de SKILL.md
coréen
Étoiles
2
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
a-timeout-names-the-waiter-not-the-cause-read-the-other-end
description
타임아웃은 기다린 쪽의 경험이지 사건의 원인이 아니다. 보고 홉(worker→backend, client→API, job→queue)이 보내는 쪽에서 응답을 안 보고 받는 쪽에서 거절을 200 으로 돌려주면, 실패한 쓰기가 성공 로그로 세탁된다 — 그리고 한쪽 끝만 읽어 만든 진단표에는 진실이 들어갈 칸이 아예 없다. 두 끝을 같은 타임스탬프로 나란히 읽어라: 모순 자체가 진단이다. 트리거 - '왜 자꾸 타임아웃이지', 재현 안 되는 간헐 행(hang), 같은 증상을 두 번 이상 오진한 이력, 종결되지 않고 쌓이는 in-flight 행.
version
1.0.0
task_types
["debugging","review","ops"]
triggers
[{"pattern":"await/poll 이 timeout 을 올렸는데 원인이 '상대가 응답 안 함' 으로만 적힐 때"},{"pattern":"in-flight/dispatched/pending 상태 행이 종결 없이 쌓여 있을 때"},{"pattern":"같은 증상으로 계측을 추가했는데 그 계측이 침묵한 이력이 있을 때"},{"pattern":"보내는 쪽 로그는 success, 받는 쪽 상태는 미완료일 때"}]
category
trap
# 타임아웃은 기다린 쪽의 경험이다 — 반대쪽 끝을 읽어라 ## Problem - **증상**: 오케스트레이터가 `TimeoutError` 를 올린다. 작업은 "상대가 응답하지 않음"으로 기록되고, 상태 행은 `dispatched` / `in_flight` 로 영원히 남는다. 드물게(월 몇 건) 터지고 재현이 안 된다. - **근본 원인**: 상대는 **5초 만에 끝냈다.** 결과 보고 POST 가 거절당했고, - 보내는 쪽은 `raise_for_status()` 없이 응답을 버렸으며 바로 다음 줄에 `success=True` 를 찍었고, - 받는 쪽은 보안상(프로빙 방지) **거절도 200** 으로 돌려줬다. 실패한 쓰기가 양쪽 모두에서 성공으로 보인다. - **흔한 오해**: "타임아웃 = 상대가 느리거나 죽었다". 그래서 계측을 **기다리는 쪽에** 더 붙이고, 기다리는 쪽이 관측한 것들로 진단표를 만든다. 그 표는 아무리 정교해져도 **반대쪽에서만 보이는 칸**을 가질 수 없다. 실제 사례에서 `TaskTimeout` 독스트링은 네 칸짜리 표를 갖고 있었다: ``` * polls 가 만점 + last_status 가 'dispatched' → 워커가 보고를 안 했다 ← 틀렸다 * polls 가 만점 + last_status 가 None → 행이 안 보인다 * polls 가 바닥 → 대기 루프가 굶었다 * last_status 가 terminal → 읽기가 다 놓쳤다 ``` 두 번(#821·#828) 조사했고 두 번 다 첫 칸으로 읽었다. 첫 칸의 문장이 **"워커가 보고를 안 했다"** 인데, 진실은 **"워커는 보고했고 그 보고가 안 착지했다"** 였다. 두 문장은 기다리는 쪽에서 **완전히 동일하게 보인다.** ## Solution 1. **두 끝을 같은 타임스탬프로 나란히 놓아라.** 한쪽만으로는 어느 문장도 모순이 아니다. 나란히 놓는 순간 증명이 된다. | 시각 | 보내는 쪽 로그 | 받는 쪽 DB 행 | |---|---|---| | 16:33:15 | `task_received` | `status='dispatched'` | | 16:33:20 | `task_completed success=True` | `status='dispatched'` ← **모순** | | 16:36:15 | — | 여전히 `dispatched`, 대기자는 180s 타임아웃 | 2. **보고 홉의 응답을 검사하는지 세라.** 같은 파일의 *다른* 호출들과 비교하는 게 가장 빠르다 — 보통 `register` / `poll` 은 `raise_for_status()` 를 부르고 **결과 보고만 안 부른다.** ```python async def _post_result(client, headers, *, payload) -> None: res = await client.post(_RESULT_PATH, headers=headers, json=payload) if res.is_success: return logger.error("result_post_rejected", task_id=payload.get("task_id"), status_code=res.status_code, body=res.text[:500]) res.raise_for_status() # 시끄러운 게 목적이다 ``` 호출부가 이미 `except Exception` 으로 루프를 지키고 있는지 확인하면 raise 가 안전하다. 3. **상태를 종결하는 코드가 있는지 `.status = ` 로 전수 조사하라.** "reaper 가 있겠지"를 믿지 마라. 실제로 백엔드 전체에 쓰기가 **정확히 둘**이었다(진입 · 정상 종료). 즉 **타임아웃 경로에는 종결이 없었다** — 대기자는 포기하는데 행은 영원히 in-flight 다. 4. 종결을 넣을 땐 **조건부 UPDATE** 로. 레이스 창에서 진짜 결과가 착지했을 수 있다. ```sql UPDATE tasks SET status='failed', error_message=:why WHERE id=:id AND status='dispatched' ``` `rowcount` 가 "내가 닫았다"의 유일한 증거다. 5. **받는 쪽의 200-on-refusal 은 함부로 깨지 마라.** 그게 의도(프로빙 방지)일 수 있고, 보통 독스트링이 변호한다. 대신 **거절을 서버 쪽 로그로** 남기고, 보내는 쪽은 전송/서버 오류(401·422·5xx)만 잡게 하라. 그러면 모든 실패가 **최소한 한쪽에서는** 이름을 갖는다. ## Key Insights - **타임아웃은 원인이 아니라 관측이다.** `deriver_error: TimeoutError` 는 "LLM 이 느렸다"로 읽히지만 실제로는 "내가 기다리다 지쳤다"만 말한다. 원인은 **반대쪽에** 있다. - **한쪽 끝에서 만든 진단표는 계속 정교해지면서 계속 틀린다.** 표의 칸을 늘리기 전에 **표가 읽고 있는 축이 하나뿐인지** 물어라. 두 번 오진한 이력이 있으면 거의 확실히 그렇다. - **실패한 쓰기는 실패한 읽기보다 조용하다.** 읽기 실패를 `[]` 로 접는 결함은 값이 비어서 티라도 나지만, 쓰기 실패는 **보낸 쪽에 성공 로그를 남기므로** 증거가 적극적으로 반대를 가리킨다. (자매편: `a-failed-read-degraded-to-empty-becomes-a-measurement`) - **드묾은 무해함이 아니다.** 4개월간 147건이면 "월 1~2건"이지만, 각각은 런 하나가 풀 타임아웃을 태우고 틀린 원인을 기록한 사건이다. 그리고 그 토큰은 **어디에도 계상되지 않는다** — 과금/쿼터를 설계 중이라면 이건 조용한 미계상 채널이다. ## Red Flags - 기다리는 쪽 예외 메시지에 원인이 "상대가 응답 안 함"으로만 적혀 있다. - 같은 증상으로 **전에도 계측을 추가했는데 그 계측이 침묵했다**는 기록이 있다. (침묵은 "문제 없음"이 아니라 **가설이 틀렸다**는 뜻이다.) - 한 파일 안에서 어떤 호출은 `raise_for_status()` 를 부르고 어떤 호출은 안 부른다. - `pending` / `dispatched` / `in_flight` 상태 행이 **오래된 것부터 쌓여** 있다. - 상태 전이 코드를 전수 조사하면 **진입은 있는데 타임아웃 종결이 없다.** - 받는 쪽 라우트가 거절 경로에서도 `200` 을 돌려준다 (보안 의도일 수 있음 — 깨지 말고 로그를 확인).
Voir sur GitHub