lenso-runner 0.2.1

Native Tokio Runtime Driver and Runner for Lenso vNext.
Documentation
use std::{collections::BTreeMap, time::Duration};

use lenso_app_plan::{ExecutionClassId, PluginCriticality, PluginInstancePlan, ResolvedAppPlan};
use lenso_kernel::{
    ActivateContext, DeactivateContext, DeterministicDriver, ExecutionAdapter,
    ExecutionAdapterCatalog, PluginFuture, PluginLifecycle, PreparedNativeApp,
    PreparedNativePlugin, RuntimeDriver, RuntimeFailure, TerminalOutcome,
};
use lenso_runner::run;

async fn configured_runtime_failure() {
    futures::future::ready(()).await;
    panic!("configured runtime failure");
}

#[derive(Debug)]
struct EmptyAdapter;

impl ExecutionAdapter for EmptyAdapter {
    fn execution_class(&self) -> ExecutionClassId {
        ExecutionClassId::native_rust()
    }

    fn prepare(&self, _plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
        Ok(PreparedNativeApp::empty())
    }
}

#[derive(Debug)]
struct FailingAdapter;

impl ExecutionAdapter for FailingAdapter {
    fn execution_class(&self) -> ExecutionClassId {
        ExecutionClassId::native_rust()
    }

    fn prepare(&self, _plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
        Err(RuntimeFailure::InvalidResolvedPlan {
            detail: "startup failure".to_owned(),
        })
    }
}

#[derive(Clone, Copy, Debug)]
enum CleanupMode {
    Failure,
    Blocked,
}

#[derive(Debug)]
struct RuntimeFailureLifecycle;

impl PluginLifecycle for RuntimeFailureLifecycle {
    fn activate(&self, context: ActivateContext) -> PluginFuture {
        context
            .tasks()
            .spawn_local(Box::pin(configured_runtime_failure()))
            .expect("the failing managed task should be scheduled");
        Box::pin(async { Ok(()) })
    }
}

#[derive(Debug)]
struct CleanupLifecycle(CleanupMode);

impl PluginLifecycle for CleanupLifecycle {
    fn deactivate(&self, _context: DeactivateContext) -> PluginFuture {
        match self.0 {
            CleanupMode::Failure => Box::pin(async {
                Err(RuntimeFailure::Internal {
                    detail: "cleanup failed".to_owned(),
                })
            }),
            CleanupMode::Blocked => Box::pin(async {
                futures::future::pending::<()>().await;
                Ok(())
            }),
        }
    }
}

#[derive(Debug)]
struct RuntimeThenCleanupAdapter(CleanupMode);

impl ExecutionAdapter for RuntimeThenCleanupAdapter {
    fn execution_class(&self) -> ExecutionClassId {
        ExecutionClassId::native_rust()
    }

    fn prepare(&self, _plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
        Ok(PreparedNativeApp::new(
            Vec::new(),
            BTreeMap::from([
                (
                    "cleanup".to_owned(),
                    PreparedNativePlugin::new(Vec::new(), CleanupLifecycle(self.0)),
                ),
                (
                    "failing".to_owned(),
                    PreparedNativePlugin::new(Vec::new(), RuntimeFailureLifecycle),
                ),
            ]),
        ))
    }
}

fn terminal_failure_plan() -> ResolvedAppPlan {
    ResolvedAppPlan::new(
        vec![
            PluginInstancePlan::new("cleanup", "package.cleanup"),
            PluginInstancePlan::new("failing", "package.failing")
                .with_criticality(PluginCriticality::Critical),
        ],
        vec![],
    )
}

#[test]
fn runner_returns_clean_shutdown_after_the_driver_requests_stop() {
    let driver = DeterministicDriver::new();
    let shutdown_driver = driver.clone();
    driver
        .spawn_local(Box::pin(async move {
            shutdown_driver.request_shutdown();
        }))
        .expect("the deterministic Driver should accept the shutdown request");

    let outcome = driver.run(run(
        ResolvedAppPlan::empty(),
        driver.clone(),
        ExecutionAdapterCatalog::single(EmptyAdapter),
        Duration::from_secs(1),
    ));

    assert_eq!(outcome, Ok(TerminalOutcome::CleanShutdown));
}

#[test]
fn runner_returns_startup_failure_without_entering_shutdown() {
    let driver = DeterministicDriver::new();

    let outcome = driver.run(run(
        ResolvedAppPlan::new(vec![PluginInstancePlan::new("plugin", "package")], vec![]),
        driver.clone(),
        ExecutionAdapterCatalog::single(FailingAdapter),
        Duration::from_secs(1),
    ));

    assert!(matches!(
        outcome,
        Ok(TerminalOutcome::StartupFailure {
            error: RuntimeFailure::InvalidResolvedPlan { detail }
        }) if detail == "startup failure"
    ));
}

#[test]
fn runner_preserves_runtime_and_cleanup_failures() {
    let driver = DeterministicDriver::new();

    let outcome = driver.run(run(
        terminal_failure_plan(),
        driver.clone(),
        ExecutionAdapterCatalog::single(RuntimeThenCleanupAdapter(CleanupMode::Failure)),
        Duration::from_secs(1),
    ));

    assert!(matches!(
        outcome,
        Ok(TerminalOutcome::RuntimeFailureDuringShutdown {
            error: RuntimeFailure::PluginRestartExhausted { instance, attempts: 0 },
            cleanup_error: RuntimeFailure::Internal { detail },
        }) if instance == "failing" && detail == "cleanup failed"
    ));
}

#[test]
fn runner_preserves_runtime_failure_when_shutdown_times_out() {
    let driver = DeterministicDriver::new();
    let clock = driver.clone();
    driver
        .spawn_local(Box::pin(async move {
            for _ in 0..8 {
                clock.yield_now().await;
            }
            clock.advance(Duration::from_millis(10));
        }))
        .expect("the deadline clock should be scheduled");

    let outcome = driver.run(run(
        terminal_failure_plan(),
        driver.clone(),
        ExecutionAdapterCatalog::single(RuntimeThenCleanupAdapter(CleanupMode::Blocked)),
        Duration::from_millis(1),
    ));

    assert!(matches!(
        outcome,
        Ok(TerminalOutcome::RuntimeFailureWithShutdownTimeout {
            error: RuntimeFailure::PluginRestartExhausted { instance, attempts: 0 },
        }) if instance == "failing"
    ));
}