[python-asyncio] ProactorEventLoop의 sock_accept는 특정한 상황에서 처리할 수 없는 예외가 발생한다.

윈도우즈에서 사용할만한 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 가 호출되면 서버가 종료되도록 설계했다.1 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정도만 되어도 잘 발생함)
더미 클라이언트로 여러 개의 소켓 접속 시도를 한다. 접속 시도 중에 더미 클라이언트 파이썬 프로세스를 강제 종료하면 문제가 재현된다.

  1. exception_handler가 호출될 때 서버를 다운 시키지 않으면 안되나?
    나는 안된다고 생각한다. 명시적으로 처리하지 못하는 exception이 있으면 서버가 다운되는 게 옳다고 생각한다. 서버 개발자들마다 철학(?)이 다를텐데 내 생각에는 exception을 뭉개버리면 서버 상태는 개발자가 예측할 수 없는 값을 갖고 뒹구르다가 또 다른 exception을 만들 수도 있고 그렇다면 뭐가 문제인지 모를 수도 있다. 또한 잘못된 데이터라도 들고 있다가 엉뚱한 값을 DB에 저장이라도 하면 헬게이트가 열리겠지.

[windows] SO_REUSEADDR

TIME_WAIT 된 소켓 재활용하는 옵션… 다아는 것 아님? 이럴 수도 있다.

윈도우즈에서 이 소켓 옵션을 사용하면, 프로세스를 띄울 때 이미 포트가 다른 프로세스에 바인드되어 있더라도 바인드 에러 없이 잘뜬다.-.,-

MS 설명에 따르면 대략 undefined behavior 인 듯 1

윈도우즈에서는 SO_EXCLUSIVEADDRUSE 를 쓰라고 한다. 이쪽 저쪽 다 되게 하려다 보니 귀찮은 일이 많네.

참고 링크
Differences Between Windows and Unix Non-Blocking Sockets
Winsock: strange conflict with SO_REUSEADDR
Why is SO_REUSEADDR disabled for Windows?
SO_REUSEADDR, SO_EXCLUSIVEADDRUSE
SO_REUSEADDR doesn’t have the same semantics on Windows as on Unix

[jenkins] git 의 특정 tag 가 지정되었을 때 빌드하기

1. project 구성 -> Repositories -> Advanced -> Refspec 항목에
+refs/tags/태그패턴:refs/remotes/origin/tags/태그패턴 입력.

예를 들어, dist/로 시작하는 모든 태그일 경우 +refs/tags/dist/*:refs/remotes/origin/tags/dist/*

2. Branches to build -> Specifier -> 태그패턴 입력
예를 들어 dist/로 시작하는 모든 태그일 경우 /dist/* 입력

[python] asyncio 지원하는 redis client 찾기

2개 있다. aioredis 와 asyncio_redis. asyncio_redis가 먼저나왔고 aioredis가 그 뒤로 나왔다. 첫번째 것이 괜찮았으면 두번째 것이 나왔겠냐 싶은데 일단….

aioredis 는 hiredis package 의존이 있다. hiredis 는 windows 지원을 하지 않는다. (workaround 가 있지만 개발자는 업데이트때 보장하지 않는다고 했다. 1 ) aioredis 개발자가 pure python fallback parser는 TBD라고 하니2 그냥 asyncio_redis 쓰다가 바꿔야겠다.

update : 쓰다보니 발견한 차이점이라면 asyncio_redis 는 zrangebyscore의 limit 옵션 구현이 안됐군 (참고 http://redis.io/commands/zrangebyscore)

update 2 : asyncio_redis 에 구현된 connection pool은 이미 사용하고 있는 connection을 할당하는 심각한 문제도 있다. (어떻게 재현하는 지는 다시 포스팅하겠음)

update 3: aioredis 개발자가 “hiredis parser가 짱 빠른거 같아 그냥 쓸래” 라는 것을 “님아 그럼 윈도우즈 사용자는 못씀!” 이라고 일단 징징거려서3 “i’ll consider this” 라는 대답도 받았으나 해당 이슈를 close 에서 안빼는 걸로봐서는 그다지 의지는 없는 듯 -_- 그래서 일단 내가 아쉬우니 asyncio_redis package용으로 간단한 ConnectionPool 만들었다. 이런일이 있을 때마다 windows python은 왕따 구나 싶은 생각이 자꾸 든다. 피해의식인가 =_=

update 4 : 못참는 용자들이 hiredis쪽에 windows support pull request를 넣고 있다. 신난다. 조만간 해결될 듯.

update 5 : 이제 aioredis 를 windows 에서도 쓸 수 있다. 2015-05-04 현재 pubsub 기능이 pypi 에 등록된 것에서는 작동하지 않지만(github 버전으로는 low-level api로 작동함) asyncio-redis 에 구현 되지 않은 command 옵션들도 지원하고 slaveof 명령어도 지원한다. 새 버전이 나오면 asyncio-redis 를 완벽하게 대체할 수 있을 것이다.

update 6 : asyncio는 2015-10-19 현재 pub/sub도 지원하고 redis 3.0 cluster는 버전업 준비중이다. 우리팀은 asyncio-redis를 삭제했다.

[python] nosetests 에서 내가 만든 모듈만 coverage 확인하는 법

내가 만든 프로젝트 최상단이 패키지로 묶여있지 않을 때 사용하는 방법

–cover-package=.

‘.’ 대신 패키지명을 입력하면 해당 패키지만 검사한다. (보통 설명에는 특정 패키지만 검사하는 것처럼 써있더라..)

[python] virtualenv 생성 시 tkinter 는 복사되지 않는 문제

matplotlib 를 사용하려고 하니 tcl 이 없다고 나옴. 잉? 예전에 python3.4.2 기본 설치하고 tkinter 작동시켜봤었는데 어떻게 된 일이지?

virtualenv 에는 os dependent 한 것들이 복사되지는 않나? 이 링크(https://bugs.launchpad.net/virtualenv/+bug/449537 )를 참고해서 확인 중이다. 해당 질문의 첫번째 댓글 단 아저씨 (https://bugs.launchpad.net/virtualenv/+bug/449537/comments/1) 말대로 python3.4.2 설치된 폴더에서 tcl 폴더만 virtualenv 로 가져오니 작동함.

좀 더 찾아봤더니 python3.4.2 에서 기본으로 제공하는 virtualenv 버전이 낮아서 (버전 1.x) 그런 것이었음.  virtualenv 의 버전을 업그레이드하고 (2015년 3월 현재 버전 12) 다시 해보면 된다.

[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 …)

[etc] sourcetree 1.6.x, windows 에서는 UI 반응이 너무 느리다.

SourceTree is very slow on windows 7
Source Tree is EXTREMELY slow for any action 

1.5.x 대는 문제 없었다고 해서 1.5.2 받았음. 이제야 쓸만하네. osx 용은 엄청 빠르던데 windows 는 대강 만들었나봄.

참고로 1.5.2 다운로드 링크 http://downloads.atlassian.com/software/sourcetree/windows/SourceTreeSetup_1.5.2.exe

 

update : 1.6.13 이 올라왔어도 약간 빨라진 정도라고… smartgit 빠르던데 그걸 쓸까… giteye도 빠르던데 아직 완성도가 모자란 것 같다.

update 2  : Has Atlasssian essentially abandoned SourceTree? sourcetree 제작자가 1.6 업데이트 이후 퇴사했다고. windows 쪽에서 sourcetree 쓰는 사람들은 고생이 많구나.

update 3 : 제작자의 퇴사는 오보라고 좀만 기다려 보라고 하더라. http://www.lifehacker.com.au/2015/01/6-month-old-critical-performance-bug-with-sourcetree-is-getting-attention/