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