use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use solverforge_core::domain::PlanningSolution;
use solverforge_core::score::Score;
use tokio::sync::mpsc;
use crate::stats::SolverTelemetry;
pub const MAX_JOBS: usize = 16;
const SLOT_FREE: u8 = 0;
const SLOT_SOLVING: u8 = 1;
const SLOT_DONE: u8 = 2;
#[derive(Debug)]
pub enum SolverEvent<S: PlanningSolution> {
Progress {
current_score: Option<S::Score>,
best_score: Option<S::Score>,
telemetry: SolverTelemetry,
},
BestSolution {
solution: S,
score: S::Score,
telemetry: SolverTelemetry,
},
Finished {
solution: S,
score: S::Score,
telemetry: SolverTelemetry,
},
}
pub trait Solvable: solverforge_core::domain::PlanningSolution + Send + 'static {
fn solve(
self,
terminate: Option<&AtomicBool>,
sender: mpsc::UnboundedSender<SolverEvent<Self>>,
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SolverStatus {
NotSolving,
Solving,
}
impl SolverStatus {
pub fn as_str(self) -> &'static str {
match self {
SolverStatus::NotSolving => "NOT_SOLVING",
SolverStatus::Solving => "SOLVING",
}
}
}
struct JobSlot {
state: AtomicU8,
terminate: AtomicBool,
}
impl JobSlot {
const fn new() -> Self {
Self {
state: AtomicU8::new(SLOT_FREE),
terminate: AtomicBool::new(false),
}
}
fn reset(&self) {
self.terminate.store(false, Ordering::Release);
self.state.store(SLOT_FREE, Ordering::Release);
}
}
pub struct SolverManager<S: Solvable> {
slots: [JobSlot; MAX_JOBS],
_phantom: std::marker::PhantomData<fn() -> S>,
}
impl<S: Solvable> Default for SolverManager<S> {
fn default() -> Self {
Self::new()
}
}
impl<S: Solvable> SolverManager<S>
where
S::Score: Score,
{
pub const fn new() -> Self {
Self {
slots: [
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
JobSlot::new(),
],
_phantom: std::marker::PhantomData,
}
}
pub fn solve(&'static self, solution: S) -> (usize, mpsc::UnboundedReceiver<SolverEvent<S>>) {
let (sender, receiver) = mpsc::unbounded_channel();
let slot_idx = self
.slots
.iter()
.position(|s| {
s.state
.compare_exchange(SLOT_FREE, SLOT_SOLVING, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
})
.expect("No free job slots available");
let slot = &self.slots[slot_idx];
slot.terminate.store(false, Ordering::SeqCst);
rayon::spawn(move || {
let terminate_ref = &slot.terminate;
solution.solve(Some(terminate_ref), sender);
slot.state.store(SLOT_DONE, Ordering::Release);
});
(slot_idx, receiver)
}
pub fn get_status(&self, job_id: usize) -> SolverStatus {
if job_id >= MAX_JOBS {
return SolverStatus::NotSolving;
}
match self.slots[job_id].state.load(Ordering::Acquire) {
SLOT_SOLVING => SolverStatus::Solving,
_ => SolverStatus::NotSolving,
}
}
pub fn terminate_early(&self, job_id: usize) -> bool {
if job_id >= MAX_JOBS {
return false;
}
let slot = &self.slots[job_id];
if slot.state.load(Ordering::Acquire) == SLOT_SOLVING {
slot.terminate.store(true, Ordering::SeqCst);
true
} else {
false
}
}
pub fn free_slot(&self, job_id: usize) {
if job_id < MAX_JOBS {
self.slots[job_id].reset();
}
}
pub fn active_job_count(&self) -> usize {
self.slots
.iter()
.filter(|s| s.state.load(Ordering::Relaxed) == SLOT_SOLVING)
.count()
}
}