lenso-kernel 0.1.0

Portable Kernel runtime for Lenso vNext applications.
Documentation
use std::{any::Any, cell::RefCell, collections::BTreeMap, rc::Rc};

use futures::FutureExt;
use lenso_app_plan::{
    AppComposition, CapabilityBinding, CapabilityEndpointPlan, CapabilityRequirementPlan,
    ModuleInstancePlan, ResolvedAppPlan,
};
use lenso_kernel::{
    ActivateContext, DeactivateContext, DeactivationReason, DeterministicDriver, InvocationContext,
    Kernel, ModuleLifecycle, ModuleLifecyclePhase, NativeExecutionAdapter, NativeRequestEndpoint,
    PrepareContext, PreparedBinding, PreparedNativeApp, PreparedNativeModule, RuntimeFailure,
};

#[derive(Debug)]
struct LifecycleEndpoint {
    capability: &'static str,
    operation: &'static [&'static str],
}

impl NativeRequestEndpoint for LifecycleEndpoint {
    fn capability_id(&self) -> &'static str {
        self.capability
    }

    fn descriptor_version(&self) -> &'static str {
        "1.0.0"
    }

    fn operations(&self) -> &'static [&'static str] {
        self.operation
    }

    fn invoke(
        &self,
        _operation: &str,
        _request: Box<dyn Any>,
        _context: InvocationContext,
    ) -> futures::future::LocalBoxFuture<
        'static,
        Result<Result<Box<dyn Any>, Box<dyn Any>>, RuntimeFailure>,
    > {
        futures::future::ready(Ok(Ok(Box::new(()) as Box<dyn Any>))).boxed_local()
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
enum Event {
    Prepare(String),
    IngressBound(String),
    Activate(String),
    IngressAccepted(String),
    BackgroundClaimed(String),
    Deactivate(String, DeactivationReason),
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ExternalWorkKind {
    None,
    Ingress,
    Background,
}

#[derive(Debug)]
struct RecordingLifecycle {
    instance_key: String,
    events: Rc<RefCell<Vec<Event>>>,
    external_observations: Rc<RefCell<Vec<bool>>>,
    fail_prepare: bool,
    fail_activate: bool,
    external_work: ExternalWorkKind,
}

impl ModuleLifecycle for RecordingLifecycle {
    fn prepare(&self, context: PrepareContext) -> lenso_kernel::ModuleFuture {
        assert_eq!(context.instance_key(), self.instance_key);
        assert_eq!(context.phase(), ModuleLifecyclePhase::Prepare);
        assert_eq!(context.entrypoint(), format!("{}.entry", self.instance_key));
        assert_eq!(
            context.configuration(),
            format!("config:{}", self.instance_key)
        );
        let events = self.events.clone();
        let instance_key = self.instance_key.clone();
        let fail = self.fail_prepare;
        let ingress = self.external_work == ExternalWorkKind::Ingress;
        Box::pin(async move {
            events
                .borrow_mut()
                .push(Event::Prepare(instance_key.clone()));
            if ingress {
                events
                    .borrow_mut()
                    .push(Event::IngressBound(instance_key.clone()));
            }
            if fail {
                Err(RuntimeFailure::InvalidResolvedPlan {
                    detail: format!("prepare failure for `{instance_key}`"),
                })
            } else {
                Ok(())
            }
        })
    }

    fn activate(&self, context: ActivateContext) -> lenso_kernel::ModuleFuture {
        assert_eq!(context.instance_key(), self.instance_key);
        assert_eq!(context.phase(), ModuleLifecyclePhase::Activate);
        assert!(!context.ready_gate().is_open());
        assert_eq!(context.readiness().phase(), ModuleLifecyclePhase::Ready);

        if self.external_work != ExternalWorkKind::None {
            let readiness = context.readiness();
            let events = self.events.clone();
            let observations = self.external_observations.clone();
            let instance_key = self.instance_key.clone();
            let event = match self.external_work {
                ExternalWorkKind::Ingress => Event::IngressAccepted(instance_key.clone()),
                ExternalWorkKind::Background => Event::BackgroundClaimed(instance_key.clone()),
                ExternalWorkKind::None => unreachable!("None was filtered above"),
            };
            context
                .tasks()
                .spawn_local(Box::pin(async move {
                    readiness.wait().await;
                    observations.borrow_mut().push(readiness.is_open());
                    events.borrow_mut().push(event);
                }))
                .expect("the deterministic Driver should accept Module work");
        }

        let events = self.events.clone();
        let instance_key = self.instance_key.clone();
        let fail = self.fail_activate;
        Box::pin(async move {
            events
                .borrow_mut()
                .push(Event::Activate(instance_key.clone()));
            if fail {
                Err(RuntimeFailure::InvalidResolvedPlan {
                    detail: format!("activate failure for `{instance_key}`"),
                })
            } else {
                Ok(())
            }
        })
    }

    fn deactivate(&self, context: DeactivateContext) -> lenso_kernel::ModuleFuture {
        assert_eq!(context.instance_key(), self.instance_key);
        assert_eq!(context.phase(), ModuleLifecyclePhase::Deactivate);
        assert_eq!(
            context.tasks().task_count(),
            0,
            "startup rollback must terminate generation tasks before deactivation"
        );
        let events = self.events.clone();
        let instance_key = self.instance_key.clone();
        let reason = context.reason();
        Box::pin(async move {
            events
                .borrow_mut()
                .push(Event::Deactivate(instance_key, reason));
            Ok(())
        })
    }
}

#[derive(Debug)]
struct RecordingAdapter {
    modules: BTreeMap<String, Rc<dyn ModuleLifecycle>>,
}

impl NativeExecutionAdapter for RecordingAdapter {
    fn prepare(&self, _plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
        let alpha: Rc<dyn NativeRequestEndpoint> = Rc::new(LifecycleEndpoint {
            capability: "capability.alpha",
            operation: &["alpha.call"],
        });
        let beta: Rc<dyn NativeRequestEndpoint> = Rc::new(LifecycleEndpoint {
            capability: "capability.beta",
            operation: &["beta.call"],
        });
        let generations = self
            .modules
            .iter()
            .map(|(instance_key, lifecycle)| {
                let endpoints = match instance_key.as_str() {
                    "z-provider" => vec![alpha.clone()],
                    "m-provider" => vec![beta.clone()],
                    _ => Vec::new(),
                };
                (
                    instance_key.clone(),
                    PreparedNativeModule::with_lifecycle(endpoints, lifecycle.clone()),
                )
            })
            .collect();
        Ok(PreparedNativeApp::new(
            vec![
                PreparedBinding::new("m-provider", "z-provider", alpha),
                PreparedBinding::new("a-consumer", "m-provider", beta),
            ],
            generations,
        ))
    }
}

fn lifecycle_plan() -> ResolvedAppPlan {
    AppComposition::new(
        vec![
            ModuleInstancePlan::new("z-provider", "package.z")
                .with_entrypoint("z-provider.entry")
                .with_configuration("config:z-provider")
                .with_capability(CapabilityEndpointPlan::new(
                    "capability.alpha",
                    "1.0.0",
                    ["alpha.call"],
                )),
            ModuleInstancePlan::new("m-provider", "package.m")
                .with_entrypoint("m-provider.entry")
                .with_configuration("config:m-provider")
                .with_capability(CapabilityEndpointPlan::new(
                    "capability.beta",
                    "1.0.0",
                    ["beta.call"],
                ))
                .with_requirement(CapabilityRequirementPlan::one("capability.alpha", "1.0.0")),
            ModuleInstancePlan::new("a-consumer", "package.a")
                .with_entrypoint("a-consumer.entry")
                .with_configuration("config:a-consumer")
                .with_requirement(CapabilityRequirementPlan::one("capability.beta", "1.0.0")),
        ],
        vec![
            CapabilityBinding::new("m-provider", "capability.alpha", "1.0.0", "z-provider"),
            CapabilityBinding::new("a-consumer", "capability.beta", "1.0.0", "m-provider"),
        ],
    )
    .resolve()
    .expect("the lifecycle graph should resolve")
}

fn recording_adapter(
    events: &Rc<RefCell<Vec<Event>>>,
    external_observations: &Rc<RefCell<Vec<bool>>>,
    fail_prepare: Option<&str>,
    fail_activate: Option<&str>,
    ingress_instance: Option<&str>,
    background_instance: Option<&str>,
) -> RecordingAdapter {
    let modules = ["z-provider", "m-provider", "a-consumer"]
        .into_iter()
        .map(|instance_key| {
            (
                instance_key.to_owned(),
                Rc::new(RecordingLifecycle {
                    instance_key: instance_key.to_owned(),
                    events: events.clone(),
                    external_observations: external_observations.clone(),
                    fail_prepare: fail_prepare == Some(instance_key),
                    fail_activate: fail_activate == Some(instance_key),
                    external_work: if ingress_instance == Some(instance_key) {
                        ExternalWorkKind::Ingress
                    } else if background_instance == Some(instance_key) {
                        ExternalWorkKind::Background
                    } else {
                        ExternalWorkKind::None
                    },
                }) as Rc<dyn ModuleLifecycle>,
            )
        })
        .collect();
    RecordingAdapter { modules }
}

#[test]
fn successful_startup_prepares_and_activates_in_dependency_order_and_opens_one_gate() {
    let events = Rc::new(RefCell::new(Vec::new()));
    let external_observations = Rc::new(RefCell::new(Vec::new()));
    let driver = DeterministicDriver::new();
    let app = driver
        .run(Kernel::start_native(
            lifecycle_plan(),
            driver.clone(),
            recording_adapter(
                &events,
                &external_observations,
                None,
                None,
                Some("z-provider"),
                Some("m-provider"),
            ),
        ))
        .expect("all Modules should start");

    assert!(app.is_ready());
    assert!(app.ready_gate().is_open());
    assert_eq!(
        *events.borrow(),
        vec![
            Event::Prepare("z-provider".to_owned()),
            Event::IngressBound("z-provider".to_owned()),
            Event::Prepare("m-provider".to_owned()),
            Event::Prepare("a-consumer".to_owned()),
            Event::Activate("z-provider".to_owned()),
            Event::Activate("m-provider".to_owned()),
            Event::Activate("a-consumer".to_owned()),
            Event::IngressAccepted("z-provider".to_owned()),
            Event::BackgroundClaimed("m-provider".to_owned()),
        ]
    );
    assert_eq!(*external_observations.borrow(), vec![true, true]);
}

#[test]
fn prepare_failure_keeps_the_app_not_ready_and_rolls_back_prepared_instances_once() {
    let events = Rc::new(RefCell::new(Vec::new()));
    let external_observations = Rc::new(RefCell::new(Vec::new()));
    let driver = DeterministicDriver::new();
    let outcome = driver.run(Kernel::start_native(
        lifecycle_plan(),
        driver.clone(),
        recording_adapter(
            &events,
            &external_observations,
            Some("m-provider"),
            None,
            Some("z-provider"),
            Some("m-provider"),
        ),
    ));

    assert!(matches!(
        outcome,
        Err(RuntimeFailure::InvalidResolvedPlan { detail })
            if detail.contains("prepare failure for `m-provider`")
    ));
    assert_eq!(
        *events.borrow(),
        vec![
            Event::Prepare("z-provider".to_owned()),
            Event::IngressBound("z-provider".to_owned()),
            Event::Prepare("m-provider".to_owned()),
            Event::Deactivate("m-provider".to_owned(), DeactivationReason::StartupRollback),
            Event::Deactivate("z-provider".to_owned(), DeactivationReason::StartupRollback),
        ]
    );
    assert!(external_observations.borrow().is_empty());
}

#[test]
fn activation_failure_never_opens_readiness_and_unwinds_all_prepared_instances_in_reverse_order() {
    let events = Rc::new(RefCell::new(Vec::new()));
    let external_observations = Rc::new(RefCell::new(Vec::new()));
    let driver = DeterministicDriver::new();
    let outcome = driver.run(Kernel::start_native(
        lifecycle_plan(),
        driver.clone(),
        recording_adapter(
            &events,
            &external_observations,
            None,
            Some("m-provider"),
            Some("z-provider"),
            Some("m-provider"),
        ),
    ));

    assert!(matches!(
        outcome,
        Err(RuntimeFailure::InvalidResolvedPlan { detail })
            if detail.contains("activate failure for `m-provider`")
    ));
    assert_eq!(
        *events.borrow(),
        vec![
            Event::Prepare("z-provider".to_owned()),
            Event::IngressBound("z-provider".to_owned()),
            Event::Prepare("m-provider".to_owned()),
            Event::Prepare("a-consumer".to_owned()),
            Event::Activate("z-provider".to_owned()),
            Event::Activate("m-provider".to_owned()),
            Event::Deactivate("a-consumer".to_owned(), DeactivationReason::StartupRollback),
            Event::Deactivate("m-provider".to_owned(), DeactivationReason::StartupRollback),
            Event::Deactivate("z-provider".to_owned(), DeactivationReason::StartupRollback),
        ]
    );
    assert!(external_observations.borrow().is_empty());
}