9072d7a833
The analyze recovery scan has been refactored to iterate through user directories and enqueue pending analysis jobs more efficiently. The `WorkerBusy` status has been introduced as an `Arc<AtomicBool>` to track whether the analyze worker is currently processing a job. This `WorkerBusy` flag is used in the `analyze::worker::run` function and managed by a `BusyGuard` RAII struct, ensuring the flag is correctly set and unset even in case of panics. The `handle_my_cases` and `handle_case_detail` handlers in `user_web.rs` now utilize the `WorkerBusy` flag to accurately display the "analyzing" status and to trigger a self-heal of orphaned analysis inputs when the worker is idle. Additionally, the `WorkerBusy` type is now exported from `server/src/lib.rs` to be accessible by other modules. The test cases have been updated to include the `WorkerBusy` parameter when spawning the analyze worker.
96 lines
3.3 KiB
Rust
96 lines
3.3 KiB
Rust
use std::path::Path;
|
|
|
|
use tracing::{info, warn};
|
|
|
|
use super::{AnalyzeJob, AnalyzeSender};
|
|
|
|
/// Walk `data_path/*/` and enqueue every pending analysis for every user.
|
|
/// Intended to run once at startup; the same primitive backs the per-user
|
|
/// self-heal triggered by web handlers.
|
|
pub async fn scan_and_enqueue(data_path: &Path, tx: &AnalyzeSender) {
|
|
let Ok(mut users) = tokio::fs::read_dir(data_path).await else {
|
|
return;
|
|
};
|
|
let mut total = 0;
|
|
while let Ok(Some(user_entry)) = users.next_entry().await {
|
|
total += enqueue_pending_for_user(&user_entry.path(), tx).await;
|
|
}
|
|
info!(count = total, "Analyze recovery scan finished");
|
|
}
|
|
|
|
/// Scan a single `<user_root>/<case>/analysis_input_v*.json` set and enqueue
|
|
/// every input whose `document_v{N}.md` is missing. Returns the number sent.
|
|
/// Channel send is synchronous on the unbounded channel — only fails if the
|
|
/// worker is gone, which is a programming error.
|
|
pub async fn enqueue_pending_for_user(user_root: &Path, tx: &AnalyzeSender) -> usize {
|
|
let Ok(mut cases) = tokio::fs::read_dir(user_root).await else {
|
|
return 0;
|
|
};
|
|
let mut sent = 0;
|
|
while let Ok(Some(case_entry)) = cases.next_entry().await {
|
|
let case_dir = case_entry.path();
|
|
if !case_dir.is_dir() || crate::paths::is_deleted(&case_dir).await {
|
|
continue;
|
|
}
|
|
let Ok(mut files) = tokio::fs::read_dir(&case_dir).await else {
|
|
continue;
|
|
};
|
|
while let Ok(Some(file)) = files.next_entry().await {
|
|
let path = file.path();
|
|
let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
|
|
continue;
|
|
};
|
|
let Some(version) = parse_input_version(name) else {
|
|
continue;
|
|
};
|
|
if case_dir.join(format!("document_v{version}.md")).exists() {
|
|
continue;
|
|
}
|
|
if tx
|
|
.send(AnalyzeJob {
|
|
case_dir: case_dir.clone(),
|
|
version,
|
|
})
|
|
.is_err()
|
|
{
|
|
warn!(case = %case_dir.display(), "recovery send failed (worker gone)");
|
|
return sent;
|
|
}
|
|
sent += 1;
|
|
}
|
|
}
|
|
sent
|
|
}
|
|
|
|
/// Parse `analysis_input_v{N}.json` and return `N` (as u32) if the pattern
|
|
/// matches exactly. Rejects malformed names, leading zeros, and overflow.
|
|
fn parse_input_version(filename: &str) -> Option<u32> {
|
|
let rest = filename.strip_prefix("analysis_input_v")?;
|
|
let num = rest.strip_suffix(".json")?;
|
|
if num.is_empty() || (num.starts_with('0') && num.len() > 1) {
|
|
return None;
|
|
}
|
|
num.parse::<u32>().ok()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parses_valid_names() {
|
|
assert_eq!(parse_input_version("analysis_input_v1.json"), Some(1));
|
|
assert_eq!(parse_input_version("analysis_input_v42.json"), Some(42));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_names() {
|
|
assert_eq!(parse_input_version("analysis_input_v.json"), None);
|
|
assert_eq!(parse_input_version("analysis_input_v01.json"), None);
|
|
assert_eq!(parse_input_version("analysis_input_vx.json"), None);
|
|
assert_eq!(parse_input_version("document_v1.md"), None);
|
|
assert_eq!(parse_input_version("analysis_input_v1.txt"), None);
|
|
assert_eq!(parse_input_version(""), None);
|
|
}
|
|
}
|