From 25c523b72cd5acdc9bbfaa0d49c8ae391624ed84 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 12 Sep 2026 18:47:49 +0900 Subject: [PATCH] =?UTF-8?q?fix(dev):=20=EC=9C=88=EB=8F=84=EC=9A=B0=20?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=EB=93=9C=EA=B0=80=20=EC=98=81=EC=9B=90?= =?UTF-8?q?=ED=9E=88=20=EB=A9=88=EC=B6=94=EB=8D=98=20uvicorn=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EB=A5=BC=20terminate()=20=EB=A1=9C=20=EA=B0=88?= =?UTF-8?q?=EC=95=84=20=EB=81=BC=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uvicorn 0.24 BaseReload.restart() 는 윈도우에서만 옛 워커를 os.kill(worker_pid, CTRL_C_EVENT) 로 내리려 함. CTRL_C_EVENT 는 프로세스 그룹을 지정해 보낼 수 없어(MSDN: 그룹 id 가 0 이 아니면 호출은 성공하지만 신호는 전달되지 않음) 워커에 아무것도 안 감. 바로 다음 join() 이 영원히 안 풀려 옛 워커가 살아서 옛 코드로 계속 응답하고, 새 워커는 태어나지 않으며, 그 뒤 파일 변경은 감지조차 안 됨. 실측(그 자리를 흉내 낸 부모·자식 한 쌍): CTRL_C_EVENT → 8초 안에 안 풀림 (DETACHED · CREATE_NO_WINDOW · +NEWGROUP 모두) terminate() → 0.00초 CTRL_BREAK → 0.00초 (자식을 그룹 우두머리로 띄웠을 때) 고침은 uvicorn 자신의 리눅스 경로를 그대로 씀. DEBUG + win32 개발 기동에서만 갈아 끼우고, uvicorn 이 위쪽에서 고치면 _patch_windows_reload 를 지우면 됨. ⚠ 앞선 두 커밋(빌드 고리·종료 시한)은 이번 멈춤의 원인이 아니었음. 빌드 고리는 실제 문제였고, 종료 시한은 다른 경우를 막는 값이 있어 남김. [boot] 표식이 이 진단을 가능하게 했으므로 함께 남김. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GrXDD23Dvt2sR7q3X6oekp --- main.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/main.py b/main.py index 86454d09..2529aea7 100644 --- a/main.py +++ b/main.py @@ -698,9 +698,52 @@ _RELOAD_SKIP = { } +def _patch_windows_reload() -> None: + """윈도우에서 uvicorn 리로더가 **영원히 멈추는 것**을 고친다 (2026-09-12 실측). + + 무엇이 잘못됐나 + uvicorn 0.24 의 `BaseReload.restart()` 는 윈도우에서만 옛 워커를 + `os.kill(worker_pid, CTRL_C_EVENT)` 로 내리려 한다. 그런데 CTRL_C_EVENT 는 + **프로세스 그룹을 지정해 보낼 수 없다** — MSDN 이 「그룹 id 가 0 이 아니면 호출은 + 성공하지만 신호는 전달되지 않는다」고 못박아 두었다. 워커는 리로더의 평범한 + 자식이라 그룹 우두머리가 아니므로 그 신호는 **아무 데도 가지 않는다.** + 바로 다음 줄 `join()` 이 영원히 안 풀려서 — + · 옛 워커는 살아서 **옛 코드로 요청을 계속 처리**하고 + · 새 워커는 태어나지 않으며 + · 그 뒤의 파일 변경은 **감지조차 안 된다**(리로더가 그 자리에 갇혀 있다) + 「고쳤는데 화면이 안 바뀐다」로 보이는 자리라 그냥 느린 것이 아니다. + + 실측 (그 자리를 그대로 흉내 낸 부모·자식 한 쌍) + CTRL_C_EVENT → **8초 안에 안 풀림** (DETACHED · CREATE_NO_WINDOW · + CREATE_NO_WINDOW+NEWGROUP 셋 모두) · `terminate()` → **0.00초** · + CTRL_BREAK_EVENT(자식을 그룹 우두머리로) → **0.00초** + ⇒ uvicorn 이 **리눅스에서 이미 쓰는** `terminate()` 가 윈도우에서도 즉시 먹는다. + 그래서 여기서는 그 리눅스 경로를 그대로 쓴다(새 방식을 지어내지 않는다). + + ⚠ 개발 기동(`DEBUG`)에서만 갈아 끼운다. uvicorn 이 위쪽에서 고치면 **이 함수를 지우면 + 된다** — 다른 자리는 아무것도 안 건드린다. + """ + from uvicorn._subprocess import get_subprocess + from uvicorn.supervisors.basereload import BaseReload + + def restart(self: BaseReload) -> None: + self.process.terminate() + self.process.join() + self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets) + self.process.start() + + BaseReload.restart = restart + logger.info("[reload] 윈도우 리로더를 terminate() 방식으로 갈아 끼움") + + if __name__ == "__main__": + import sys + import uvicorn + if DEBUG and sys.platform == "win32": + _patch_windows_reload() + logger.info(f"Starting server on {SERVER_HOST}:{SERVER_PORT}") uvicorn.run( "main:app",