LabHub
배우기 러닝패스 코스

연결 하나가 느려지자 나머지가 전부 멈췄다 · 운영자의 연결 진단기: 예산·원인·회수 · 실습

연결 진단기: 동시에 두드리고 안전하게 그만두기

LabHub 에서 이어서 보기

목표

여러 TCP 연결 시도를 제한된 예산으로 진행하고, 거절·마감·취소를 구분하며 모든 자원을 회수하는 진단기를 만듭니다.

90분 실습입니다. 기본 세션은 60분이므로 만료 전에 +시간으로 연장하세요(최대180분). 세션이 끝나면 파일이 사라집니다. 중요한 작업물은 종료 전에 별도로 보관하세요.

왜 중요한가

연결 성공만 보는 진단기는 실패 원인을 숨기고, 취소가 안 되는 도구는 사용자가 강제 종료하게 만듭니다. 이번 실습은 앞의 공정성·SO_ERROR·제어 채널을 한 반복문에서 연결합니다. 실제 통신은 실습 환경의 loopback만 사용하며 외부 서버를 검사하지 않습니다. 데이터 전달·HTTP·TLS·재시도·DNS·서버의 graceful shutdown은 이 실습의 범위가 아닙니다. DefaultSelector 기반 완료 처리이며 ET 읽기 서버가 아닙니다.

검사는 소스 파일을 임포트하여 동작을 시험합니다. 파일 맨 위에서 서버를 띄우거나 입력을 기다리지 마세요. 추가 패키지와 권한은 필요 없습니다. /opt/fixtures/reactor의 세 probe.py는 별도 개념 관찰용이며 수정하거나 제출할 재료 파일이 아닙니다.

단계

1. 연결 시도의 입력과 초기 상태를 정의한다/root/reactor/client.py에서 Dial(sock, host, port, deadline=None)을 구현하세요. 입력 sock은 호출자가 연 서로 다른 Linux AF_INET/SOCK_STREAM 소켓입니다. host는 숫자 IPv4 문자열만, port는 bool을 제외한 1–65535 int만 받으며 위반은 ValueError입니다. deadline=None이면 time.monotonic()+30, 지정 값은 bool을 제외한 유한·음이 아닌 int/float여야 합니다. sock을 비차단으로 바꾸고 sock, endpoint=(정규화한 host, port), float인 deadline, state='new', error=None을 보관하세요. 생성 실패 시 소켓 회수는 호출자 책임입니다.

2. 즉시 성공과 연결 중 상태를 구분한다/root/reactor/client.py에서 Dial.start()는 new에서만 sock.connect_ex(endpoint)를 한 번 호출합니다. 0/EISCONN은 connected·error=0, EINPROGRESS/EALREADY는 pending·error=None, 다른 코드는 failed·error=코드입니다. OSError는 errno를 기록하고 errno가 없으면 EIO입니다. new가 아니면 호출 없이 기존 state를 반환합니다. 모든 경로에서 state 문자열을 반환하며, 이 Linux IPv4 계약에서는 EAGAIN을 pending에 포함하지 않습니다.

3. 최초 SO_ERROR를 보존한다/root/reactor/client.py에서 Dial.writable()은 pending일 때만 getsockopt(SOL_SOCKET, SO_ERROR)를 한 번 읽고 0/EISCONN은 connected·0, 다른 코드는 failed·해당 코드로 기록합니다. OSError는 errno 또는 EIO로 실패 처리합니다. pending이 아니면 조회하지 않습니다. 현재 state를 반환합니다. 호출자는 WRITE 준비를 관찰한 뒤에만 pending의 이 메서드를 호출해야 합니다. 실제 TCP listen 포트와 bind만 한 거절 포트도 검사합니다.

4. 대기열 마감과 취소 사유를 보존한다/root/reactor/client.py에서 Dial.expire(now)는 new/pending이며 now >= deadline일 때만 timed_out·ETIMEDOUT으로 바꿉니다. now는 호출자가 제공하는 유한 단조 시각입니다. Dial.cancel()은 new/pending만 cancelled·ECANCELED로 바꿉니다. 두 메서드는 이미 종료한 상태와 오류를 덮지 않으며 소켓을 직접 닫지 않습니다. deadline도 연장하지 않습니다.

5. 중복 없는 준비 큐를 만든다/root/reactor/client.py에서 ReadyQueue()에 push(item), pop(), discard(item), __len__을 구현하세요. item은 None이 아닌 해시 가능한 연결 식별자입니다. push는 이미 있으면 무시, pop은 FIFO로 제거·반환하고 비었으면 None, discard는 해당 항목을 제거하고 없어도 안전해야 합니다. 꺼낸 항목은 다시 추가할 수 있으며 같은 FD의 다른 세대 식별자는 구분합니다. run에서는 Dial 객체 자체를 키로 씁니다.

6. 정지 상태를 기록하고 커널 대기를 깨운다/root/reactor/client.py에서 Control()은 비차단 socketpair의 reader/writer와 처음 False인 threading.Event stop_event를 만듭니다. request_stop()은 Event.set 후 writer.send(b'Q')를 한 번 호출하고 BlockingIOError만 무시합니다. drain()은 reader.recv(4096)를 한 번 호출하여 bytes를 반환하되 BlockingIOError는 b'', EOF는 ConnectionError입니다. close()는 reader를 닫다 예외가 나도 writer까지 닫습니다. run 종료 뒤 request_stop 호출은 계약 밖입니다.

7. 상한과 예산을 지키며 모든 소켓을 회수한다/root/reactor/client.py에서 run(dials, control, limit=8, budget=4, ready=None)을 구현하세요. dials는 서로 다른 열린 소켓을 가진 서로 다른 새 Dial의 list(최대128개), limit은 bool 제외 1–128 int, budget은 bool 제외 1–64 int입니다. 목록 타입·개수·Dial 중복·new 상태와 두 예산을 검증해 위반 시 ValueError로 거절하며 이때 소유권은 호출자에게 남습니다. DefaultSelector 생성에 성공하면 함수가 모든 데이터 소켓과 Control을 소유합니다. 제어 reader를 READ/data=None으로 등록하고 ready가 있으면 한 번 호출합니다. 매 턴 정지를 먼저 확인하고 new/pending에 마감을 적용합니다. 정지면 미완료 전부 cancel하고 종료합니다. 한 턴에 최대 budget개를 시작하고 pending 최대 limit개를 WRITE/data=Dial로 등록합니다. 즉시 종료 항목은 회수합니다. 대기는 가장 가까운 마감까지 시간과 1초 중 작은 값(최소0)이며, 준비 큐가 남거나 대기 목록에 시작할 여유가 있으면 0입니다. select 결과의 제어 이벤트는 drain, WRITE는 중복 없는 준비 큐에 넣습니다. 정지를 다시 확인한 뒤 큐에서 최대 budget개만 꺼내고, I/O 직전 마감 확인 후 pending의 writable을 호출합니다. 종료된 항목은 큐에서 삭제·unregister 후 close합니다. 예외가 나도 다른 데이터 소켓과 Control, selector까지 회수하고 오류를 전파해야 합니다. 한 회수의 실패가 나머지 회수를 막지 않게 하세요. 빈 목록도 Control을 회수하고 []를 반환합니다. 정상 종료는 입력 순서의 {'state': 상태, 'error': 정수} list입니다. 진단 연결은 성공해도 닫으며 재사용하지 않습니다.

8. 성공과 실패 원인을 손실 없이 집계한다/root/reactor/client.py에서 summarize(results)는 dict들의 list만 받고 위반은 ValueError입니다. 각 행은 state가 connected/failed/timed_out/cancelled, error는 bool 제외 int여야 합니다. connected는 0, 다른 상태는 양수이며 timed_out은 ETIMEDOUT, cancelled는 ECANCELED여야 합니다. pending 등 미완료도 ValueError로 거절합니다. 반환은 total과 네 상태별 건수, errors 사전입니다. errors는 connected를 제외한 errno의 문자열을 키로 합산하고 키를 정렬합니다. 빈 입력은 total·네 상태 모두0과 errors={}입니다. 원본 목록과 행은 수정하지 않습니다. 이 단계는 앞의 실제 TCP·마감·취소·예산·예외 회수도 재검사합니다.

참고

단계 8개

  1. 연결 시도의 입력과 초기 상태를 정의한다
  2. 즉시 성공과 연결 중 상태를 구분한다
  3. 최초 SO_ERROR를 보존한다
  4. 대기열 마감과 취소 사유를 보존한다
  5. 중복 없는 준비 큐를 만든다
  6. 정지 상태를 기록하고 커널 대기를 깨운다
  7. 상한과 예산을 지키며 모든 소켓을 회수한다
  8. 성공과 실패 원인을 손실 없이 집계한다