윈도우즈에서 사용할만한 EventLoop는 ProactorEventLoop 이 유일하다. (SelectorEventLoop은 1024개 이상의 소켓을 만들 수 없다.) ProactorEventLoop은 내부적으로 윈도우즈의 IOCP 를 사용한다. sock_accept 메소드가 IOCP의 AcceptEx를 호출할 때 미처 처리하지 못한 사례가 있다. sock_accept 구현상, 그 사례에서 발생하는 예외는 호출자가 처리하지 못한다. 결국 loop의 exception_handler가 불린다.
미처 처리하지 못한 사례란 다음과 같다.
요약하자면, 서버의 AcceptEx 가 완료되기 전에 backlog 에만 연결되어있던 클라이언트가 강제종료가 되면 ERROR_NETNAME_DELETED(OSError 64)가 발생한다는 것이다. 뭐 OSError Exception이 발생한다면 except 해주면 되잖아? 대략 아래와 같은 식으로 하면 잡히겠지 했다.
@asyncio.coroutine
def accept(loop, s):
while True:
try:
yield from loop.sock_accept(s)
except asyncio.CancelledError:
logging.info("on socket accept failed - cancelled error")
except OSError:
logging.info("on socket accept failed - os error ")
else:
logging.info("on socket accept")
해당 상황에서 on socket accept failed – cancelled error 메시지가 나오긴 한다. 그럼 잡힌것 아닌가? 그런데 아래와 같은 출력도 함께 발생한다.
ERROR:asyncio:Task exception was never retrieved
future: exception=OSError(22, '지정된 네트워크 이름을 더 이상 사용할 수 없습니다', None, 64, None)>
Traceback (most recent call last):
File "d:\python342\Lib\asyncio\tasks.py", line 236, in _step
result = next(coro)
File "d:\python342\Lib\asyncio\windows_events.py", line 363, in accept_coro
yield from future
File "d:\python342\Lib\asyncio\futures.py", line 390, in __iter__
return self.result() # May raise too.
File "d:\python342\Lib\asyncio\futures.py", line 277, in result
raise self._exception
File "d:\python342\Lib\asyncio\windows_events.py", line 560, in _poll
value = callback(transferred, key, ov)
File "d:\python342\Lib\asyncio\windows_events.py", line 351, in finish_accept
ov.getresult()
OSError: [WinError 64] 지정된 네트워크 이름을 더 이상 사용할 수 없습니다
그래서 ProactorEventLoop 소스를 까봤다.
# windows_events.py
def accept(self, listener):
self._register_with_iocp(listener)
conn = self._get_accept_socket(listener.family)
ov = _overlapped.Overlapped(NULL)
ov.AcceptEx(listener.fileno(), conn.fileno())
def finish_accept(trans, key, ov):
ov.getresult()
# Use SO_UPDATE_ACCEPT_CONTEXT so getsockname() etc work.
buf = struct.pack('@P', listener.fileno())
conn.setsockopt(socket.SOL_SOCKET,
_overlapped.SO_UPDATE_ACCEPT_CONTEXT, buf)
conn.settimeout(listener.gettimeout())
return conn, conn.getpeername()
@coroutine
def accept_coro(future, conn):
# Coroutine closing the accept socket if the future is cancelled
try:
yield from future
except futures.CancelledError:
conn.close()
raise
future = self._register(ov, listener, finish_accept)
coro = accept_coro(future, conn)
tasks.async(coro, loop=self._loop)
return future
1. 8번째 줄 finish_accept 함수에서 ERROR_NETNAME_DELETED(OSError 64) exception이 발생한다.
2. 이 exception은 이미 생성한 23번 째 줄 future 객체의 exception으로 등록된다.(windows_events.py의 560번째줄 참고)
3. accept_coro 라는 coroutine 에서도 그 future 객체를 yield from 하고 있으니 exception이 발생한다.
4. accept_coro 에서는 OSError 에 대한 except 처리를 안하고 있으니 25번째 줄 코드로 생성된 task(coro task라고 부르자)에 exception으로 등록된다.
5. coro task 가 삭제될 때 exception이 따로 처리 되지 않으면 eventloop은 exception_handler를 호출한다.
exception_handler가 호출되면 안되나? 나는 exception_handler 가 호출되면 서버가 종료되도록 설계했다. exception_handler를 최후의 수단이라고 생각하며, 이 한계 내에서 처리가 되어야 한다.
이 문제를 근본적으로 해결하려면 accept_co coroutine 을 수정해야 한다
#요렇게?
@coroutine
def accept_coro(future, conn):
# Coroutine closing the accept socket if the future is cancelled
try:
yield from future
except futures.CancelledError:
conn.close()
raise
except OSError as exc:
if exc.winerror == _overlapped.ERROR_NETNAME_DELETED:
pass
이외에도 tasks.async 의 리턴 값을 future와 같이 호출자에게 리턴해서 coro task 의 add_done_callback 이라도 심을 수 있게 하던지…(아 이건 아니야…)
python asyncio가 수정되기 전까지는, 처음 링크건 IOCP 관련 포스팅에서 언급된 것처럼 AcceptEx 요청 개수를 늘려서 backlog 쪽에서 클라이언트가 대기하지 않는 방식을 사용하자. 난 이미 이 방식을 사용하고 있었는데 AcceptEx 요청 개수가 내가 원하는 순간 접속 시도 회수보다 작았기 때문에 문제가 발생했던 것으로 보인다. 제일 위에 언급했던 accept 함수를 아래와 같이 수정하면 될 것이다.
@asyncio.coroutine
def accept(loop, s):
for i in range(2000):
loop.create_task(accept_impl(loop, s))
@asyncio.coroutine
def accept_impl(loop, s):
while True:
try:
yield from loop.sock_accept(s)
except OSError:
logging.error("oserror")
참고로 이 포스팅을 하기 위해 재확인차 작성했던 소스를 첨부한다.
https://gist.github.com/kernel0/400d040dcf2734457123
약간 설명하자면 listen 에 backlog 값을 크게 지정한다. (서버 소스의 accept방식이면 500정도만 되어도 잘 발생함)
더미 클라이언트로 여러 개의 소켓 접속 시도를 한다. 접속 시도 중에 더미 클라이언트 파이썬 프로세스를 강제 종료하면 문제가 재현된다.