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 let slot = Box::leak(Box::new(JobSlot::new()));
98 slot.state.store(SLOT_SOLVING, Ordering::Release);
99 slot.worker_running.store(true, Ordering::Release);
100 Self {
101 job_id: usize::MAX,
102 slot,
103 }
104 }
105
106 pub fn job_id(&self) -> usize {
107 self.job_id
108 }
109
110 pub fn is_cancel_requested(&self) -> bool {
111 self.slot.terminate.load(Ordering::Acquire)
112 }
113
114 pub(crate) fn is_pause_requested(&self) -> bool {
115 self.slot.pause_requested.load(Ordering::Acquire)
116 }
117
118 pub(crate) fn cancel_flag(&self) -> &'static AtomicBool {
119 &self.slot.terminate
120 }
121
122 pub fn emit_progress(
123 &self,
124 current_score: Option<S::Score>,
125 best_score: Option<S::Score>,
126 telemetry: SolverTelemetry,
127 ) {
128 self.emit_non_snapshot_event(current_score, best_score, telemetry, EventKind::Progress);
129 }
130
131 pub fn emit_best_solution(
132 &self,
133 solution: S,
134 current_score: Option<S::Score>,
135 best_score: S::Score,
136 telemetry: SolverTelemetry,
137 ) {
138 self.slot.with_publication(|sender, record| {
139 let state = self.current_state();
140 let terminal_reason = record.terminal_reason;
141 record.current_score = current_score;
142 record.best_score = Some(best_score);
143 record.publish_telemetry(telemetry);
144
145 let snapshot_revision = record.push_snapshot(super::types::SolverSnapshot {
146 job_id: self.job_id,
147 snapshot_revision: 0,
148 lifecycle_state: state,
149 terminal_reason,
150 current_score,
151 best_score: Some(best_score),
152 telemetry: record.telemetry.clone(),
153 solution: solution.clone(),
154 });
155
156 let metadata = record.next_metadata(self.job_id, state, Some(snapshot_revision));
157 if let Some(sender) = sender {
158 let _ = sender.send(SolverEvent::BestSolution { metadata, solution });
159 }
160 });
161 }
162
163 pub fn emit_completed(
164 &self,
165 solution: S,
166 current_score: Option<S::Score>,
167 best_score: S::Score,
168 telemetry: SolverTelemetry,
169 terminal_reason: SolverTerminalReason,
170 ) {
171 loop {
172 let disposition = self.slot.with_publication(|sender, record| {
173 if self.current_state().is_terminal() {
174 return CompletionDisposition::AlreadyTerminal;
175 }
176 if self.is_cancel_requested() {
177 return CompletionDisposition::Cancel;
178 }
179 if self.is_pause_requested()
180 || matches!(
181 self.current_state(),
182 SolverLifecycleState::PauseRequested | SolverLifecycleState::Paused
183 )
184 {
185 return CompletionDisposition::Pause;
186 }
187
188 self.slot.state.store(SLOT_COMPLETED, Ordering::SeqCst);
189 record.terminal_reason = Some(terminal_reason);
190 record.checkpoint_available = false;
191 record.current_score = current_score;
192 record.best_score = Some(best_score);
193 record.publish_telemetry(telemetry.clone());
194
195 let snapshot_revision = record.push_snapshot(super::types::SolverSnapshot {
196 job_id: self.job_id,
197 snapshot_revision: 0,
198 lifecycle_state: SolverLifecycleState::Completed,
199 terminal_reason: Some(terminal_reason),
200 current_score,
201 best_score: Some(best_score),
202 telemetry: record.telemetry.clone(),
203 solution: solution.clone(),
204 });
205
206 let metadata = record.next_metadata(
207 self.job_id,
208 SolverLifecycleState::Completed,
209 Some(snapshot_revision),
210 );
211 if let Some(sender) = sender {
212 let _ = sender.send(SolverEvent::Completed {
213 metadata,
214 solution: solution.clone(),
215 });
216 }
217 CompletionDisposition::Published
218 });
219
220 match disposition {
221 CompletionDisposition::Published | CompletionDisposition::AlreadyTerminal => {
222 return;
223 }
224 CompletionDisposition::Cancel => {
225 self.emit_cancelled(current_score, Some(best_score), telemetry);
226 return;
227 }
228 CompletionDisposition::Pause => {
229 if self.pause_with_snapshot(
230 solution.clone(),
231 current_score,
232 Some(best_score),
233 telemetry.clone(),
234 ) {
235 continue;
236 }
237 if self.is_cancel_requested() {
238 self.emit_cancelled(current_score, Some(best_score), telemetry);
239 return;
240 }
241 }
242 }
243 }
244 }
245
246 pub fn emit_cancelled(
247 &self,
248 current_score: Option<S::Score>,
249 best_score: Option<S::Score>,
250 telemetry: SolverTelemetry,
251 ) {
252 self.emit_non_snapshot_terminal_event(
253 SolverLifecycleState::Cancelled,
254 SolverTerminalReason::Cancelled,
255 current_score,
256 best_score,
257 telemetry,
258 EventKind::Cancelled,
259 );
260 }
261
262 pub fn emit_failed(&self, error: String) {
263 self.slot.with_publication(|sender, record| {
264 if matches!(
265 self.current_state(),
266 SolverLifecycleState::Completed
267 | SolverLifecycleState::Cancelled
268 | SolverLifecycleState::Failed
269 ) {
270 return;
271 }
272 self.slot.state.store(SLOT_FAILED, Ordering::SeqCst);
273 record.terminal_reason = Some(SolverTerminalReason::Failed);
274 record.checkpoint_available = false;
275 record.failure_message = Some(error.clone());
276 record.clear_candidate_trace_detail();
277 let metadata = record.next_metadata(self.job_id, SolverLifecycleState::Failed, None);
278 if let Some(sender) = sender {
279 let _ = sender.send(SolverEvent::Failed { metadata, error });
280 }
281 });
282 }
283
284 pub(crate) fn is_terminal(&self) -> bool {
285 self.current_state().is_terminal()
286 }
287
288 fn current_state(&self) -> SolverLifecycleState {
289 self.slot
290 .raw_state()
291 .expect("runtime accessed a freed job slot")
292 }
293
294 fn emit_non_snapshot_event(
295 &self,
296 current_score: Option<S::Score>,
297 best_score: Option<S::Score>,
298 telemetry: SolverTelemetry,
299 kind: EventKind,
300 ) {
301 self.slot.with_publication(|sender, record| {
302 if kind == EventKind::Resumed && self.is_cancel_requested() {
303 return;
304 }
305 let lifecycle_state = match kind {
306 EventKind::Progress => self.current_state(),
307 EventKind::Resumed => {
308 self.slot.state.store(SLOT_SOLVING, Ordering::SeqCst);
309 SolverLifecycleState::Solving
310 }
311 EventKind::Cancelled => unreachable!(),
312 };
313 record.current_score = current_score;
314 record.best_score = best_score;
315 record.publish_telemetry(telemetry);
316 if lifecycle_state != SolverLifecycleState::Paused {
317 record.checkpoint_available = false;
318 }
319 let metadata = record.next_metadata(self.job_id, lifecycle_state, None);
320 let event = match kind {
321 EventKind::Progress => SolverEvent::Progress { metadata },
322 EventKind::Resumed => SolverEvent::Resumed { metadata },
323 EventKind::Cancelled => unreachable!(),
324 };
325 if let Some(sender) = sender {
326 let _ = sender.send(event);
327 }
328 });
329 }
330
331 fn emit_non_snapshot_terminal_event(
332 &self,
333 lifecycle_state: SolverLifecycleState,
334 terminal_reason: SolverTerminalReason,
335 current_score: Option<S::Score>,
336 best_score: Option<S::Score>,
337 telemetry: SolverTelemetry,
338 kind: EventKind,
339 ) {
340 self.slot.with_publication(|sender, record| {
341 if self.current_state().is_terminal() {
342 return;
343 }
344 match lifecycle_state {
345 SolverLifecycleState::Cancelled => {
346 self.slot.state.store(SLOT_CANCELLED, Ordering::SeqCst);
347 }
348 SolverLifecycleState::Failed => {
349 self.slot.state.store(SLOT_FAILED, Ordering::SeqCst);
350 }
351 _ => {}
352 }
353 record.terminal_reason = Some(terminal_reason);
354 record.checkpoint_available = false;
355 record.current_score = current_score;
356 record.best_score = best_score;
357 record.publish_telemetry(telemetry);
358 let metadata = record.next_metadata(self.job_id, lifecycle_state, None);
359 let event = match kind {
360 EventKind::Cancelled => SolverEvent::Cancelled { metadata },
361 EventKind::Progress | EventKind::Resumed => unreachable!(),
362 };
363 if let Some(sender) = sender {
364 let _ = sender.send(event);
365 }
366 });
367 }
368}
369
370pub(super) fn panic_payload_to_string(payload: Box<dyn Any + Send>) -> String {
371 if let Some(message) = payload.downcast_ref::<&'static str>() {
372 (*message).to_string()
373 } else if let Some(message) = payload.downcast_ref::<String>() {
374 message.clone()
375 } else if let Some(payload) = payload.downcast_ref::<SolverPanicPayload>() {
376 payload.message().to_string()
377 } else {
378 "solver panicked with a non-string payload".to_string()
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::{panic_payload_to_string, SolverPanicPayload};
385
386 #[test]
387 fn retained_failure_uses_foreign_runtime_message() {
388 let payload = SolverPanicPayload::new("foreign traceback", 41_u8);
389 assert_eq!(
390 panic_payload_to_string(Box::new(payload)),
391 "foreign traceback"
392 );
393 }
394
395 #[test]
396 fn foreign_runtime_payload_remains_owned() {
397 let payload = SolverPanicPayload::new("foreign traceback", 41_u8);
398 let (message, payload) = payload.into_parts();
399 assert_eq!(message, "foreign traceback");
400 assert_eq!(*payload.downcast::<u8>().expect("u8 payload"), 41);
401 }
402}