[python/asyncio] loop.sock_recv 를 사용하는 곳 이외에서 소켓 종료를 시키면 곤란한 일이 생깁니다.

개발 중인 python server 는 python asyncio 가 제공하는 high-level io(streams) 나 low-level io(transport/protocol) 를 쓰지 않고 eventloop 의 low-level socket api 를 쓴다. 앞의 2개는 더미클라이언트를 만들면서 둘 다 써봤는데 서버쪽에서 사용하기에는 세세한 컨트롤이 부족해 보였다. (내가 몰라서 그런것일 수도 있다)

osx selectorEventLoop에서 500~1000 정도 접속 테스트중에 더미클라리언트로 접속 후 접속 종료를 시켰더니 loop 에 등록한 exception handler 가 호출됐다. 보통 처리해야할 exception을 누락시켰을 때 저 exception handler 까지 가는 데 이번에는 그런 exception도 아니었다.(ValueError) 분명  future 객체를 반환하는 sock_recv 안에서 발생하는 것으로 보여서 add_done_callback 까지 걸었는데도 그 callback 으로 오지도 않고 계속 exception handler 로 갔다.

위 문제가 발생한 원인은 다음과 같다. sock_recv 는 내부적으로 future 객체를 만든다. 바로 요청을 처리하지 못하면  loop의 add_reader 함수를 통해 loop에 내장된 selectors api 를 이용한다. 데이터 수신 완료가 되면 future 로 통지한다. 이 통지를 하기 전에 eventloop 에서 작업을 하는 데 이때 exception이 발생하면 future 객체로 exception을 전달하지 않고 상위로 올라가 exception handler 를 호출하는 것이다.

그럼 ValueError exception이 발생한 원인은 무엇인가. sock_recv 내부적으로 remove_reader를 호출하는데 이 때 socket_handle이 -1  (INVALID_HANDLE)로 넘어갔기 때문이다.

왜 소켓 핸들은 -1 인가? 이는 recv 처리가 아닌 곳에서 이미 소켓 종료 처리를 했기 때문이다. send 쪽에서 실패 시 소켓 종료 처리를 하고 있었는데, 이는 내가 여태까지 일해왔던 windows C++ 기반 멀티쓰레드 서버에서 흔히 쓰던 방식이었다.  비윈도우즈쪽의 소켓 종료 판정 부분은 recv 에서만 한다던지 그런 얘기를 아직 들어본 적이 없지만, 적어도 python asyncio 쪽에서는 sock_recv 가 불리는 부분 이외의 곳에서 socket 을 종료시키면 안되는 룰이 있는 것으로 보인다. (아님 버그던지)

sock_recv 를 쓰는 부분 외에서도 소켓을 종료할 수 있는 방법이 있다. 소켓 종료 처리 전, 그러니까 socket.close() 를 호출하기 전에 loop.remove_reader(소켓핸들) 를 호출하는 것이다. 하지만 윈도우즈 쪽에서 사용하는 ProactorEventLoop 에는 remove_reader api가 아예 없다. (문서상으로 지원되지 않는다고 명시해놨다) 저 함수를 호출하기 전에 유닉스 계열 루프 인지 윈도우즈 계열 루프인지 확인을 해야하니 깔끔한 코드가 나오지 않는다. 그냥 별탈 없다면 recv 쪽에서만 종료 판정을 하도록 수정해야겠다. (하지만 send 하다가 send buffer 를 다 채우면 종료가 돼야 하는데… 종료 요청 플래그를 켜서 recv 쪽에서 해줘야 하나 ㅠㅠ)

참고로 윈도우즈 쪽 Proactor Event Loop 는 저런 문제가 없다. IOCP작업을 하다가 exception 이 발생하면 원래의 future 에 set_exception을 이쁘게 한다. (하지만 윈도우즈쪽 sock_accept 가 나를 빡치게 했지..[python-asyncio] ProactorEventLoop의 sock_accept은 특정한 상황에서 처리할 수 없는 예외가 발생한다.)

 

update : 내가 구현한 코드쪽에서 필요한 부분이 있어서 결국 어디서나 소켓 종료할 수 있게 변경했다. ProactorEventLoop 가 아니면 remove_reader 를 호출하도록 했음. python asyncio/selector_events.py 의 _SelectorTransport 의 close method에서도 remove_reader 를 호출하고 있어서 나도 동일하게 작업함. (이러다보면 결국 low-level API 랑 비슷하지만 더 안좋은 코드를 만드는 게 아닐까 싶네… 비슷한 얘기들이 떠오른다. MFC가 싫어서 win32 api 로만 작업했는데 결국은 안좋은 MFC가 됐고, stl 이 싫어서 만들었더니 안좋은 stl …)

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다