/// Background job system for long-running embedding tasks. /// POST /vectors/index returns a job_id immediately. /// GET /vectors/jobs/{id} returns progress. /// Embedding runs in background via tokio::spawn. use serde::Serialize; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "lowercase")] pub enum JobType { Embed, ProfileActivation, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "lowercase")] pub enum JobStatus { Running, Completed, Failed, } #[derive(Debug, Clone, Serialize)] pub struct Job { pub id: String, #[serde(skip_serializing_if = "Option::is_none")] pub job_type: Option, pub status: JobStatus, pub index_name: Option, pub profile_id: Option, pub total_chunks: usize, #[serde(rename = "processed")] pub processed_alias: usize, pub progress_pct: f32, pub storage_key: Option, pub error: Option, pub started_at: String, pub completed_at: Option, #[serde(skip_serializing_if = "Option::is_none")] pub result: Option, } impl Job { pub fn new_embed(index_name: &str, total_chunks: usize) -> Self { Self { id: format!("job-{}", chrono::Utc::now().timestamp_millis()), job_type: Some(JobType::Embed), status: JobStatus::Running, index_name: Some(index_name.to_string()), profile_id: None, total_chunks, processed_alias: 0, progress_pct: 0.0, storage_key: None, error: None, started_at: chrono::Utc::now().to_rfc3339(), completed_at: None, result: None, } } pub fn new_profile_activation(profile_id: &str) -> Self { Self { id: format!("job-{}", chrono::Utc::now().timestamp_millis()), job_type: Some(JobType::ProfileActivation), status: JobStatus::Running, index_name: None, profile_id: Some(profile_id.to_string()), total_chunks: 0, processed_alias: 0, progress_pct: 0.0, storage_key: None, error: None, started_at: chrono::Utc::now().to_rfc3339(), completed_at: None, result: None, } } } #[derive(Clone)] pub struct JobTracker { jobs: Arc>>, } impl JobTracker { pub fn new() -> Self { Self { jobs: Arc::new(RwLock::new(HashMap::new())), } } pub async fn create_embed(&self, index_name: &str, total_chunks: usize) -> String { let job = Job::new_embed(index_name, total_chunks); let id = job.id.clone(); self.jobs.write().await.insert(id.clone(), job); id } pub async fn create_profile_activation(&self, profile_id: &str) -> String { let job = Job::new_profile_activation(profile_id); let id = job.id.clone(); self.jobs.write().await.insert(id.clone(), job); id } pub async fn update_embed_progress(&self, id: &str, embedded: usize, _rate: f32) { let mut jobs = self.jobs.write().await; if let Some(job) = jobs.get_mut(id) { job.processed_alias = embedded; job.progress_pct = if job.total_chunks > 0 { (embedded as f32 / job.total_chunks as f32) * 100.0 } else { 0.0 }; } } pub async fn complete(&self, id: &str, result: Option) { let mut jobs = self.jobs.write().await; if let Some(job) = jobs.get_mut(id) { job.status = JobStatus::Completed; job.progress_pct = 100.0; job.completed_at = Some(chrono::Utc::now().to_rfc3339()); job.result = result; } } pub async fn fail(&self, id: &str, error: String) { let mut jobs = self.jobs.write().await; if let Some(job) = jobs.get_mut(id) { job.status = JobStatus::Failed; job.error = Some(error); job.completed_at = Some(chrono::Utc::now().to_rfc3339()); } } pub async fn get(&self, id: &str) -> Option { self.jobs.read().await.get(id).cloned() } pub async fn list(&self) -> Vec { self.jobs.read().await.values().cloned().collect() } }