Documentation
SolverManager
Run retained solver jobs with lifecycle-complete streaming, snapshots, and exact pause/resume.
SolverManager is the retained runtime API for running solver jobs. It owns the
authoritative lifecycle state, streams SolverEvent values, retains snapshots
for later inspection, and exposes exact in-process pause/resume.
Creating a SolverManager
SolverManager::new() is a const fn that takes no arguments. It’s designed to be used as a static:
use solverforge::prelude::*;
static MANAGER: SolverManager<Schedule> = SolverManager::new();
Solving
Call .solve() with your planning solution. It returns a Result containing a
(job_id, Receiver) tuple:
let (job_id, rx) = MANAGER.solve(solution).expect("solver job should start");
The receiver yields SolverEvent<S> values, not raw (solution, score) tuples.
Each event carries SolverEventMetadata with the job id, event sequence,
lifecycle state, telemetry, current/best score, and the latest
snapshot_revision when one exists.
In the current runtime surface, retained telemetry keeps exact generated,
evaluated, accepted, not-doable, acceptor-rejected, forager-ignored,
hard-improving, hard-neutral, hard-worse, conflict-repair, and construction-slot
counts together with generation and evaluation durations. SelectorTelemetry
also carries selector_index and selector_label so local-search and VND
diagnostics can tie those counters back to the active neighborhood.
MoveTelemetry aggregates the same search path by static move label, and the
bounded applied-move trace records the first applied move steps with selected
candidate index, per-step generated/evaluated/accepted counts, ignored accepted
moves, score delta, and hard feasibility before and after the move. Any
displayed moves/s value is derived at the edge rather than stored as the
canonical runtime metric.
PhaseTelemetry identifies the active phase and its local elapsed, step, move,
score-calculation, generation-time, and evaluation-time counters. A generated
count means a candidate was actually yielded to the engine; it does not include
an unrequested logical tail after a cursor or forager short-circuits.
Consume them in a loop:
use solverforge::SolverEvent;
let (job_id, mut rx) = MANAGER.solve(solution).expect("solver job should start");
while let Some(event) = rx.blocking_recv() {
match event {
SolverEvent::Progress { metadata } => {
println!(
"job {} state {:?} current {:?} best {:?}",
metadata.job_id,
metadata.lifecycle_state,
metadata.current_score,
metadata.best_score
);
}
SolverEvent::BestSolution { metadata, .. } => {
println!(
"new best at snapshot {:?}",
metadata.snapshot_revision
);
}
SolverEvent::PauseRequested { metadata } => {
println!("pause requested for job {}", metadata.job_id);
}
SolverEvent::Paused { metadata } => {
println!(
"job {} paused at snapshot {:?}",
metadata.job_id,
metadata.snapshot_revision
);
}
SolverEvent::Resumed { metadata } => {
println!("job {} resumed", metadata.job_id);
}
SolverEvent::Completed { metadata, .. } => {
println!(
"job {} completed with reason {:?}",
metadata.job_id,
metadata.terminal_reason
);
break;
}
SolverEvent::Cancelled { metadata } => {
println!("job {} cancelled", metadata.job_id);
break;
}
SolverEvent::Failed { metadata, error } => {
println!("job {} failed: {}", metadata.job_id, error);
break;
}
}
}
The event variants are:
Progress— telemetry plus lifecycle metadataBestSolution— an owned structurally complete improving solution plus a retained snapshotPauseRequested— pause has been requested but not yet settledPaused— the runtime reached a safe resumable boundary; snapshot metadata is present only after mandatory construction is completeResumed— a paused job continued from its exact retained in-process stateCompleted— the final owned structurally complete best solutionCancelled— the job was explicitly cancelledFailed— the runtime aborted with an error
Candidate Trace Diagnostics
Set [candidate_trace].max_entries in solver.toml to retain a bounded ordered
prefix of candidates pulled by the engine. A trace records the canonical config,
resolved phase plan and execution policy, source/phase/step coordinates, logical
operation identity, and ordered disposition changes such as interruption,
evaluation, rejection, selection, and application. Truncation, incomplete
identity, and provenance status are explicit; a digest alone is not proof of a
complete or qualified run.
Routine SolverEvent metadata, get_status(...), and snapshots intentionally
leave telemetry.candidate_trace empty. Candidate traces can be much larger than
aggregate counters, so request one atomically with
get_telemetry_detail(job_id). The returned SolverTelemetryDetail pairs its
optional trace with a SolverStatus from the same publication instant,
including the exact score and latest snapshot revision. This prevents a caller
from combining a trace from one progress publication with counters from another.
Advanced benchmark or bridge callers can submit externally validated trace
provenance through solve_with_qualified_candidate_trace_provenance(...).
Qualification changes only the diagnostic header; it does not select a
different runner or lifecycle.
Solver Status
Check the current state of a job:
let status = MANAGER.get_status(job_id).expect("job should exist");
println!("state: {:?}", status.lifecycle_state);
println!("terminal reason: {:?}", status.terminal_reason);
println!("checkpoint available: {}", status.checkpoint_available);
println!("event sequence: {}", status.event_sequence);
println!("latest snapshot: {:?}", status.latest_snapshot_revision);
SolverStatus is a struct, not a two-state enum. The lifecycle state is one
of:
SolvingPauseRequestedPausedCompletedCancelledFailed
Terminal jobs also expose a separate terminal_reason:
CompletedTerminatedByConfigCancelledFailed
This distinction matters because a structurally complete job can be Completed
for a normal solve end or for a configured termination condition. If a
configured limit fires before mandatory construction completes, the job is
Failed instead and exposes no partial solution.
Pause, Resume, and Cancel
Use lifecycle controls when you need interactive job management:
MANAGER.pause(job_id).expect("pause should be accepted");
MANAGER.resume(job_id).expect("resume should be accepted");
MANAGER.cancel(job_id).expect("cancel should be accepted");
pause() is not a best-effort hint. The runtime settles it at a safe boundary,
emits Paused, and only then allows resume(). Once mandatory construction is
complete, the pause also retains a checkpoint-backed solution snapshot. Before
that gate opens, the in-process state remains resumable but the Paused event
has no snapshot_revision, checkpoint_available remains false, and
get_snapshot(...) returns no partial construction state.
The built-in construction, local-search, and retained phase flow poll control
state inside large candidate work and again around phase and terminal hooks.
Pending pause or cancellation cannot be overwritten by ordinary default
completion. Long-running work emits metadata-only phase pulses, paused time is
excluded from active phase elapsed telemetry, and pause(), cancel(), or
config termination unwind without app-side watchdog code.
Snapshots and Analysis
Every retained solution snapshot has a monotonic snapshot_revision within its
job. Fetch the latest or a specific revision:
let latest = MANAGER.get_snapshot(job_id, None).expect("latest snapshot");
let exact = MANAGER
.get_snapshot(job_id, Some(latest.snapshot_revision))
.expect("requested snapshot");
Snapshot availability begins only after mandatory construction is complete.
Callers must treat snapshot_revision as optional on lifecycle metadata and
must not assume that a paused job already has a renderable solution.
If your planning solution is Analyzable, you can request score analysis for a
specific snapshot revision:
let analysis = MANAGER
.analyze_snapshot(job_id, Some(latest.snapshot_revision))
.expect("snapshot analysis");
println!("analysis score: {:?}", analysis.analysis.score);
Analysis is snapshot-bound. You do not analyze the live mutable job directly.
After pause() is accepted, PauseRequested is published before any later
worker-side event already carrying PauseRequested state. Treat that ordering
as authoritative when synchronizing UI or service-layer state.
Delete and Slot Reuse
Jobs remain retained after Completed, Cancelled, or Failed so you can read
their final status, snapshots, and analysis. Delete the terminal job when you
are done with it:
MANAGER.delete(job_id).expect("delete terminal job");
Deleting a retained terminal job is what frees the slot for reuse.
Active Jobs
Check how many jobs are currently running:
let count = MANAGER.active_job_count();
This counts visible retained jobs, including paused and terminal jobs that have not been deleted yet.
The Solvable Trait
Your planning solution must implement Solvable to be used with
SolverManager. This is generated automatically when #[planning_solution]
includes a constraints = "..." path.
See Also
- Configuration — Solver configuration
- Termination — Termination conditions