solverforge_solver/manager/solver_manager/
runtime.rs1use std::any::Any;
2use std::fmt::{self, Debug};
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use solverforge_core::domain::PlanningSolution;
6
7use super::slot::{JobSlot, SLOT_CANCELLED, SLOT_COMPLETED, SLOT_FAILED, SLOT_SOLVING};
8use super::types::{SolverEvent, SolverLifecycleState, SolverTerminalReason};
9use crate::stats::SolverTelemetry;
10
11mod pause;
12
13pub struct SolverPanicPayload {
16 message: String,
17 payload: Box<dyn Any + Send>,
18}
19
20impl SolverPanicPayload {
21 pub fn new(message: impl Into<String>, payload: impl Any + Send) -> Self {
22 Self {
23 message: message.into(),
24 payload: Box::new(payload),
25 }
26 }
27
28 pub fn message(&self) -> &str {
29 &self.message
30 }
31
32 pub fn into_parts(self) -> (String, Box<dyn Any + Send>) {
33 (self.message, self.payload)
34 }
35}
36
37impl Debug for SolverPanicPayload {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 f.debug_struct("SolverPanicPayload")
40 .field("message", &self.message)
41 .finish_non_exhaustive()
42 }
43}
44
45pub struct SolverRuntime<S: PlanningSolution> {
50 job_id: usize,
51 pub(super) slot: &'static JobSlot<S>,
52}
53
54impl<S: PlanningSolution> Clone for SolverRuntime<S> {
55 fn clone(&self) -> Self {
56 *self
57 }
58}
59
60impl<S: PlanningSolution> Copy for SolverRuntime<S> {}
61
62impl<S: PlanningSolution> Debug for SolverRuntime<S> {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.debug_struct("SolverRuntime")
65 .field("job_id", &self.job_id)
66 .finish()
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71enum EventKind {
72 Progress,
73 Resumed,
74 Cancelled,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78enum CompletionDisposition {
79 Published,
80 Pause,
81 Cancel,
82 AlreadyTerminal,
83}
84
85impl<S: PlanningSolution> SolverRuntime<S> {
86 pub(super) fn new(job_id: usize, slot: &'static JobSlot<S>) -> Self {
87 Self { job_id, slot }
88 }
89
90 pub fn detached() -> Self {
97 thread_local! {
103 static DETACHED_SLOT: std::cell::RefCell<Option<&'static dyn std::any::Any>> =
104 const { std::cell::RefCell::new(None) };
105 }
106 let slot: &'static JobSlot<S> = DETACHED_SLOT.with(|cell| {
107 let mut storage = cell.borrow_mut();
108 let stored = storage.get_or_insert_with(|| {
109 let erased: Box<dyn std::any::Any> = Box::new(JobSlot::<S>::new());
110 Box::leak(erased)
111 });
112 match stored.downcast_ref::<JobSlot<S>>() {
113 Some(slot) => slot,
114 None => unreachable!("the detached slot registry stores exactly one type"),
115 }
116 });
117 slot.reset_for_detached_reuse();
118 slot.state.store(SLOT_SOLVING, Ordering::Release);
119 slot.worker_running.store(true, Ordering::Release);
120 Self {
121 job_id: usize::MAX,
122 slot,
123 }
124 }
125
126 pub fn job_id(&self) -> usize {
127 self.job_id
128 }
129
130 pub fn is_cancel_requested(&self) -> bool {
131 self.slot.terminate.load(Ordering::Acquire)
132 }
133
134 pub(crate) fn is_pause_requested(&self) -> bool {
135 self.slot.pause_requested.load(Ordering::Acquire)
136 }
137
138 pub(crate) fn cancel_flag(&self) -> &'static AtomicBool {
139 &self.slot.terminate
140 }
141
142 pub fn emit_progress(
143 &self,
144 current_score: Option<S::Score>,
145 best_score: Option<S::Score>,
146 telemetry: SolverTelemetry,
147 ) {
148 self.emit_non_snapshot_event(current_score, best_score, telemetry, EventKind::Progress);
149 }
150
151 pub fn emit_best_solution(
152 &self,
153 solution: S,
154 current_score: Option<S::Score>,
155 best_score: S::Score,
156 telemetry: SolverTelemetry,
157 ) {
158 self.slot.with_publication(|sender, record| {
159 let state = self.current_state();
160 let terminal_reason = record.terminal_reason;
161 record.current_score = current_score;
162 record.best_score = Some(best_score);
163 record.publish_telemetry(telemetry);
164
165 let snapshot_revision = record.push_snapshot(super::types::SolverSnapshot {
166 job_id: self.job_id,
167 snapshot_revision: 0,
168 lifecycle_state: state,
169 terminal_reason,
170 current_score,
171 best_score: Some(best_score),
172 telemetry: record.telemetry.clone(),
173 solution: solution.clone(),
174 });
175
176 let metadata = record.next_metadata(self.job_id, state, Some(snapshot_revision));
177 if let Some(sender) = sender {
178 let _ = sender.send(SolverEvent::BestSolution { metadata, solution });
179 }
180 });
181 }
182
183 pub fn emit_completed(
184 &self,
185 solution: S,
186 current_score: Option<S::Score>,
187 best_score: S::Score,
188 telemetry: SolverTelemetry,
189 terminal_reason: SolverTerminalReason,
190 ) {
191 loop {
192 let disposition = self.slot.with_publication(|sender, record| {
193 if self.current_state().is_terminal() {
194 return CompletionDisposition::AlreadyTerminal;
195 }
196 if self.is_cancel_requested() {
197 return CompletionDisposition::Cancel;
198 }
199 if self.is_pause_requested()
200 || matches!(
201 self.current_state(),
202 SolverLifecycleState::PauseRequested | SolverLifecycleState::Paused
203 )
204 {
205 return CompletionDisposition::Pause;
206 }
207
208 self.slot.state.store(SLOT_COMPLETED, Ordering::SeqCst);
209 record.terminal_reason = Some(terminal_reason);
210 record.checkpoint_available = false;
211 record.current_score = current_score;
212 record.best_score = Some(best_score);
213 record.publish_telemetry(telemetry.clone());
214
215 let snapshot_revision = record.push_snapshot(super::types::SolverSnapshot {
216 job_id: self.job_id,
217 snapshot_revision: 0,
218 lifecycle_state: SolverLifecycleState::Completed,
219 terminal_reason: Some(terminal_reason),
220 current_score,
221 best_score: Some(best_score),
222 telemetry: record.telemetry.clone(),
223 solution: solution.clone(),
224 });
225
226 let metadata = record.next_metadata(
227 self.job_id,
228 SolverLifecycleState::Completed,
229 Some(snapshot_revision),
230 );
231 if let Some(sender) = sender {
232 let _ = sender.send(SolverEvent::Completed {
233 metadata,
234 solution: solution.clone(),
235 });
236 }
237 CompletionDisposition::Published
238 });
239
240 match disposition {
241 CompletionDisposition::Published | CompletionDisposition::AlreadyTerminal => {
242 return;
243 }
244 CompletionDisposition::Cancel => {
245 self.emit_cancelled(current_score, Some(best_score), telemetry);
246 return;
247 }
248 CompletionDisposition::Pause => {
249 if self.pause_with_snapshot(
250 solution.clone(),
251 current_score,
252 Some(best_score),
253 telemetry.clone(),
254 ) {
255 continue;
256 }
257 if self.is_cancel_requested() {
258 self.emit_cancelled(current_score, Some(best_score), telemetry);
259 return;
260 }
261 }
262 }
263 }
264 }
265
266 pub fn emit_cancelled(
267 &self,
268 current_score: Option<S::Score>,
269 best_score: Option<S::Score>,
270 telemetry: SolverTelemetry,
271 ) {
272 self.emit_non_snapshot_terminal_event(
273 SolverLifecycleState::Cancelled,
274 SolverTerminalReason::Cancelled,
275 current_score,
276 best_score,
277 telemetry,
278 EventKind::Cancelled,
279 );
280 }
281
282 pub fn emit_failed(&self, error: String) {
283 self.slot.with_publication(|sender, record| {
284 if matches!(
285 self.current_state(),
286 SolverLifecycleState::Completed
287 | SolverLifecycleState::Cancelled
288 | SolverLifecycleState::Failed
289 ) {
290 return;
291 }
292 self.slot.state.store(SLOT_FAILED, Ordering::SeqCst);
293 record.terminal_reason = Some(SolverTerminalReason::Failed);
294 record.checkpoint_available = false;
295 record.failure_message = Some(error.clone());
296 record.clear_candidate_trace_detail();
297 let metadata = record.next_metadata(self.job_id, SolverLifecycleState::Failed, None);
298 if let Some(sender) = sender {
299 let _ = sender.send(SolverEvent::Failed { metadata, error });
300 }
301 });
302 }
303
304 pub(crate) fn is_terminal(&self) -> bool {
305 self.current_state().is_terminal()
306 }
307
308 fn current_state(&self) -> SolverLifecycleState {
309 self.slot
310 .raw_state()
311 .expect("runtime accessed a freed job slot")
312 }
313
314 fn emit_non_snapshot_event(
315 &self,
316 current_score: Option<S::Score>,
317 best_score: Option<S::Score>,
318 telemetry: SolverTelemetry,
319 kind: EventKind,
320 ) {
321 self.slot.with_publication(|sender, record| {
322 if kind == EventKind::Resumed && self.is_cancel_requested() {
323 return;
324 }
325 let lifecycle_state = match kind {
326 EventKind::Progress => self.current_state(),
327 EventKind::Resumed => {
328 self.slot.state.store(SLOT_SOLVING, Ordering::SeqCst);
329 SolverLifecycleState::Solving
330 }
331 EventKind::Cancelled => unreachable!(),
332 };
333 record.current_score = current_score;
334 record.best_score = best_score;
335 record.publish_telemetry(telemetry);
336 if lifecycle_state != SolverLifecycleState::Paused {
337 record.checkpoint_available = false;
338 }
339 let metadata = record.next_metadata(self.job_id, lifecycle_state, None);
340 let event = match kind {
341 EventKind::Progress => SolverEvent::Progress { metadata },
342 EventKind::Resumed => SolverEvent::Resumed { metadata },
343 EventKind::Cancelled => unreachable!(),
344 };
345 if let Some(sender) = sender {
346 let _ = sender.send(event);
347 }
348 });
349 }
350
351 fn emit_non_snapshot_terminal_event(
352 &self,
353 lifecycle_state: SolverLifecycleState,
354 terminal_reason: SolverTerminalReason,
355 current_score: Option<S::Score>,
356 best_score: Option<S::Score>,
357 telemetry: SolverTelemetry,
358 kind: EventKind,
359 ) {
360 self.slot.with_publication(|sender, record| {
361 if self.current_state().is_terminal() {
362 return;
363 }
364 match lifecycle_state {
365 SolverLifecycleState::Cancelled => {
366 self.slot.state.store(SLOT_CANCELLED, Ordering::SeqCst);
367 }
368 SolverLifecycleState::Failed => {
369 self.slot.state.store(SLOT_FAILED, Ordering::SeqCst);
370 }
371 _ => {}
372 }
373 record.terminal_reason = Some(terminal_reason);
374 record.checkpoint_available = false;
375 record.current_score = current_score;
376 record.best_score = best_score;
377 record.publish_telemetry(telemetry);
378 let metadata = record.next_metadata(self.job_id, lifecycle_state, None);
379 let event = match kind {
380 EventKind::Cancelled => SolverEvent::Cancelled { metadata },
381 EventKind::Progress | EventKind::Resumed => unreachable!(),
382 };
383 if let Some(sender) = sender {
384 let _ = sender.send(event);
385 }
386 });
387 }
388}
389
390pub(super) fn panic_payload_to_string(payload: Box<dyn Any + Send>) -> String {
391 if let Some(message) = payload.downcast_ref::<&'static str>() {
392 (*message).to_string()
393 } else if let Some(message) = payload.downcast_ref::<String>() {
394 message.clone()
395 } else if let Some(payload) = payload.downcast_ref::<SolverPanicPayload>() {
396 payload.message().to_string()
397 } else {
398 "solver panicked with a non-string payload".to_string()
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::{panic_payload_to_string, SolverPanicPayload};
405
406 #[test]
407 fn retained_failure_uses_foreign_runtime_message() {
408 let payload = SolverPanicPayload::new("foreign traceback", 41_u8);
409 assert_eq!(
410 panic_payload_to_string(Box::new(payload)),
411 "foreign traceback"
412 );
413 }
414
415 #[test]
416 fn foreign_runtime_payload_remains_owned() {
417 let payload = SolverPanicPayload::new("foreign traceback", 41_u8);
418 let (message, payload) = payload.into_parts();
419 assert_eq!(message, "foreign traceback");
420 assert_eq!(*payload.downcast::<u8>().expect("u8 payload"), 41);
421 }
422}