liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! SRV-039: the root question on a server that has Lys and no grant stream.
//!
//! SRV-038's question is built by the grant stream owner and asked at its
//! ready fences. A server that checks grants by live asks alone starts no
//! owner, so without this nothing ever asked whether its network has a root.
//! Here the question is built from the app credential and asked once at
//! start, in the background so start never waits on Lys; never on a timer and
//! never per request. A server with
//! Lys and no credential to ask with says so on `/health` with its cause.

use std::sync::Arc;

use super::root_presence::{LysRoot, RootPresence};
use crate::ServerError;
use crate::config::types::LysConfig;

/// What `/health` holds for the root question.
#[derive(Debug)]
pub enum RootSlot {
    /// The question, asked by the grant stream owner or once at start.
    Asked(Arc<RootPresence>),
    /// A server with Lys that has nothing to ask with.
    Unasked {
        /// The stable cause.
        cause: &'static str,
    },
}

impl RootSlot {
    /// The `lys_root` value and its `lys_root_cause`.
    #[must_use]
    pub fn health(&self) -> (&'static str, Option<&'static str>) {
        match self {
            Self::Asked(root) => {
                let answer = root.answer();
                (answer.health(), answer.cause())
            }
            Self::Unasked { cause } => (LysRoot::NotAsked.health(), Some(cause)),
        }
    }
}

/// Who holds the root question on this server.
#[derive(Debug)]
pub enum RootQuestion {
    /// The grant stream owner builds and asks it (SRV-038).
    Owner,
    /// Asked once at start with the app credential.
    AtStart(Arc<RootPresence>),
    /// Nothing to ask with: `/health` reports `not_asked` with this cause.
    Unasked(&'static str),
}

/// Decides who holds the question for `lys`, building it when this server
/// asks it at start.
///
/// # Errors
/// The app credential cannot be read, or the question cannot be built.
pub fn root_question(
    lys: &LysConfig,
    handle: Option<tokio::runtime::Handle>,
) -> Result<RootQuestion, ServerError> {
    if lys.membership.is_some() || lys.grant_stream.is_some() {
        return Ok(RootQuestion::Owner);
    }
    let Some(path) = lys.app_credential_path.as_ref() else {
        return Ok(RootQuestion::Unasked("no_asking_credential"));
    };
    let Some(handle) = handle else {
        return Ok(RootQuestion::Unasked("no_ask_runtime"));
    };
    let refused = |message: String| ServerError::ConfigValidation { message };
    let credential = std::fs::read_to_string(path).map_err(|error| {
        refused(format!(
            "auth.lys.app_credential_path '{}' cannot be read: {}",
            path.display(),
            error.kind()
        ))
    })?;
    let api = lys.api_url.as_deref().unwrap_or(&lys.issuer);
    let root = RootPresence::new(&lys.network_id, api, credential.trim().to_owned(), handle)
        .map_err(|_| refused("lys_root_question_invalid".to_owned()))?;
    Ok(RootQuestion::AtStart(Arc::new(root)))
}

/// Builds the start question for a server with no grant stream owner, installs
/// what `/health` reports, and asks once in the background, so start never
/// waits on Lys.
///
/// # Errors
/// As [`root_question`], or a second installation on the health handle.
pub fn start(
    lys: &LysConfig,
    health: &crate::health::HealthServerHandle,
    runtime: Option<&tokio::runtime::Runtime>,
) -> Result<(), ServerError> {
    match root_question(lys, runtime.map(|runtime| runtime.handle().clone()))? {
        RootQuestion::Owner => Ok(()),
        RootQuestion::Unasked(cause) => health.install_lys_root(RootSlot::Unasked { cause }),
        RootQuestion::AtStart(root) => {
            health.install_lys_root(RootSlot::Asked(Arc::clone(&root)))?;
            root.ask_in_background();
            Ok(())
        }
    }
}