feat: ingest cooperative cancellation (p9-fb-04) #57
Reference in New Issue
Block a user
Delete Branch "feat/p9-fb-04-cancel"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Ctrl-C / Esc 가
kebab ingest를 즉시 중단 — 도그푸딩 막힘 강도 3위 (긴 ingest 를 중단할 수 없어 새 KB 시작 시 좌절). 현재 asset 마무리 후 이후 skip,Aborted { partial_counts }발신,Ok(IngestReport)반환 (Err 아님). 부분 commit 보존 → 다음 ingest 가 idempotent 재개.spec PR #51 의 §10 / §2.4a 의 cancel invariant 위에 build. p9-fb-01 (#52) facade + p9-fb-02 (#53) CLI display + p9-fb-03 (#55) TUI background 위에 4번째 layer.
변경
신규 facade
기존
ingest_with_config_progress가cancel=Noneforwarding. asset loop iter 시작 boundary 마다 atomic load → true 면 break +IngestEvent::Aborted { counts: <partial> }+ 정상 반환. Lock 없음.CLI
ctrlc = \"3\"(cross-platform SIGINT handler).crates/kebab-cli/src/cancel.rs::install_sigint_cancel()— 첫 신호: cancel.store(true) + stderr hint, 두 번째:std::process::exit(130)(canonical SIGINT exit code).Cmd::Ingest가 install 후 facade 에 token 전달.TUI
IngestState.cancel: Arc<AtomicBool>field (회차 1 review 의 reshape 정확).start_ingest가 cancel + tx 둘 다 만들어 worker 에 move.cancel_running_ingest(&app)helper —Esc/Ctrl-C가 in-flight 일 때만 cancel 우선, 그 외에는 quit.Test plan
cargo test -p kebab-app --test ingest_cancel— 3 PASS (cancel-before / cancel-mid / no-cancel default).cargo test -p kebab-tui --lib ingest_progress— 13 PASS (10 기존 + 3 cancel_running_ingest 신규).cargo clippy -p kebab-app -p kebab-cli -p kebab-tui --all-targets -- -D warningsclean.미적용 (out of scope)
후속
tasks/p9/p9-fb-04-ingest-cancellation.mdstatusin_progress→completed한 줄 commit.Ctrl-C / Esc 가 ingest 를 즉시 중단. 현재 in-flight asset 마무리 후 이후 asset 미실행, IngestEvent::Aborted { partial_counts } 발신, Ok(IngestReport) 정상 반환 (Err 아님). 부분 commit 보존, 다음 ingest 가 idempotent 재개. 신규 facade: kebab-app::ingest_with_config_cancellable(.., progress, cancel: Option<Arc<AtomicBool>>). 기존 _progress 가 cancel=None forwarding wrapper. asset loop 시작 boundary 마다 atomic load — true 면 break + Aborted emit + 정상 종료. Lock 없음. CLI: ctrlc crate 신규 dep. SIGINT handler 가 첫 신호에 cancel.store(true) + stderr hint, 두 번째 신호에 std::process::exit(130) (canonical SIGINT exit code). install_sigint_cancel() helper 가 Arc<AtomicBool> 반환, Cmd::Ingest 가 facade 에 전달. TUI: IngestState 에 cancel: Arc<AtomicBool> field 추가 (회차 1 review 결과의 reshape 정확). start_ingest 가 둘 다 만들어 worker 에 clone move. cancel_running_ingest(&app) helper — Esc / Ctrl-C 가 ingest 진행 중일 때만 cancel 우선, 그 외에는 quit. Test: - 3 facade integration (cancel-before / cancel-mid / no-cancel default). - 3 tui lib unit (cancel_running_ingest no-state / in-flight / terminated). Plan 갱신: p9-fb-04 status planned → in_progress. 머지 후 한 줄 commit 으로 completed flip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>회차 1 — invariant 명시 1건 + redundant assertion 1건 + DoD partial 1건 + 칭찬 2건.
핵심 actionable:
cancel.rs의SIGNAL_COUNTstatic 의 process-global lifetime 명시 한 줄.ingest_cancel.rs::cancel_mid_loop의report.new == 1 || 0 || 2모든 가능한 값 — 다음 줄의< 3와 중복. 제거 + race timing 의도 코멘트.총평: spec PR #51 의 §10 cancel invariant + p9-fb-01/02/03 위에 정확히 4번째 layer.
was_cancelled단일 terminal emit +is_cancel_chordNLL 우회 둘 다 깔끔. 위 nit 2건만 정리하면 머지.(칭찬)
was_cancelledboolean + 함수 끝의 단일 terminal emit (Aborted vs Completed if/else) — emit 위치가 두 곳으로 분기되지 않고 한 곳에서 결정. 미래 누군가 새 terminal variant (e.g.Failed) 추가 시 한 곳만 수정. AggregateCounts 의final_counts도 한 번 build → emit 과 IngestReport 둘 다 동일 값 보장.(DoD partial — CLI integration test 누락) p9-fb-04 spec DoD 의 "CLI Ctrl-C handler test" 부분 미충족. signal handler 는 subprocess 에 SIGINT 보내야 —
Command::output().spawn()후kill -INT <pid>가능하지만 timing 의존 + flaky 위험. 본 PR 에서는cancel.rs::cancel_module_compiles가 trivial sanity 만, 실제 SIGINT 흐름은 manual.Why: spec DoD 가 명시. 다만 signal handler test 는 일반적으로 subprocess + 시간 제어 필요 (1) — 안정 보장 어려움.
How to apply (둘 중):
tests/cli_sigint.rs가Command::spawn()+kill+ 결과 검증. flaky 위험 감수.(redundant assertion)
report.new == 1 || report.new == 0 || report.new == 2— 모든 가능한 값이 (3 미만 = 0/1/2). 다음 줄의assert!(report.new < 3, ...)가 실질 검증. 위 라인 사실상 항상 true 이라 noise.Why: redundant assertion 이 미래 reader 에게 "왜 0/1/2 만?" 의문 + 실제 race 시나리오 표시 효과 X.
How to apply: 위 줄 (
assert!(report.new == 1 || ...)) 제거 + 아래 줄에 race 의 timing 의도 명시 —// cancel-mid is timing-dependent: listener flips cancel after the first AssetFinished, but the loop may have started 1 more asset by the time the next iteration check runs. 0 (race won), 1 (first only), or 2 (one extra slipped in) all valid; 3 = cancel never propagated, fail.(invariant 명시)
static SIGNAL_COUNT: AtomicU8가 process-global —kebabbinary 가 종료될 때까지 reset 안 됨. 다중install_sigint_cancel호출 시 (예: 미래kebab eval run --with-cancel추가 시 두 번째 호출) 이전 count 잔존. 다행히ctrlc::set_handler자체가 두 번째 호출 시Err(MultipleHandlers)— 안전 net.Why: 미래 reader 가
SIGNAL_COUNT의 lifetime 의문 → 코드 옆에서 명시 필요.How to apply:
static SIGNAL_COUNT위에 코멘트 한 줄 —// Process-lifetime: ctrlc::set_handler 가 multi-install 차단하므로 reset 불필요. 다중 caller 가 같은 cancel token 공유하려면 cancel arg 받는 별 fn 으로 분리 필요.(칭찬)
is_cancel_chord분리 —match안 if-guard 로 합치면 borrow checker 가state의 immutable +inner의 mutable borrow 동시 사용 거부. let-binding 으로 분기 미리 결정 →cancel_running_ingest(state)호출이inner보유 전 완료. NLL + match arm lifetime 의 corner case 우회의 깨끗한 패턴. 같은 함수의r키 처리와 일관 (둘 다inner보유 전 state 사용).회차 2 — 회차 1 actionable 2건 (SIGNAL_COUNT lifetime 코멘트 / cancel-mid race timing 코멘트) 모두 정확히 반영. CLI Ctrl-C subprocess integration test 는 flaky 위험으로 별 task — 합리적 trade-off. APPROVE.