Files
rustytorch/demos/slidescope-shared/src/jobs.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
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]>
2026-04-12 07:01:58 -07:00

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);
}
}