- 홈
- Documentation
- 네이티브
- 네이티브 Rust 태스크 실행 및 취소 (`pi-natives`)
네이티브 Rust 태스크 실행 및 취소 (`pi-natives`)
이 문서는 crates/pi-natives가 네이티브 작업을 스케줄링하는 방법과 JS 옵션(timeoutMs, AbortSignal)에서 Rust 실행으로 취소가 전달되는 방식을 설명합니다.
구현 파일
섹션 제목: “구현 파일”crates/pi-natives/src/task.rscrates/pi-natives/src/grep.rscrates/pi-natives/src/glob.rscrates/pi-natives/src/fd.rscrates/pi-natives/src/shell.rscrates/pi-natives/src/pty.rscrates/pi-natives/src/html.rscrates/pi-natives/src/image.rscrates/pi-natives/src/clipboard.rscrates/pi-natives/src/text.rscrates/pi-natives/src/ps.rs
핵심 기본 요소 (task.rs)
섹션 제목: “핵심 기본 요소 (task.rs)”task.rs는 세 가지 핵심 구성 요소를 정의합니다:
-
task::blocking(tag, cancel_token, work)napi::AsyncTask/Task를 래핑합니다.compute()는 libuv 워커 스레드에서 실행됩니다(CPU 집약적이거나 블로킹/동기 시스템 호출의 경우).- JS
Promise<T>를 반환합니다.
-
task::future(env, tag, work)env.spawn_future(...)를 래핑합니다.- Tokio 런타임에서 비동기 작업을 실행합니다.
PromiseRaw<'env, T>를 반환합니다.
-
CancelToken/AbortToken/AbortReasonCancelToken::new(timeout_ms, signal)은 데드라인과 선택적AbortSignal을 결합합니다.CancelToken::heartbeat()는 블로킹 루프를 위한 협력적 취소입니다.CancelToken::wait()는 비동기 취소 대기입니다(Signal/Timeout/UserCtrl-C).AbortToken은 외부 코드가 중단을 요청할 수 있게 합니다(abort(reason)).
blocking vs future: 실행 모델 및 선택 기준
섹션 제목: “blocking vs future: 실행 모델 및 선택 기준”task::blocking 사용 시
섹션 제목: “task::blocking 사용 시”작업이 CPU 집약적이거나 근본적으로 동기/블로킹인 경우 사용합니다:
- 정규식/파일 스캐닝 (
grep,glob,fuzzy_find) - 동기 PTY 루프 내부 (
spawn_blocking을 통한run_pty_sync) - 클립보드/이미지/html 변환
동작 방식:
- 작업 클로저는 클론된
CancelToken을 받습니다. - 취소는 코드가
ct.heartbeat()?를 확인하는 곳에서만 감지됩니다. - 클로저
Err(...)은 JS 프로미스를 거부합니다.
task::future 사용 시
섹션 제목: “task::future 사용 시”작업이 비동기 작업을 await해야 하는 경우 사용합니다:
- 셸 세션 오케스트레이션 (
shell.run,executeShell) - 완료와 취소 간의 태스크 경쟁 (
tokio::select!)
동작 방식:
- Future는 일반 완료와
ct.wait()간에 경쟁할 수 있습니다. - 취소 경로에서 비동기 구현은 일반적으로 내부 서브시스템(예:
tokio_util::CancellationToken)에 취소를 전파하고 선택적으로 유예 타임아웃 후 강제 중단합니다.
JS API ↔ Rust 익스포트 매핑 (태스크/취소 관련)
섹션 제목: “JS API ↔ Rust 익스포트 매핑 (태스크/취소 관련)”| JS facing API | Rust 익스포트 (#[napi]) | 스케줄러 | 취소 연결 |
|---|---|---|---|
grep(options, onMatch?) | grep | task::blocking("grep", ct, ...) | CancelToken::new(options.timeoutMs, options.signal) + ct.heartbeat() |
glob(options, onMatch?) | glob | task::blocking("glob", ct, ...) | CancelToken::new(...) + 필터 루프에서 ct.heartbeat() |
fuzzyFind(options) | fuzzy_find | task::blocking("fuzzy_find", ct, ...) | CancelToken::new(...) + 스코어링 루프에서 ct.heartbeat() |
shell.run(options, onChunk?) | Shell::run | task::future(env, "shell.run", ...) | ct.wait()가 실행 태스크와 경쟁; Tokio CancellationToken에 브리지 |
executeShell(options, onChunk?) | execute_shell | task::future(env, "shell.execute", ...) | 위와 동일 |
pty.start(options, onChunk?) | PtySession::start | task::future(env, "pty.start", ...) + 내부 spawn_blocking | 동기 PTY 루프에서 heartbeat()를 통해 CancelToken 확인 |
htmlToMarkdown(html, options?) | html_to_markdown | task::blocking("html_to_markdown", (), ...) | 없음 (() 토큰) |
PhotonImage.parse/encode/resize | PhotonImage::{parse,encode,resize} | task::blocking(...) | 없음 (() 토큰) |
copyToClipboard/readImageFromClipboard | copy_to_clipboard / read_image_from_clipboard | task::blocking(...) | 없음 (() 토큰) |
text.rs와 ps.rs는 현재 task::blocking/task::future를 사용하지 않으므로 이 취소 경로에 참여하지 않습니다.
취소 생명주기 및 상태 전이
섹션 제목: “취소 생명주기 및 상태 전이”CancelToken 생명주기
섹션 제목: “CancelToken 생명주기”CancelToken은 협력적이며 상태를 가집니다:
Created ├─ 신호 없음 + 타임아웃 없음 -> 수동 토큰 (외부에서 설정되지 않는 한 중단되지 않음) ├─ 신호 등록됨 -> AbortSignal 콜백을 기다림 └─ 데드라인 설정됨 -> 타임아웃 확인이 활성화됨
Running ├─ heartbeat()/wait()가 신호 감지 -> AbortReason::Signal ├─ heartbeat()/wait()가 데드라인 감지 -> AbortReason::Timeout ├─ wait()가 Ctrl-C 감지 -> AbortReason::User └─ 중단 없음 -> 계속
Aborted (종료 상태) └─ 첫 번째 중단 이유가 우선 (원자적 플래그 + 알림자)시작 전 vs 실행 중 취소
섹션 제목: “시작 전 vs 실행 중 취소”-
시작 전 / 첫 번째 취소 확인 전:
ct.wait()에서 경쟁하는task::future사용자는select!에 진입하면 즉시 취소를 해결할 수 있습니다.task::blocking사용자는 클로저 코드가heartbeat()에 도달할 때만 취소를 감지합니다. 클로저가 일찍 heartbeat를 수행하지 않으면 취소가 지연됩니다.
-
실행 중:
blocking: 다음heartbeat()가Err("Aborted: ...")를 반환합니다.future:ct.wait()브랜치가select!에서 승리하고, 이후 코드가 종속 비동기 기계를 취소합니다(셸의 경우: Tokio 토큰을 취소하고, 최대 2초 대기 후 태스크를 중단합니다).
장기 실행 루프에 대한 Heartbeat 요구 사항
섹션 제목: “장기 실행 루프에 대한 Heartbeat 요구 사항”heartbeat()는 크기가 제한되지 않거나 큰 작업 세트를 가진 루프에서 예측 가능한 주기로 실행되어야 합니다.
관찰된 패턴:
glob::filter_entries: 필터링/매칭 전에 각 항목을 확인합니다.fd::score_entries: 스캔된 각 후보를 확인합니다.grep_sync: 무거운 검색 단계 전에 명시적 취소 확인, 그리고 토큰도 수신하는 fs-cache 호출.run_pty_sync: 매 루프 틱마다 확인(~16ms 슬립 주기)하고 취소 시 자식 프로세스를 종료합니다.
실용적 규칙: 외부 크기 입력에 대한 루프는 heartbeat 없이 짧은 제한 구간을 초과해서는 안 됩니다.
JS에 대한 실패 동작 및 오류 전파
섹션 제목: “JS에 대한 실패 동작 및 오류 전파”블로킹 태스크
섹션 제목: “블로킹 태스크”오류 경로:
- 클로저가
Err(napi::Error)를 반환합니다(heartbeat()중단 포함). Task::compute()가Err를 반환합니다.AsyncTask가 JS 프로미스를 거부합니다.
일반적인 오류 문자열:
Aborted: TimeoutAborted: Signal- 도메인 오류 (
Failed to decode image: ...,Conversion error: ...등)
Future 태스크
섹션 제목: “Future 태스크”오류 경로:
- 비동기 본문이
Err(napi::Error)를 반환하거나 조인 실패가 매핑됩니다(... task failed: {err}). task::future가 스폰한 프로미스가 거부됩니다.- 일부 API는 거부 대신 의도적으로 구조화된 취소 결과를 반환합니다(
ShellRunResult/ShellExecuteResult에cancelled/timed_out플래그 및exit_code: None포함).
취소 보고 방식 분리
섹션 제목: “취소 보고 방식 분리”- 오류로서의 중단:
heartbeat()?를 사용하는 대부분의 블로킹 익스포트. - 타입화된 결과로서의 중단: 결과 구조체에서 취소를 모델링하는 셸/pty 스타일 명령 API.
API별로 하나의 모델을 선택하고 명시적으로 문서화하십시오.
일반적인 함정
섹션 제목: “일반적인 함정”-
블로킹 루프에서 heartbeat 누락
- 증상: 루프가 끝날 때까지 타임아웃/신호가 무시되는 것처럼 보입니다.
- 해결: 루프 상단과 항목별 비용이 큰 단계 전에
ct.heartbeat()?를 추가합니다.
-
취소할 수 없는 긴 섹션
- 증상: 단일 대형 호출(디코드, 정렬, 압축 등) 중에 취소 지연이 급증합니다.
- 해결: 작업을 heartbeat 경계가 있는 청크로 분할합니다; 불가능한 경우 지연을 문서화합니다.
-
비동기 실행기 블로킹
- 증상: 동기 집약적 코드가 future 내에서 직접 실행될 때 비동기 API가 멈춥니다.
- 해결: CPU/동기 블록을
task::blocking또는tokio::task::spawn_blocking으로 이동합니다.
-
일관성 없는 취소 시맨틱
- 증상: 한 API는 취소 시 거부하고, 다른 API는 플래그와 함께 해결하여 호출자를 혼란스럽게 합니다.
- 해결: 도메인별로 표준화하고 래퍼 문서를 일치시킵니다.
-
중첩된 비동기 태스크에서 취소 브리지 누락
- 증상: 외부 토큰이 취소되었지만 내부 리더/서브프로세스 태스크가 계속 실행됩니다.
- 해결: 내부 토큰/신호로 취소를 브리지하고 유예 타임아웃 + 강제 중단 폴백을 적용합니다.
새로운 취소 가능 익스포트를 위한 체크리스트
섹션 제목: “새로운 취소 가능 익스포트를 위한 체크리스트”-
작업을 올바르게 분류합니다:
- CPU 집약적이거나 동기 블로킹 ->
task::blocking - 비동기 I/O /
await오케스트레이션 ->task::future
- CPU 집약적이거나 동기 블로킹 ->
-
필요할 때 취소 입력을 노출합니다:
#[napi(object)]옵션에timeoutMs와signal을 포함합니다let ct = task::CancelToken::new(timeout_ms, signal);을 생성합니다
-
모든 레이어에 취소를 연결합니다:
- 블로킹 루프: 안정적인 간격으로
ct.heartbeat()? - 비동기 오케스트레이션:
ct.wait()와 경쟁하고 서브태스크/토큰을 취소합니다
- 블로킹 루프: 안정적인 간격으로
-
취소 계약을 결정합니다:
- 중단 오류로 프로미스를 거부하거나,
- 타입화된
{ cancelled, timedOut, ... }으로 해결합니다 - API 계열에 대해 이 계약을 일관되게 유지합니다
-
컨텍스트와 함께 실패를 전파합니다:
Error::from_reason(format!("...: {err}"))를 통해 오류를 매핑합니다- 단계별 접두사를 포함합니다 (
spawn,decode,wait등)
-
시작 전 및 실행 중 취소를 처리합니다:
- 취소 확인/대기는 비용이 큰 본문 전과 장기 실행 중에 이루어져야 합니다
-
실행기 오용이 없는지 검증합니다:
spawn_blocking/블로킹 태스크 래퍼 없이 비동기 future 내에서 직접 장기 동기 작업을 실행하지 않습니다