fix: decouple oneliner-heal from request handler tasks
heal_orphans_if_idle called regenerate_missing_oneliners_for_user inline on the request task, blocking the browser for up to N×60s when orphans existed (observed with 16 cases after manual cleanup). Now: CAS on new OnelinerHealBusy flag + tokio::spawn + BusyGuard reset-on-drop. Handler returns immediately; the existing OnelinerUpdated SSE event delivers results via the usual reload. Startup recovery shares the flag so a concurrent page-load during boot cannot fire a parallel regen loop against Ollama. Regression test uses wiremock + timeout(150ms) + .expect(5) to verify both non-blocking return and dedup of parallel invocations.
This commit is contained in:
@@ -11,7 +11,6 @@ use tracing::{info, warn};
|
||||
|
||||
use time::{Date, OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use crate::PipelineState;
|
||||
use crate::analyze::{
|
||||
ANALYSIS_INPUT_FILE, DOCUMENT_FILE, auto_trigger, recovery as analyze_recovery,
|
||||
};
|
||||
@@ -25,6 +24,7 @@ use crate::paths;
|
||||
use crate::routes::case_actions::read_document;
|
||||
use crate::routes::web::{RecordingView, scan_recordings};
|
||||
use crate::transcribe::recovery as transcribe_recovery;
|
||||
use crate::{BusyGuard, PipelineState};
|
||||
|
||||
/// UI-layer oneliner status for the case list. Combines the persisted
|
||||
/// [`OnelinerState`] with derived transit states that only make sense
|
||||
@@ -299,8 +299,8 @@ impl PipelineState {
|
||||
user_root: &Path,
|
||||
slug: &str,
|
||||
http_client: &reqwest::Client,
|
||||
config: &Config,
|
||||
vocab: &Gazetteer,
|
||||
config: &Arc<Config>,
|
||||
vocab: &Arc<Gazetteer>,
|
||||
events_tx: &EventSender,
|
||||
) {
|
||||
if !self.analyze_busy.0.load(Ordering::Acquire) {
|
||||
@@ -309,18 +309,45 @@ impl PipelineState {
|
||||
if !self.transcribe_busy.0.load(Ordering::Acquire) {
|
||||
transcribe_recovery::enqueue_pending_for_user(user_root, slug, &self.transcribe_tx)
|
||||
.await;
|
||||
// Oneliner self-heal: if the transcribe worker is busy, it will
|
||||
// write a fresh oneliner at batch-end anyway — skip to avoid a
|
||||
// redundant LLM call.
|
||||
transcribe_recovery::regenerate_missing_oneliners_for_user(
|
||||
user_root,
|
||||
slug,
|
||||
http_client,
|
||||
config,
|
||||
vocab,
|
||||
events_tx,
|
||||
)
|
||||
.await;
|
||||
// Oneliner self-heal: if the transcribe worker is busy, it
|
||||
// will write a fresh oneliner at batch-end anyway — skip to
|
||||
// avoid a redundant LLM call. Otherwise, fire the regen in a
|
||||
// detached task so the request handler returns immediately;
|
||||
// SSE `OnelinerUpdated` events deliver results to the browser.
|
||||
// The CAS on `oneliner_heal_busy` guards against concurrent
|
||||
// page-loads spawning their own parallel regen loops against
|
||||
// the same cases.
|
||||
if self
|
||||
.oneliner_heal_busy
|
||||
.0
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok()
|
||||
{
|
||||
let user_root = user_root.to_path_buf();
|
||||
let slug_owned = slug.to_owned();
|
||||
let http_client = http_client.clone();
|
||||
let config = Arc::clone(config);
|
||||
let vocab = Arc::clone(vocab);
|
||||
let events_tx = events_tx.clone();
|
||||
let busy = Arc::clone(&self.oneliner_heal_busy.0);
|
||||
info!(user = %slug, "oneliner heal spawned");
|
||||
tokio::spawn(async move {
|
||||
// `BusyGuard::Drop` resets the flag to `false` — even
|
||||
// on panic inside the regen loop. The redundant
|
||||
// `store(true)` inside `BusyGuard::new` is harmless;
|
||||
// the CAS above already owns the flag transition.
|
||||
let _guard = BusyGuard::new(busy);
|
||||
transcribe_recovery::regenerate_missing_oneliners_for_user(
|
||||
&user_root,
|
||||
&slug_owned,
|
||||
&http_client,
|
||||
&config,
|
||||
&vocab,
|
||||
&events_tx,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user