<aside> 🛡️

결론 — Phase 1 RIO는 안전성 검증을 통과했다.

자가검증(3렌즈) · 결함 방어 · 스트레스 소크(A) · Application Verifier(B) — 네 방식으로 교차 확인했다. critical/major 결함 0건, 발견한 minor 1건은 방어 완료. 이제 Phase 2 대규모 실측에 넣을 준비가 됐다.

</aside>

왜 안전성부터?

RIO는 전송 계층만 IOCP에서 갈아끼운 것이라(상위 로직 불변), 성능 실측에 앞서 "이 교체가 서버를 불안정하게 만들지 않았는가"를 먼저 확인했다. IOCP와 다른 RIO 특유의 위험 4가지가 표적이다.

위험 내용
소유권 불변식 "한 세션의 RQ는 소유 워커만 만진다" — 비소유 스레드가 건드리면 CQ가 오염된다(RIO는 내부 락이 없다)
핸드오프 레이스 CONNECTED push ~ NewConn 핸드오프 틈에 명령(FlushSend·Disconnect)이 역전될 수 있다
종료 경로 CancelIoEx가 없어, closesocket으로 pending을 에러완료시켜 IOCount 수렴을 유도한다 — 대량에서도 정확해야 한다
메모리 경계 등록 슬랩 슬라이스 오버런, 세션 재사용 시 상태 리셋 누락

1. 자가검증 — 3렌즈 정독

RIO 경로 전체(IOCPServer.cpp 등)를 결함 · 과한 구현 · 최적화 세 관점으로 정독하고, 후보를 코드로 재검증했다.

렌즈 결과
결함 · 정확성 critical/major 0. 체크리스트(슬랩 경계 · 링 랩 · IOCount 증감 짝 · 세션 재사용 리셋 · 셧다운 순서 · RIO_BUF offset · 배치 드레인 · notify 재무장 · cmdQueue) 전부 정상. 이론상 minor 1건만(아래 2번)
과한 구현 구조적 과잉 0. pin 3중 장치 · notify 재드레인 · FlushSend 재투입은 전부 RIO 제약에서 강제된 필수 복잡도. 고칠 값 1건 = 주석 오기(IOCPServer.h:352가 실제 없는 "스핀" 단계 서술) → 수정 완료
최적화 즉시 결함 0. 실측 선행 후보 2건(① 모니터 카운터 공유 캐시라인 샤딩 ② 유휴 전이 스핀) — 병목 확인 전 착수 비권장, Phase 2 계측 후 판단

2. 발견한 결함 1건 + 방어 (minor)

FlushPendingSends에서 송신 대기 표식(_queuedForSend)이 오염될 수 있는 극미 확률의 창을 발견했다.

메커니즘: push할 sessionIdInterlockedExchange 전에 읽는데, 읽은 직후 그 세션 슬롯이 재사용되면 → exchange는 새 세션의 표식을 TRUE로 세우지만 push되는 건 옛 id. 워커가 FindSession으로 새 세션을 못 찾아 표식을 못 지우고, 그 세션은 생애 동안 송신 mute 상태가 된다.

성격: 세션 풀이 고정이라 use-after-free/크래시는 아니고 논리 오염뿐. 발생 조건이 ns 단위 레이스라 사실상 관측 불가. RIO 고유가 아니라 기존 SendWorker 경로에서 승계된 공유 구멍.

방어 (C 방식): sessionId 읽기를 exchange 성공 블록 으로 옮긴다. push id가 방금 표식을 세운 세션의 현재 id와 항상 일치하므로, 워커가 정확히 그 세션을 찾아 표식을 정리한다. 재사용이 그 사이 또 끼어도 Initialize의 리셋이 최종값이라 "TRUE인 채 아무도 안 지움" 상태가 구조적으로 불가능해진다.

// before — sessionId를 exchange "전"에 읽음 (재사용 시 push id ≠ 표식 세운 세션)
const int64_t sessionId = session->_sessionId;
session->_sendDirty = false;
if (InterlockedExchange(&session->_queuedForSend, TRUE) == FALSE)
    perWorker[owner].push_back(sessionId);

// after — exchange 성공 "후"에 읽어 push id와 표식 세운 세션을 일치
session->_sendDirty = false;
if (InterlockedExchange(&session->_queuedForSend, TRUE) == FALSE)
{
    const int64_t sessionId = session->_sessionId;
    perWorker[owner].push_back(sessionId);
}

비용은 사실상 0(push하는 경우에만 id 읽기, 원자연산·pin 추가 없음). RIO·SendWorker 양 경로에 적용, IOCP/RIO 양쪽 Release 빌드 통과.


3. A — 강화 스트레스 소크 (safety-soak.ps1)

방법: 접속폭풍(DisconnectTest=1 + ReconnectIntervalMs↓ + 클라 3000)으로 세션 슬롯 재사용을 빈발시켜, 방어한 레이스 창을 반복 타격한다. 판마다 서버를 재기동하고 CTRL_C로 graceful 종료(활성 세션 대량을 비소유 스레드가 Disconnect 핸드오프 → 종료 경로 드레인). 이를 N판 반복.