Consistent formatting pass: line wrapping, import sorting, trailing whitespace removal, let-chain indentation, merged derive attributes, and unsafe block reformatting. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
241 lines
6.5 KiB
Rust
241 lines
6.5 KiB
Rust
//! Job management types for processing queue
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::nmf::NmfConfig;
|
|
|
|
/// Status of a processing job
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JobStatus {
|
|
/// Unique job identifier
|
|
pub id: String,
|
|
/// Slide being processed
|
|
pub slide_id: String,
|
|
/// Current job state
|
|
pub state: JobState,
|
|
/// Progress percentage (0-100)
|
|
pub progress: u8,
|
|
/// Current phase description
|
|
pub phase: String,
|
|
/// Unix timestamp when job was queued
|
|
pub queued_at: i64,
|
|
/// Unix timestamp when processing started
|
|
pub started_at: Option<i64>,
|
|
/// Unix timestamp when processing completed
|
|
pub completed_at: Option<i64>,
|
|
/// Error message if failed
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
impl JobStatus {
|
|
/// Create a new queued job
|
|
pub fn new_queued(id: String, slide_id: String) -> Self {
|
|
Self {
|
|
id,
|
|
slide_id,
|
|
state: JobState::Queued,
|
|
progress: 0,
|
|
phase: "Queued".to_string(),
|
|
queued_at: chrono_timestamp(),
|
|
started_at: None,
|
|
completed_at: None,
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
/// Update to running state
|
|
pub fn start(&mut self) {
|
|
self.state = JobState::Running;
|
|
self.started_at = Some(chrono_timestamp());
|
|
}
|
|
|
|
/// Update progress
|
|
pub fn update_progress(&mut self, progress: u8, phase: &str) {
|
|
self.progress = progress.min(100);
|
|
self.phase = phase.to_string();
|
|
}
|
|
|
|
/// Mark as completed
|
|
pub fn complete(&mut self) {
|
|
self.state = JobState::Completed;
|
|
self.progress = 100;
|
|
self.phase = "Completed".to_string();
|
|
self.completed_at = Some(chrono_timestamp());
|
|
}
|
|
|
|
/// Mark as failed
|
|
pub fn fail(&mut self, error: &str) {
|
|
self.state = JobState::Failed;
|
|
self.phase = "Failed".to_string();
|
|
self.error = Some(error.to_string());
|
|
self.completed_at = Some(chrono_timestamp());
|
|
}
|
|
}
|
|
|
|
/// Job execution state
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
#[derive(Default)]
|
|
pub enum JobState {
|
|
/// Job is waiting in queue
|
|
#[default]
|
|
Queued,
|
|
/// Job is currently processing
|
|
Running,
|
|
/// Job completed successfully
|
|
Completed,
|
|
/// Job failed
|
|
Failed,
|
|
}
|
|
|
|
/// Progress update for real-time tracking
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JobProgress {
|
|
/// Job ID
|
|
pub job_id: String,
|
|
/// Progress percentage (0-100)
|
|
pub percent: u8,
|
|
/// Current phase
|
|
pub phase: ProcessingPhase,
|
|
/// Optional message
|
|
pub message: Option<String>,
|
|
}
|
|
|
|
/// Processing phases for NMF workflow
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
#[derive(Default)]
|
|
pub enum ProcessingPhase {
|
|
/// Loading image data
|
|
#[default]
|
|
Loading,
|
|
/// Converting to optical density
|
|
ConvertingOd,
|
|
/// Estimating stain vectors
|
|
EstimatingStains,
|
|
/// Running NMF iterations
|
|
Processing,
|
|
/// Generating output images
|
|
GeneratingOutput,
|
|
/// Saving results
|
|
Saving,
|
|
/// Completed
|
|
Complete,
|
|
}
|
|
|
|
impl ProcessingPhase {
|
|
/// Get typical progress percentage for this phase
|
|
pub fn typical_progress(&self) -> u8 {
|
|
match self {
|
|
Self::Loading => 5,
|
|
Self::ConvertingOd => 10,
|
|
Self::EstimatingStains => 15,
|
|
Self::Processing => 50, // Main processing 15-85%
|
|
Self::GeneratingOutput => 90,
|
|
Self::Saving => 95,
|
|
Self::Complete => 100,
|
|
}
|
|
}
|
|
|
|
/// Get human-readable description
|
|
pub fn description(&self) -> &'static str {
|
|
match self {
|
|
Self::Loading => "Loading image data",
|
|
Self::ConvertingOd => "Converting to optical density",
|
|
Self::EstimatingStains => "Estimating stain vectors",
|
|
Self::Processing => "Running NMF algorithm",
|
|
Self::GeneratingOutput => "Generating stain maps",
|
|
Self::Saving => "Saving results",
|
|
Self::Complete => "Processing complete",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Job request to queue processing
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ProcessingRequest {
|
|
/// Slide to process
|
|
pub slide_id: String,
|
|
/// NMF configuration
|
|
pub config: NmfConfig,
|
|
/// Priority (higher = processed sooner)
|
|
pub priority: u8,
|
|
}
|
|
|
|
impl ProcessingRequest {
|
|
/// Create a new processing request with default priority
|
|
pub fn new(slide_id: String, config: NmfConfig) -> Self {
|
|
Self {
|
|
slide_id,
|
|
config,
|
|
priority: 5, // Default middle priority
|
|
}
|
|
}
|
|
|
|
/// Create a high-priority request
|
|
pub fn high_priority(slide_id: String, config: NmfConfig) -> Self {
|
|
Self {
|
|
slide_id,
|
|
config,
|
|
priority: 10,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get current Unix timestamp
|
|
fn chrono_timestamp() -> i64 {
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_secs() as i64)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_job_status_lifecycle() {
|
|
let mut job = JobStatus::new_queued("job1".to_string(), "slide1".to_string());
|
|
assert_eq!(job.state, JobState::Queued);
|
|
assert_eq!(job.progress, 0);
|
|
|
|
job.start();
|
|
assert_eq!(job.state, JobState::Running);
|
|
assert!(job.started_at.is_some());
|
|
|
|
job.update_progress(50, "Processing");
|
|
assert_eq!(job.progress, 50);
|
|
|
|
job.complete();
|
|
assert_eq!(job.state, JobState::Completed);
|
|
assert_eq!(job.progress, 100);
|
|
}
|
|
|
|
#[test]
|
|
fn test_job_status_failure() {
|
|
let mut job = JobStatus::new_queued("job1".to_string(), "slide1".to_string());
|
|
job.start();
|
|
job.fail("Out of memory");
|
|
|
|
assert_eq!(job.state, JobState::Failed);
|
|
assert_eq!(job.error, Some("Out of memory".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_processing_phase_serialization() {
|
|
let phase = ProcessingPhase::Processing;
|
|
let json = serde_json::to_string(&phase).unwrap();
|
|
assert_eq!(json, "\"processing\"");
|
|
}
|
|
|
|
#[test]
|
|
fn test_processing_request() {
|
|
let request = ProcessingRequest::new("slide1".to_string(), NmfConfig::default());
|
|
assert_eq!(request.priority, 5);
|
|
|
|
let high = ProcessingRequest::high_priority("slide2".to_string(), NmfConfig::default());
|
|
assert_eq!(high.priority, 10);
|
|
}
|
|
}
|