agent: code-atomiser-fix — cancellable Whisper inference + bounded drain + lock-over-await
Three findings chain: thread::spawn discarded the JoinHandle and gave no cancellation route into the whisper backend; drain_inference busy- polled with no timeout; stop_live_transcription_session held the lifecycle AsyncMutex across the await of a non-cancellable spawn_blocking handle. Together: a single wedged inference (ggml deadlock, GPU stall) bricked every future start/stop until app restart. Fix: each inference task carries an Arc<AtomicBool> abort_flag; the flag is wired into whisper-rs::FullParams::set_abort_callback_safe so the spawned blocking thread checks it and exits cleanly. drain_inference is bounded by a deadline (3 x chunk_duration, min 2s); on expiry the flag is set, the receiver is dropped, and a typed Error::InferenceTimeout status is surfaced. Drop for InferenceTask asserts the abort flag so any '?'-propagation or panic unwind closes the cancellation route without relying on the explicit drain path. stop_live_transcription_session restructured so the lifecycle guard is dropped BEFORE the JoinHandle is awaited; start_live_transcription_session releases the guard explicitly after installing the RunningLiveSession. Unit test added: dropping_inference_task_sets_abort_flag covers the Race-A regression directly. A real Race-B drain-timeout test would require a wedged whisper-rs, which is hard to fixture; that path is covered by the SAFETY comment and cargo build + manual smoke. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
use transcribe_rs::{SpeechModel, TranscribeOptions, TranscriptionResult};
|
||||
@@ -155,6 +156,35 @@ impl LocalEngine {
|
||||
inference_ms,
|
||||
})
|
||||
}
|
||||
|
||||
/// Cancellable variant of `transcribe_sync`. Pipes `abort_flag`
|
||||
/// through to the backend so the live-session drain timeout can
|
||||
/// break a wedged whisper-rs decode out of its loop. Backends that
|
||||
/// can't honour the flag (Parakeet) fall through to their plain
|
||||
/// `transcribe_sync` via the trait's default implementation.
|
||||
pub fn transcribe_sync_with_abort(
|
||||
&self,
|
||||
audio: &AudioSamples,
|
||||
options: &TranscriptionOptions,
|
||||
abort_flag: Arc<AtomicBool>,
|
||||
) -> Result<TimedTranscript> {
|
||||
let mut guard = self.engine.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let backend = guard.as_mut().ok_or(Error::EngineNotLoaded)?;
|
||||
|
||||
let start = Instant::now();
|
||||
let segments =
|
||||
backend.transcribe_sync_with_abort(audio.samples(), options, abort_flag)?;
|
||||
let inference_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
Ok(TimedTranscript {
|
||||
transcript: Transcript::new(
|
||||
segments,
|
||||
options.language.clone().unwrap_or_else(|| "en".to_string()),
|
||||
audio.duration_secs(),
|
||||
),
|
||||
inference_ms,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Thin wrapper over `ParakeetModel` that overrides `transcribe_raw` to
|
||||
|
||||
Reference in New Issue
Block a user