feat: ingest cooperative cancellation (p9-fb-04) #57

Merged
altair823 merged 2 commits from feat/p9-fb-04-cancel into main 2026-05-02 21:39:57 +00:00
Owner

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

#[doc(hidden)]
pub fn ingest_with_config_cancellable(
    config: kebab_config::Config,
    scope: SourceScope,
    summary_only: bool,
    progress: Option<Sender<IngestEvent>>,
    cancel: Option<Arc<AtomicBool>>,
) -> anyhow::Result<IngestReport>;

기존 ingest_with_config_progresscancel=None forwarding. asset loop iter 시작 boundary 마다 atomic load → true 면 break + IngestEvent::Aborted { counts: <partial> } + 정상 반환. Lock 없음.

CLI

  • 새 dep 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 warnings clean.

미적용 (out of scope)

  • embed / RAG streaming cancel (별 task)
  • resume from checkpoint (현재는 idempotent re-run 으로 충분)

후속

  • 머지 후 tasks/p9/p9-fb-04-ingest-cancellation.md status in_progresscompleted 한 줄 commit.
## 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 ```rust #[doc(hidden)] pub fn ingest_with_config_cancellable( config: kebab_config::Config, scope: SourceScope, summary_only: bool, progress: Option<Sender<IngestEvent>>, cancel: Option<Arc<AtomicBool>>, ) -> anyhow::Result<IngestReport>; ``` 기존 `ingest_with_config_progress` 가 `cancel=None` forwarding. asset loop iter 시작 boundary 마다 atomic load → true 면 break + `IngestEvent::Aborted { counts: <partial> }` + 정상 반환. Lock 없음. ### CLI - 새 dep `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 - [x] `cargo test -p kebab-app --test ingest_cancel` — 3 PASS (cancel-before / cancel-mid / no-cancel default). - [x] `cargo test -p kebab-tui --lib ingest_progress` — 13 PASS (10 기존 + 3 cancel_running_ingest 신규). - [x] `cargo clippy -p kebab-app -p kebab-cli -p kebab-tui --all-targets -- -D warnings` clean. ## 미적용 (out of scope) - embed / RAG streaming cancel (별 task) - resume from checkpoint (현재는 idempotent re-run 으로 충분) ## 후속 - 머지 후 `tasks/p9/p9-fb-04-ingest-cancellation.md` status `in_progress` → `completed` 한 줄 commit.
altair823 added 1 commit 2026-05-02 21:36:48 +00:00
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>
claude-reviewer-01 requested changes 2026-05-02 21:38:38 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — invariant 명시 1건 + redundant assertion 1건 + DoD partial 1건 + 칭찬 2건.

핵심 actionable:

  1. (invariant 명시) cancel.rsSIGNAL_COUNT static 의 process-global lifetime 명시 한 줄.
  2. (redundant assertion) ingest_cancel.rs::cancel_mid_loopreport.new == 1 || 0 || 2 모든 가능한 값 — 다음 줄의 < 3 와 중복. 제거 + race timing 의도 코멘트.
  3. (DoD partial) spec DoD 의 "CLI Ctrl-C handler test" 부분 미충족 — signal handler 는 subprocess + timing 의존이라 flaky 위험. 본 PR 에 추가하거나 별 task 로 미룸 (권장: 미룸 — facade integration 3 PASS + tui lib unit 가 안정 surface).

총평: spec PR #51 의 §10 cancel invariant + p9-fb-01/02/03 위에 정확히 4번째 layer. was_cancelled 단일 terminal emit + is_cancel_chord NLL 우회 둘 다 깔끔. 위 nit 2건만 정리하면 머지.

회차 1 — invariant 명시 1건 + redundant assertion 1건 + DoD partial 1건 + 칭찬 2건. 핵심 actionable: 1. **(invariant 명시)** `cancel.rs` 의 `SIGNAL_COUNT` static 의 process-global lifetime 명시 한 줄. 2. **(redundant assertion)** `ingest_cancel.rs::cancel_mid_loop` 의 `report.new == 1 || 0 || 2` 모든 가능한 값 — 다음 줄의 `< 3` 와 중복. 제거 + race timing 의도 코멘트. 3. **(DoD partial)** spec DoD 의 \"CLI Ctrl-C handler test\" 부분 미충족 — signal handler 는 subprocess + timing 의존이라 flaky 위험. 본 PR 에 추가하거나 별 task 로 미룸 (권장: 미룸 — facade integration 3 PASS + tui lib unit 가 안정 surface). 총평: spec PR #51 의 §10 cancel invariant + p9-fb-01/02/03 위에 정확히 4번째 layer. `was_cancelled` 단일 terminal emit + `is_cancel_chord` NLL 우회 둘 다 깔끔. 위 nit 2건만 정리하면 머지.

(칭찬) was_cancelled boolean + 함수 끝의 단일 terminal emit (Aborted vs Completed if/else) — emit 위치가 두 곳으로 분기되지 않고 한 곳에서 결정. 미래 누군가 새 terminal variant (e.g. Failed) 추가 시 한 곳만 수정. AggregateCounts 의 final_counts 도 한 번 build → emit 과 IngestReport 둘 다 동일 값 보장.

(칭찬) `was_cancelled` boolean + 함수 끝의 단일 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 (둘 중):

  1. 본 PR 에 add — tests/cli_sigint.rsCommand::spawn() + kill + 결과 검증. flaky 위험 감수.
  2. 본 PR 의 DoD 미충족 명시 + 별 task 로 미루기. 권장 — facade integration (3 PASS) + tui lib unit 만 본 PR 의 안정 surface.
(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 (둘 중): 1. 본 PR 에 add — `tests/cli_sigint.rs` 가 `Command::spawn()` + `kill` + 결과 검증. flaky 위험 감수. 2. 본 PR 의 DoD 미충족 명시 + 별 task 로 미루기. 권장 — facade integration (3 PASS) + tui lib unit 만 본 PR 의 안정 surface.

(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.

(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 — kebab binary 가 종료될 때까지 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 으로 분리 필요.

(invariant 명시) `static SIGNAL_COUNT: AtomicU8` 가 process-global — `kebab` binary 가 종료될 때까지 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 사용).

(칭찬) `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 사용).
altair823 added 1 commit 2026-05-02 21:39:44 +00:00
회차 1 actionable 2건 반영 + 1건 (CLI Ctrl-C integration test)
은 본 PR 에서 별도 task 로 미룸 (signal handler subprocess test 의
flaky 위험 + facade 3 PASS + tui lib 3 PASS 가 안정 surface).

- cancel.rs::install_sigint_cancel: SIGNAL_COUNT 위에 process-lifetime
  invariant 코멘트 — multi-install 차단 (ctrlc::set_handler) 덕분에
  reset 불필요. 미래 다중 caller 가 같은 cancel token 공유하려면
  install 함수 분리 필요.
- ingest_cancel.rs::cancel_mid_loop: redundant `report.new == 1 || 0
  || 2` 제거, race timing 의도 코멘트로 대체 (0=listener 승, 1=first
  only, 2=extra slipped in 모두 valid; 3 = cancel never propagated
  = 유일한 fail).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-02 21:39:51 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 actionable 2건 (SIGNAL_COUNT lifetime 코멘트 / cancel-mid race timing 코멘트) 모두 정확히 반영. CLI Ctrl-C subprocess integration test 는 flaky 위험으로 별 task — 합리적 trade-off. APPROVE.

회차 2 — 회차 1 actionable 2건 (SIGNAL_COUNT lifetime 코멘트 / cancel-mid race timing 코멘트) 모두 정확히 반영. CLI Ctrl-C subprocess integration test 는 flaky 위험으로 별 task — 합리적 trade-off. APPROVE.
altair823 merged commit 6d5c98bf87 into main 2026-05-02 21:39:57 +00:00
altair823 deleted branch feat/p9-fb-04-cancel 2026-05-02 21:39:58 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#57