<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 수렴을 유도한다 — 대량에서도 정확해야 한다 |
| 메모리 경계 | 등록 슬랩 슬라이스 오버런, 세션 재사용 시 상태 리셋 누락 |
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 계측 후 판단 |
FlushPendingSends에서 송신 대기 표식(_queuedForSend)이 오염될 수 있는 극미 확률의 창을 발견했다.
메커니즘: push할 sessionId를 InterlockedExchange 전에 읽는데, 읽은 직후 그 세션 슬롯이 재사용되면 → 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 빌드 통과.
safety-soak.ps1)방법: 접속폭풍(DisconnectTest=1 + ReconnectIntervalMs↓ + 클라 3000)으로 세션 슬롯 재사용을 빈발시켜, 방어한 레이스 창을 반복 타격한다. 판마다 서버를 재기동하고 CTRL_C로 graceful 종료(활성 세션 대량을 비소유 스레드가 Disconnect 핸드오프 → 종료 경로 드레인). 이를 N판 반복.