비동기 Rust에서의 취소 처리

1 month ago 13

  • 비동기 Rust 환경에서의 취소 처리는 편리하지만, 잘못 다루면 예기치 않은 버그와 어려움이 발생함
  • 동기 Rust에서는 명시적인 플래그 체크나 프로세스 종료 등이 필요하지만, 비동기 Rust에서는 future를 드롭하는 것만으로 취소가 매우 쉽게 가능함
  • 취소 안전성(cancel safety)과 취소 정합성(cancel correctness)은 서로 다른 개념으로, 한 future의 취소가 시스템 전반에 문제를 초래할 수 있음
  • 취소와 관련된 주요 문제 패턴으로는 Tokio mutex, select 매크로, try_join, 그리고 future 사용 실수 등이 있음
  • 완벽한 해법은 없지만, 취소-안전 API 사용, future 핀 고정, task 분리 등으로 취소로 인한 문제를 줄일 수 있음

서론

  • 본 게시물은 비동기 Rust의 취소 처리(cancellation) 에 관한 RustConf 2025 발표 내용을 기반으로 함
  • 일반적인 Rust 비동기 코드 예시에서 메시지 수신 또는 전송 루프에 timeout을 추가하면 종종 메시지가 유실되는 문제를 발견할 수 있음
  • Oxide Computer Company 등 실제 대규모 시스템에서 async Rust를 활용하며 겪은 취소 문제 및 실제 버그 사례를 다룸
  • 글은 1) 취소 개념, 2) 취소 분석, 3) 실제적인 해결책 세 부분으로 구성됨
  • 필자는 Rust signal handling, cargo-nextest 개발 등을 통해 비동기 Rust의 장점과 어려움을 경험한 바 있음

1. 취소란 무엇인가?

취소의 의미

  • 취소(cancellation) 란 어떤 비동기 작업을 시작했다가 도중에 그 작업을 중단하는 상황임
  • 예시: 대용량 다운로드/네트워크 요청, 부분 파일 읽기 등에서 중간에 취소 가능

동기 Rust에서의 취소 방법

  • 일반적으로 원자성 플래그를 통해 주기적으로 취소 여부를 체크하거나, 특수한 예외(panic) 활용, 프로세스 전체 강제 종료 등 방식 존재
  • 일부 프레임워크(Salsa 등)는 panic payload 사용하지만, Rust의 모든 플랫폼에서 동작하지 않음(특히 Wasm 환경 등)
  • 스레드만 강제로 종료하는 것은 Rust 안전성 및 뮤텍스 구조상 허용되지 않음
  • 요약하면, 동기 Rust에서는 범용적·안전한 취소 프로토콜이 존재하지 않음

비동기 Rust: Future란 무엇인가?

  • Future는 Rust 컴파일러가 생성한 상태기계(state machine) 로 메모리 내 단순 데이터에 불과함
  • 생성만으로 실행되지 않고, await 또는 poll 호출 시에만 진행됨
  • Rust의 Future는 수동적(inert)이며, 명시적 poll/await가 없으면 아무 작업도 처리하지 않음
  • Go/JavaScript/C# 등은 future 생성 시 즉시 실행 개시하는 것과 대조적

비동기 Rust의 취소 프로토콜

  • Future 취소는 단순히 드롭(drop)하거나 poll/await를 더 이상 호출하지 않는 방식임
  • State machine이므로 언제든지 Future를 버릴 수 있음
  • 비동기 Rust에서 취소가 매우 강력하면서도 쉽게 적용됨
  • 그러나 너무 쉽게 조용히 future가 드롭되어, 자식 future까지 연쇄적으로 취소됨(소유권 모델에 따라)
  • 이러한 특성 때문에 취소가 비지역적(non-local) 현상이 되어, 전체 호출 체인에 영향을 끼침

2. 취소 분석

취소 안전성과 취소 정합성

  • 취소 안전성(cancel safety) : 개별 future가 부작용 없이 안전하게 취소될 수 있는 속성
    • 예: Tokio의 sleep future는 취소 안전함
    • 반면, Tokio의 MPSC send는 drop 시 메시지 유실 위험 존재(취소 안전성 없음)
  • 취소 정합성(cancel correctness) : 시스템 전체가 취소 상황에서 본질적 속성을 유지하는 글로벌 성질임
    • 취소-안전 future가 시스템 내에 없다면 정합성 문제 없음
    • 취소-안전하지 않은 future가 실제로 취소되어야만 문제가 발생함
    • 취소로 인해 데이터 유실, 불변성 위반, 누락된 클린업 발생 시 취소 정합성 위반됨

Tokio mutex의 어려움

  • Tokio mutex는 락을 가져와 데이터 보정 후 해제를 통해 동작함
  • 문제: lock 안에서 상태를 임시로 위반(예: Option<T>를 None으로 변경) 후, await 경유 시 future가 취소되면 잘못된 상태에 데이터가 고정됨
  • 실제 현업(예: Oxide에서 sled 상태 관리)에서 await 포인트에서 취소로 인해 불안정 상태 발생함
  • 이처럼 비동기 코드의 상태 관리에서 취소가 매우 위험한 결함의 원인이 됨

취소 발생 패턴 및 예시

  • .await 빠진 future 호출: Rust는 미사용 future에 경고하지만, Result 반환값을 _로 받는 경우 미경고(Clippy 최신 린트 적용 필요)
  • try_join 등 try 연산: 하나의 future 실패 시 나머지 취소 발생(실제 서비스 중지 로직에서 버그로 연결)
  • select 매크로: 여러 future 병렬 처리 후, 완료된 이외의 future를 모두 취소(select 루프에서 데이터 유실 등 위험 커짐)
  • 이러한 패턴은 문서상 언급되어 있지만, 실제로 다양한 곳에서 비동기 취소가 암묵적으로 발생할 수 있음

3. 무엇을 할 수 있을까?

  • 취소 정합성 관련 문제의 근본적·완전한 해법은 아직 없음
  • 다만 실무적으로, 다음과 같은 방법들로 취소 결함 가능성을 줄일 수 있음

취소-안전 future로 재구성

  • MPSC send 예시: 예약(reserve) 후 실제 전송(send) 분리해 부분 취소-안전성 확보
    • 예약 작업은 취소해도 관련 메시지가 유실되지 않음
    • permit 획득 후엔 취소 걱정 없이 전송 가능
  • AsyncWrite의 write_all: 전체 버퍼 write_all은 취소 불안정, write_all_buf는 버퍼 커서를 활용해 취소 시 진행 상태 추적 가능
    • 루프 내에서 write_all_buf로 부분 진행 상태를 안전하게 재개 가능

취소를 피하는 future 운용

  • future pinning: select 루프 등에서 future를 pin으로 고정해 취소되지 않게 참조로 poll 하여 대기
    • 예: reserve future를 재사용하면 예약 대기 순번 유지
  • task 활용: tokio::spawn 등으로 future를 task로 실행하면, 핸들을 드롭해도 task 자체는 런타임에서 별도 관리되어 강제 취소되지 않음
    • Oxide Dropshot HTTP 서버 등에서 각 요청을 별도 task로 실행, 클라이언트 연결이 끊겨도 요청 처리의 완료 보장

체계적인 해법?

  • 현재 safe Rust 차원에서는 제약적이나, 논의되고 있는 접근 방법 있음
    • Async drop: future가 취소될 때 비동기 클린업 코드 실행 허용
    • 선형 타입(linear types) : drop 시 특정 코드 강제 실행 또는 특정 future를 취소 불가능하게 표시
  • 위 방식들 모두 구현상 어려움 존재

결론 및 권장 사항

  • Future는 수동적(passive)이라는 특성을 근본적으로 인지해야 함
  • 취소 안전성(cancel safety) , 취소 정합성(cancel correctness) 개념 숙지 필요
  • 주요 취소 버그 사례 및 코드 패턴을 파악해, 대응 전략 미리 준비 필요
  • 일부 실전 권고
    • Tokio mutex 사용을 피하고 대안을 고려
    • 부분-완성 API 또는 취소-안전 API 설계/활용
    • 취소-안전하지 않은 future에는 꼭 완료를 보장하는 코드 구조 채택
  • 추가로, cooperative cancellation, actor 모델, structured concurrency, panic safety, mutex poisoning 등 심화 주제도 검토 권장
  • 관련 자료는 sunshowers/cancelling-async-rust에서 참고 가능

읽어주셔서 감사함. 발표 및 참고 자료 검토와 피드백을 제공한 Oxide 동료들에게 감사를 표함

Read Entire Article