solverforge-solver 0.8.6

Solver engine for SolverForge
Documentation
use std::time::Duration;

use solverforge_core::score::SoftScore;

use super::super::{
    SolverEvent, SolverLifecycleState, SolverManager, SolverManagerError, SolverTerminalReason,
};
use super::lifecycle_solutions::{
    DeleteReservationSolution, LifecycleSolution, PauseOrderingSolution,
    PauseRequestedProgressSolution, TrivialLifecycleSolution,
};

#[test]
fn retained_job_pause_resume_completion_flow() {
    static MANAGER: SolverManager<LifecycleSolution> = SolverManager::new();

    let solution = LifecycleSolution::new(7);
    let gate = solution.gate.clone();
    let (job_id, mut receiver) = MANAGER.solve(solution).expect("job should start");

    match receiver.blocking_recv().expect("best solution event") {
        SolverEvent::BestSolution { metadata, .. } => {
            assert_eq!(metadata.event_sequence, 1);
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Solving);
            assert_eq!(metadata.snapshot_revision, Some(1));
        }
        other => panic!("unexpected event: {other:?}"),
    }

    match receiver.blocking_recv().expect("progress event") {
        SolverEvent::Progress { metadata } => {
            assert_eq!(metadata.event_sequence, 2);
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Solving);
            assert_eq!(metadata.snapshot_revision, Some(1));
        }
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.pause(job_id).expect("pause should be accepted");

    match receiver.blocking_recv().expect("pause requested event") {
        SolverEvent::PauseRequested { metadata } => {
            assert_eq!(metadata.event_sequence, 3);
            assert_eq!(
                metadata.lifecycle_state,
                SolverLifecycleState::PauseRequested
            );
        }
        other => panic!("unexpected event: {other:?}"),
    }

    gate.allow_next_step();

    match receiver.blocking_recv().expect("paused event") {
        SolverEvent::Paused { metadata } => {
            assert_eq!(metadata.event_sequence, 4);
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Paused);
            assert_eq!(metadata.snapshot_revision, Some(2));
        }
        other => panic!("unexpected event: {other:?}"),
    }

    let status = MANAGER.get_status(job_id).expect("status while paused");
    assert_eq!(status.lifecycle_state, SolverLifecycleState::Paused);
    assert!(status.checkpoint_available);
    assert_eq!(status.event_sequence, 4);
    assert_eq!(status.latest_snapshot_revision, Some(2));

    let paused_snapshot = MANAGER.get_snapshot(job_id, None).expect("paused snapshot");
    assert_eq!(
        paused_snapshot.lifecycle_state,
        SolverLifecycleState::Paused
    );
    assert_eq!(paused_snapshot.snapshot_revision, 2);

    let analysis = MANAGER
        .analyze_snapshot(job_id, Some(paused_snapshot.snapshot_revision))
        .expect("analysis for paused snapshot");
    assert_eq!(
        analysis.snapshot_revision,
        paused_snapshot.snapshot_revision
    );
    assert_eq!(analysis.lifecycle_state, SolverLifecycleState::Paused);
    assert_eq!(analysis.analysis.score, SoftScore::of(7));

    MANAGER.resume(job_id).expect("resume should be accepted");

    match receiver.blocking_recv().expect("resumed event") {
        SolverEvent::Resumed { metadata } => {
            assert_eq!(metadata.event_sequence, 5);
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Solving);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    match receiver.blocking_recv().expect("progress after resume") {
        SolverEvent::Progress { metadata } => {
            assert_eq!(metadata.event_sequence, 6);
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Solving);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    match receiver.blocking_recv().expect("completed event") {
        SolverEvent::Completed { metadata, .. } => {
            assert_eq!(metadata.event_sequence, 7);
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Completed);
            assert_eq!(
                metadata.terminal_reason,
                Some(SolverTerminalReason::Completed)
            );
        }
        other => panic!("unexpected event: {other:?}"),
    }

    let status = MANAGER.get_status(job_id).expect("status after completion");
    assert_eq!(status.lifecycle_state, SolverLifecycleState::Completed);
    assert_eq!(
        status.terminal_reason,
        Some(SolverTerminalReason::Completed)
    );
    assert_eq!(status.event_sequence, 7);
    assert!(!status.checkpoint_available);

    MANAGER.delete(job_id).expect("delete terminal job");
    assert!(matches!(
        MANAGER.get_status(job_id),
        Err(SolverManagerError::JobNotFound { .. })
    ));
}

#[test]
fn retained_job_invalid_transitions_cancel_and_delete() {
    static MANAGER: SolverManager<LifecycleSolution> = SolverManager::new();

    let solution = LifecycleSolution::new(3);
    let gate = solution.gate.clone();
    let (job_id, mut receiver) = MANAGER.solve(solution).expect("job should start");

    assert!(matches!(
        MANAGER.resume(job_id),
        Err(SolverManagerError::InvalidStateTransition { action, .. }) if action == "resume"
    ));

    assert!(matches!(
        MANAGER.delete(job_id),
        Err(SolverManagerError::InvalidStateTransition { action, .. }) if action == "delete"
    ));

    match receiver.blocking_recv().expect("best solution event") {
        SolverEvent::BestSolution { .. } => {}
        other => panic!("unexpected event: {other:?}"),
    }
    match receiver.blocking_recv().expect("progress event") {
        SolverEvent::Progress { .. } => {}
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.cancel(job_id).expect("cancel should be accepted");

    gate.allow_next_step();

    match receiver.blocking_recv().expect("cancelled event") {
        SolverEvent::Cancelled { metadata } => {
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Cancelled);
            assert_eq!(
                metadata.terminal_reason,
                Some(SolverTerminalReason::Cancelled)
            );
        }
        other => panic!("unexpected event: {other:?}"),
    }

    let status = MANAGER.get_status(job_id).expect("status after cancel");
    assert_eq!(status.lifecycle_state, SolverLifecycleState::Cancelled);
    assert_eq!(
        status.terminal_reason,
        Some(SolverTerminalReason::Cancelled)
    );

    MANAGER.delete(job_id).expect("delete cancelled job");
    assert!(matches!(
        MANAGER.get_status(job_id),
        Err(SolverManagerError::JobNotFound { .. })
    ));
}

#[test]
fn retained_job_progress_reflects_pause_requested_state() {
    static MANAGER: SolverManager<PauseRequestedProgressSolution> = SolverManager::new();

    let solution = PauseRequestedProgressSolution::new(11);
    let gate = solution.gate.clone();
    let (job_id, mut receiver) = MANAGER.solve(solution).expect("job should start");

    match receiver.blocking_recv().expect("best solution event") {
        SolverEvent::BestSolution { metadata, .. } => {
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Solving);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.pause(job_id).expect("pause should be accepted");

    match receiver.blocking_recv().expect("pause requested event") {
        SolverEvent::PauseRequested { metadata } => {
            assert_eq!(
                metadata.lifecycle_state,
                SolverLifecycleState::PauseRequested
            );
        }
        other => panic!("unexpected event: {other:?}"),
    }

    gate.allow_next_step();

    match receiver.blocking_recv().expect("progress event") {
        SolverEvent::Progress { metadata } => {
            assert_eq!(
                metadata.lifecycle_state,
                SolverLifecycleState::PauseRequested
            );
        }
        other => panic!("unexpected event: {other:?}"),
    }

    match receiver.blocking_recv().expect("paused event") {
        SolverEvent::Paused { metadata } => {
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Paused);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.cancel(job_id).expect("cancel should be accepted");

    match receiver.blocking_recv().expect("cancelled event") {
        SolverEvent::Cancelled { metadata } => {
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Cancelled);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.delete(job_id).expect("delete cancelled job");
}

#[test]
fn retained_job_pause_requested_event_precedes_worker_pause_events() {
    static MANAGER: SolverManager<PauseOrderingSolution> = SolverManager::new();

    let (job_id, mut receiver) = MANAGER
        .solve(PauseOrderingSolution::new(17))
        .expect("job should start");

    match receiver.blocking_recv().expect("best solution event") {
        SolverEvent::BestSolution { metadata, .. } => {
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Solving);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.pause(job_id).expect("pause should be accepted");

    match receiver.blocking_recv().expect("pause requested event") {
        SolverEvent::PauseRequested { metadata } => {
            assert_eq!(metadata.event_sequence, 2);
            assert_eq!(
                metadata.lifecycle_state,
                SolverLifecycleState::PauseRequested
            );
        }
        other => panic!("unexpected event: {other:?}"),
    }

    let mut saw_pause_requested_progress = false;
    loop {
        match receiver
            .blocking_recv()
            .expect("pause lifecycle event after request")
        {
            SolverEvent::Progress { metadata } => {
                saw_pause_requested_progress = true;
                assert_eq!(
                    metadata.lifecycle_state,
                    SolverLifecycleState::PauseRequested
                );
                assert!(metadata.event_sequence > 2);
            }
            SolverEvent::Paused { metadata } => {
                assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Paused);
                break;
            }
            other => panic!("unexpected event: {other:?}"),
        }
    }

    assert!(
        saw_pause_requested_progress,
        "worker should emit pause-requested progress before settling the snapshot"
    );

    MANAGER.cancel(job_id).expect("cancel should be accepted");

    match receiver.blocking_recv().expect("cancelled event") {
        SolverEvent::Cancelled { metadata } => {
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Cancelled);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.delete(job_id).expect("delete cancelled job");
}

#[test]
fn retained_job_delete_keeps_slot_reserved_until_worker_exit() {
    static MANAGER: SolverManager<DeleteReservationSolution> = SolverManager::new();

    let solution = DeleteReservationSolution::new();
    let release_return = solution.release_return.clone();
    let (job_id, mut receiver) = MANAGER.solve(solution).expect("job should start");

    match receiver.blocking_recv().expect("completed event") {
        SolverEvent::Completed { metadata, .. } => {
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Completed);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.delete(job_id).expect("delete completed job");
    assert!(matches!(
        MANAGER.get_status(job_id),
        Err(SolverManagerError::JobNotFound { .. })
    ));
    assert_eq!(MANAGER.active_job_count(), 0);
    assert!(!MANAGER.slot_is_free_for_test(job_id));

    release_return.allow_next_step();

    let deadline = std::time::Instant::now() + Duration::from_secs(1);
    while std::time::Instant::now() < deadline {
        if MANAGER.slot_is_free_for_test(job_id) {
            return;
        }
        std::thread::yield_now();
    }

    panic!("slot {job_id} was not released after the worker exited");
}

#[test]
fn trivial_job_cancelled_while_paused_reports_cancelled() {
    static MANAGER: SolverManager<TrivialLifecycleSolution> = SolverManager::new();

    let solution = TrivialLifecycleSolution::new();
    let gate = solution.gate.clone();
    let (job_id, mut receiver) = MANAGER.solve(solution).expect("job should start");

    MANAGER.pause(job_id).expect("pause should be accepted");

    match receiver.blocking_recv().expect("pause requested event") {
        SolverEvent::PauseRequested { metadata } => {
            assert_eq!(
                metadata.lifecycle_state,
                SolverLifecycleState::PauseRequested
            );
        }
        other => panic!("unexpected event: {other:?}"),
    }

    gate.allow_next_step();

    match receiver.blocking_recv().expect("paused event") {
        SolverEvent::Paused { metadata } => {
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Paused);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.cancel(job_id).expect("cancel should be accepted");

    match receiver.blocking_recv().expect("cancelled event") {
        SolverEvent::Cancelled { metadata } => {
            assert_eq!(metadata.lifecycle_state, SolverLifecycleState::Cancelled);
            assert_eq!(
                metadata.terminal_reason,
                Some(SolverTerminalReason::Cancelled)
            );
        }
        other => panic!("unexpected event: {other:?}"),
    }

    MANAGER.delete(job_id).expect("delete cancelled job");
}