solverforge-fsr / src /solver /service.rs
blackopsrepl's picture
feat(core): scale Bergamo routing model
610cbd2
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};
use solverforge::{
HardSoftScore, SolverEvent, SolverManager, SolverManagerError, SolverSnapshot,
SolverSnapshotAnalysis, SolverStatus,
};
use super::event_payload::{
bootstrap_event_type, bootstrap_snapshot_event_type, event_payload,
snapshot_status_event_payload, status_event_payload,
};
use crate::domain::FieldServicePlan;
// Static manager — must be 'static for retained job execution.
static MANAGER: SolverManager<FieldServicePlan> = SolverManager::new();
struct JobState {
sse_tx: broadcast::Sender<String>,
}
/// Manages retained solving jobs and broadcasts lifecycle-complete SSE payloads.
pub struct SolverService {
jobs: Arc<RwLock<HashMap<usize, JobState>>>,
}
impl SolverService {
pub fn new() -> Self {
Self {
jobs: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn start_job(&self, plan: FieldServicePlan) -> Result<String, SolverManagerError> {
let (job_id, receiver) = MANAGER.solve(plan)?;
let (sse_tx, _) = broadcast::channel(64);
self.jobs.write().insert(
job_id,
JobState {
sse_tx: sse_tx.clone(),
},
);
let jobs = Arc::clone(&self.jobs);
tokio::spawn(async move {
drain_receiver(jobs, job_id, sse_tx, receiver).await;
});
Ok(job_id.to_string())
}
pub fn subscribe(&self, id: &str) -> Option<broadcast::Receiver<String>> {
let job_id = parse_job_id(id).ok()?;
self.jobs
.read()
.get(&job_id)
.map(|state| state.sse_tx.subscribe())
}
pub fn bootstrap_event(&self, id: &str) -> Result<String, SolverManagerError> {
let job_id = parse_job_id(id)?;
let status = MANAGER.get_status(job_id)?;
if let Some(revision) = status.latest_snapshot_revision {
let snapshot = MANAGER.get_snapshot(job_id, Some(revision))?;
return Ok(snapshot_status_event_payload(
job_id,
bootstrap_snapshot_event_type(status.lifecycle_state),
&status,
&snapshot,
));
}
Ok(status_event_payload(
job_id,
bootstrap_event_type(status.lifecycle_state),
&status,
))
}
pub fn get_status(&self, id: &str) -> Result<SolverStatus<HardSoftScore>, SolverManagerError> {
let job_id = parse_job_id(id)?;
MANAGER.get_status(job_id)
}
pub fn pause(&self, id: &str) -> Result<(), SolverManagerError> {
MANAGER.pause(parse_job_id(id)?)
}
pub fn resume(&self, id: &str) -> Result<(), SolverManagerError> {
MANAGER.resume(parse_job_id(id)?)
}
pub fn cancel(&self, id: &str) -> Result<(), SolverManagerError> {
MANAGER.cancel(parse_job_id(id)?)
}
pub fn delete(&self, id: &str) -> Result<(), SolverManagerError> {
let job_id = parse_job_id(id)?;
MANAGER.delete(job_id)?;
self.jobs.write().remove(&job_id);
Ok(())
}
pub fn get_snapshot(
&self,
id: &str,
snapshot_revision: Option<u64>,
) -> Result<SolverSnapshot<FieldServicePlan>, SolverManagerError> {
MANAGER.get_snapshot(parse_job_id(id)?, snapshot_revision)
}
pub fn analyze_snapshot(
&self,
id: &str,
snapshot_revision: Option<u64>,
) -> Result<SolverSnapshotAnalysis<HardSoftScore>, SolverManagerError> {
MANAGER.analyze_snapshot(parse_job_id(id)?, snapshot_revision)
}
}
async fn drain_receiver(
jobs: Arc<RwLock<HashMap<usize, JobState>>>,
job_id: usize,
sse_tx: broadcast::Sender<String>,
mut receiver: mpsc::UnboundedReceiver<SolverEvent<FieldServicePlan>>,
) {
while let Some(event) = receiver.recv().await {
let payload = match &event {
SolverEvent::Progress { metadata } => {
event_payload(job_id, "progress", metadata, None, None)
}
SolverEvent::BestSolution { metadata, solution } => {
event_payload(job_id, "best_solution", metadata, Some(solution), None)
}
SolverEvent::PauseRequested { metadata } => {
event_payload(job_id, "pause_requested", metadata, None, None)
}
SolverEvent::Paused { metadata } => {
event_payload(job_id, "paused", metadata, None, None)
}
SolverEvent::Resumed { metadata } => {
event_payload(job_id, "resumed", metadata, None, None)
}
SolverEvent::Completed { metadata, solution } => {
event_payload(job_id, "completed", metadata, Some(solution), None)
}
SolverEvent::Cancelled { metadata } => {
event_payload(job_id, "cancelled", metadata, None, None)
}
SolverEvent::Failed { metadata, error } => {
event_payload(job_id, "failed", metadata, None, Some(error.as_str()))
}
};
if !jobs.read().contains_key(&job_id) {
return;
}
let _ = sse_tx.send(payload);
}
}
fn parse_job_id(id: &str) -> Result<usize, SolverManagerError> {
id.parse::<usize>()
.map_err(|_| SolverManagerError::JobNotFound { job_id: usize::MAX })
}
impl Default for SolverService {
fn default() -> Self {
Self::new()
}
}