frame-core 0.3.0

Component model, lifecycle, process isolation — hosts components as supervised BEAM process trees
Documentation
use std::sync::Arc;

use beamr::process::ExitReason;
use beamr::scheduler::Scheduler;

use super::{ComponentId, ComponentRecord, ComponentRegistry, RegistryError};
use crate::error::TombstoneKind;
use crate::supervision::observation::{ExitObservation, ObservationError};

/// Honest account of the host-forced cleanup used to recover a Failed tree.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ForcedCleanupReport {
    /// Disposition of every child still retained after the failed drain.
    pub children: Vec<ChildCleanup>,
    /// Disposition of the retained native supervisor, when one existed.
    pub supervisor: Option<ProcessCleanup>,
    /// Monitor failure observed while joining it, when it did not exit cleanly.
    pub monitor_failure: Option<String>,
}

/// Cleanup result for one named child process.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ChildCleanup {
    /// Child key from its retained declaration.
    pub name: String,
    /// Process identifier whose tombstone was observed.
    pub pid: u64,
    /// Whether Frame killed a survivor or found it already dead.
    pub disposition: ProcessCleanup,
}

/// How one process reached a tombstone during failed-tree recovery.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProcessCleanup {
    /// The process was alive, so Frame terminated it and observed this tombstone.
    Killed {
        /// Tombstone observed after host termination.
        tombstone: TombstoneKind,
    },
    /// The process had already exited; Frame observed its existing tombstone.
    AlreadyDead {
        /// Existing tombstone observed without termination.
        tombstone: TombstoneKind,
    },
}

impl ComponentRegistry {
    pub(super) fn recover_failed_tree(
        &self,
        id: ComponentId,
        record: &Arc<ComponentRecord>,
    ) -> Result<Option<ForcedCleanupReport>, RegistryError> {
        let monitor_failure = record
            .tree
            .lock()
            .map_err(|_| RegistryError::SynchronizationPoisoned)?
            .take()
            .and_then(|tree| tree.join_finished(id).err())
            .map(|error| error.to_string());
        let children = record
            .status
            .children
            .lock()
            .map_err(|_| RegistryError::SynchronizationPoisoned)?
            .clone();
        let mut cleaned_children = Vec::with_capacity(children.len());
        for child in children {
            cleaned_children.push(ChildCleanup {
                name: child.name,
                pid: child.pid,
                disposition: cleanup_process(id, &self.scheduler, child.pid, self.config)?,
            });
        }
        record
            .status
            .children
            .lock()
            .map_err(|_| RegistryError::SynchronizationPoisoned)?
            .clear();
        let supervisor_pid = *record
            .status
            .supervisor
            .lock()
            .map_err(|_| RegistryError::SynchronizationPoisoned)?;
        let supervisor = supervisor_pid
            .map(|pid| cleanup_process(id, &self.scheduler, pid, self.config))
            .transpose()?;
        if supervisor.is_some() {
            *record
                .status
                .supervisor
                .lock()
                .map_err(|_| RegistryError::SynchronizationPoisoned)? = None;
        }
        if cleaned_children.is_empty() && supervisor.is_none() && monitor_failure.is_none() {
            Ok(None)
        } else {
            Ok(Some(ForcedCleanupReport {
                children: cleaned_children,
                supervisor,
                monitor_failure,
            }))
        }
    }
}

fn cleanup_process(
    id: ComponentId,
    scheduler: &Arc<Scheduler>,
    pid: u64,
    config: crate::supervision::LifecycleConfig,
) -> Result<ProcessCleanup, RegistryError> {
    let observation = ExitObservation::register(scheduler, pid, config.operation_timeout)
        .map_err(|error| recovery_observation_error(id, pid, error))?;
    let was_alive = scheduler.process_table().get(pid).is_some();
    if was_alive {
        scheduler.terminate_process(pid, ExitReason::Kill);
    }
    let reason = observation
        .wait()
        .map_err(|error| recovery_observation_error(id, pid, error))?;
    let tombstone = tombstone_kind(reason);
    if was_alive {
        Ok(ProcessCleanup::Killed { tombstone })
    } else {
        Ok(ProcessCleanup::AlreadyDead { tombstone })
    }
}

fn recovery_observation_error(id: ComponentId, pid: u64, error: ObservationError) -> RegistryError {
    let detail = match error {
        ObservationError::Spawn(detail) => format!("recovery observer spawn failed: {detail}"),
        ObservationError::ReadyDeadline => "recovery observer readiness timed out".to_owned(),
        ObservationError::Registration(detail) => {
            format!("recovery monitor registration failed: {detail}")
        }
        ObservationError::TombstoneDeadline => {
            format!("timed out observing recovered process {pid} tombstone")
        }
        ObservationError::Protocol(detail) => detail,
        ObservationError::CleanupDeadline => "recovery observer cleanup timed out".to_owned(),
    };
    RegistryError::SupervisorProtocol { id, detail }
}

const fn tombstone_kind(reason: ExitReason) -> TombstoneKind {
    match reason {
        ExitReason::Normal => TombstoneKind::Normal,
        ExitReason::Kill => TombstoneKind::Kill,
        ExitReason::Killed => TombstoneKind::Killed,
        ExitReason::Error => TombstoneKind::Error,
        ExitReason::NoConnection => TombstoneKind::NoConnection,
        ExitReason::NoProc => TombstoneKind::NoProcess,
    }
}