[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) 다시 해보면 된다.