Watch at Ease세상을 읽고, 일상의 답을 찾다.

기술 · 실용 가이드

Python cannot schedule new futures after shutdown? 먼저 끝난 수명 주기를 증명하세요

Executor 인스턴스 종료, 인터프리터 종료, asyncio 기본 풀 종료를 구분한 뒤 재시도나 교체를 결정합니다.

Python future 제출 거부에서 수명 주기 소유자와 안전한 종료로 이어지는 원본 그림
원본 일러스트: 재시도나 교체 전에 어떤 수명 주기가 끝났는지 증명합니다. Watch at Ease

RuntimeError: cannot schedule new futures after shutdown은 작업자 수를 늘리라는 신호가 아닙니다. 특정 Executor, asyncio 기본 실행기 또는 인터프리터 전체의 수명 주기가 끝난 뒤 제출자가 경계를 넘었다는 증거입니다. 정확한 문구와 전체 submit 스택으로 소유자를 찾습니다.

재시도를 추가하기 전에 증거를 보존하세요. 각 실행기 세대에는 한 소유자만 두고, 새 생산자를 먼저 멈추고, 수락된 작업을 의도대로 완료하거나 취소한 뒤 shutdown을 한 번만 호출합니다. asyncio.run이 루프를 소유하면 기본 실행기 종료도 맡깁니다.

1. 정확한 종료 신호를 분류합니다

전체 트레이스백을 저장하고 after shutdown과 after interpreter shutdown을 구분하세요. submit, map, 스케줄러, 라이브러리 도우미, run_in_executor 중 어디서 왔는지도 기록합니다.

생성, 제출 수락, 종료 시작, 종료 완료, 최초 거부 시 실행기 세대 ID, PID, 스레드 또는 작업 이름과 단조 시간을 기록합니다. 공개된 소유권 계약을 기준으로 판단합니다.

2. 인스턴스 종료 후 재사용을 고칩니다

with 블록을 벗어나면 실행기가 닫힙니다. 장기 객체가 같은 인스턴스를 저장하면 첫 실행만 성공하고 두 번째가 실패합니다. 각 독립 실행마다 새로 만들거나 전체 반복 사용 기간의 소유자로 with를 옮기세요.

각 호출자가 RuntimeError를 잡아 몰래 새 풀을 만들면 세대가 경쟁하고 작업이 중복될 수 있습니다. 소유자가 잠금이나 상태 기계로 현재 세대를 게시하고 draining 이후 제출을 거부한 뒤 교체해야 합니다.

3. 인터프리터 종료와 daemon 경쟁을 고칩니다

interpreter shutdown 문구는 프로세스 종료가 시작되었다는 뜻입니다. main이 반환된 뒤 daemon 컨트롤러, 스케줄러, 콜백이 계속 제출하거나 주 스레드 예외가 처리되지 않을 때 흔합니다.

명시적 중지 신호로 생산자를 멈추고, 새 제출을 막고, 제어 스레드와 futures를 기다리고, 실행기를 닫은 뒤 main을 반환하세요. 필요한 작업의 유일한 소유자를 daemon으로 두지 마세요.

4. asyncio 기본 실행기 소유권을 지킵니다

run_in_executor(None, ...)는 루프의 기본 실행기를 사용합니다. shutdown_default_executor() 뒤에는 다시 제출할 수 없습니다. asyncio.run은 기본 실행기와 루프 종료를 관리하므로 이중 종료하지 않습니다.

수동 루프에서는 실행기 작업을 만드는 서버와 콜백을 먼저 멈추고 관련 작업을 기다린 뒤 shutdown_default_executor()를 await하고 루프를 닫습니다. 다른 스레드의 콜백도 경계 전에 배출합니다.

5. 재시작과 종료 시험으로 증명합니다

연속 두 번 실행, 큐가 있는 종료, 생산자 활성 중 재시작, 주 스레드 실패를 시험하세요. 수락된 작업은 하나의 종결 상태만 가져야 하고 draining 뒤 제출이 없어야 합니다.

대기 작업을 완료할지 취소할지 정책을 정하고 단언하세요. 생산자 중지가 실행기 종료보다 앞선 시간선과 함께 중복 부작용, 고아 스레드, 누락 예외, 늦은 제출이 없음을 남깁니다.

Executor 인스턴스, 인터프리터, asyncio 기본 풀 종료 결정표
원본 일러스트: 정확한 RuntimeError 경로를 소유자별 안전한 수정에 연결합니다. Watch at Ease