saddle-framework 0.2.0

The single business-facing facade for Saddle applications
Documentation
use std::sync::atomic::{AtomicU8, Ordering};

use saddle_admission::AdmissionTrustedCandidateSetOwner;
use saddle_db::internal::VerifiedDatabaseSourceCandidateOwner;
use saddle_observability::VerifiedObservabilitySourceCandidateOwner;
use saddle_runtime::production_fact::VerifiedRuntimeSourceCandidateOwner;
use saddle_service::internal::VerifiedServiceSourceCandidateOwner;

use crate::application::startup_error;

const AVAILABLE: u8 = 0;
const PREPARED: u8 = 1;
const COMMITTED: u8 = 2;
static STATE: AtomicU8 = AtomicU8::new(AVAILABLE);

struct CandidateOwners {
    admission: AdmissionTrustedCandidateSetOwner,
    database: VerifiedDatabaseSourceCandidateOwner,
    service: VerifiedServiceSourceCandidateOwner,
    runtime: VerifiedRuntimeSourceCandidateOwner,
    observability: VerifiedObservabilitySourceCandidateOwner,
}

/// Recoverable failure from the sole 0.2 R11 candidate startup transaction.
#[doc(hidden)]
pub struct StartupCandidateTransactionRejection {
    owners: Option<CandidateOwners>,
}

/// Prepared candidate transaction. Dropping it is fail-closed; callers use
/// `rollback` to recover the exact five inputs for an explicit retry.
///
/// ```compile_fail
/// use saddle::{PreparedStartupCandidateTransaction, StartupCandidateTransactionRejection};
/// fn duplicate(value: PreparedStartupCandidateTransaction) { let _ = value.clone(); }
/// ```
#[doc(hidden)]
pub(crate) struct PreparedStartupCandidateTransaction {
    owners: Option<CandidateOwners>,
}

/// Final, non-replayable candidate owner carried by the production server.
///
/// ```compile_fail
/// let _ = saddle::CommittedStartupCandidateOwner {};
/// ```
#[doc(hidden)]
pub(crate) struct CommittedStartupCandidateOwner {
    _owners: CandidateOwners,
}

impl PreparedStartupCandidateTransaction {
    /// Infallibly commits after every listener-preparation check has passed.
    #[doc(hidden)]
    pub(crate) fn commit(mut self) -> CommittedStartupCandidateOwner {
        let owners = self.owners.take().expect("prepared owners are present");
        if !commit_slot() {
            std::process::abort();
        }
        CommittedStartupCandidateOwner { _owners: owners }
    }
}

/// The only facade entry that combines the current R11 approved set, Admission
/// result and four component-local candidate owners. No raw fact or identity
/// is accepted.
#[doc(hidden)]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::result_large_err)]
fn prepare_startup_candidate_transaction(
    admission: AdmissionTrustedCandidateSetOwner,
    database: VerifiedDatabaseSourceCandidateOwner,
    service: VerifiedServiceSourceCandidateOwner,
    runtime: VerifiedRuntimeSourceCandidateOwner,
    observability: VerifiedObservabilitySourceCandidateOwner,
) -> Result<PreparedStartupCandidateTransaction, StartupCandidateTransactionRejection> {
    prepare(CandidateOwners {
        admission,
        database,
        service,
        runtime,
        observability,
    })
}

#[allow(clippy::result_large_err)]
fn prepare(
    owners: CandidateOwners,
) -> Result<PreparedStartupCandidateTransaction, StartupCandidateTransactionRejection> {
    if !prepare_slot() {
        return Err(StartupCandidateTransactionRejection {
            owners: Some(owners),
        });
    }
    Ok(PreparedStartupCandidateTransaction {
        owners: Some(owners),
    })
}

pub(crate) fn prepare_production_candidate_transaction(
    service_fact: saddle_service::internal::ServiceProductionFactInput,
) -> crate::Result<PreparedStartupCandidateTransaction> {
    let set = saddle_core::verify_trusted_candidate_set(
        saddle_core::approved_trusted_candidate_set_input(),
    )
    .map_err(|_| startup_error("saddle.trusted_candidate_set_rejected"))?;
    let admission = saddle_admission::consume_trusted_candidate_set(set);

    let database = match saddle_db::internal::bind_database_source_candidate(
        saddle_db::internal::database_production_fact_input(),
        saddle_db::internal::database_source_candidate_input(),
    ) {
        Ok(owner) => owner,
        Err(_) => {
            rollback_admission(admission);
            return Err(startup_error("saddle.database_candidate_rejected"));
        }
    };
    let service = match saddle_service::internal::bind_service_source_candidate(
        service_fact,
        saddle_service::internal::service_source_candidate_input(),
    ) {
        Ok(owner) => owner,
        Err(_) => {
            let _ = saddle_db::internal::rollback_database_source_candidate(database);
            rollback_admission(admission);
            return Err(startup_error("saddle.service_candidate_rejected"));
        }
    };
    let runtime = match saddle_runtime::production_fact::bind_runtime_source_candidate(
        saddle_runtime::production_fact::runtime_production_fact_input(),
        saddle_runtime::production_fact::runtime_source_candidate_input(),
    ) {
        Ok(owner) => owner,
        Err(_) => {
            let _ = service.rollback();
            let _ = saddle_db::internal::rollback_database_source_candidate(database);
            rollback_admission(admission);
            return Err(startup_error("saddle.runtime_candidate_rejected"));
        }
    };
    let observability_fact = match saddle_observability::observability_production_fact_input() {
        Ok(fact) => fact,
        Err(_) => {
            let _ = saddle_runtime::production_fact::rollback_runtime_source_candidate(runtime);
            let _ = service.rollback();
            let _ = saddle_db::internal::rollback_database_source_candidate(database);
            rollback_admission(admission);
            return Err(startup_error("saddle.observability_fact_failed"));
        }
    };
    let observability = match saddle_observability::bind_observability_source_candidate(
        observability_fact,
        saddle_observability::observability_source_candidate_input(),
    ) {
        Ok(owner) => owner,
        Err(_) => {
            let _ = saddle_runtime::production_fact::rollback_runtime_source_candidate(runtime);
            let _ = service.rollback();
            let _ = saddle_db::internal::rollback_database_source_candidate(database);
            rollback_admission(admission);
            return Err(startup_error("saddle.observability_candidate_rejected"));
        }
    };
    prepare_startup_candidate_transaction(admission, database, service, runtime, observability)
        .map_err(|failure| {
            drop(failure);
            startup_error("saddle.startup_candidate_replay")
        })
}

fn rollback_admission(admission: AdmissionTrustedCandidateSetOwner) {
    let set = saddle_admission::rollback_trusted_candidate_set_admission(admission);
    let _ = saddle_core::rollback_trusted_candidate_set(set);
}

impl Drop for PreparedStartupCandidateTransaction {
    fn drop(&mut self) {
        if let Some(owners) = self.owners.take() {
            rollback_all(owners);
            rollback_slot();
        }
    }
}

fn prepare_slot() -> bool {
    STATE
        .compare_exchange(AVAILABLE, PREPARED, Ordering::AcqRel, Ordering::Acquire)
        .is_ok()
}

fn rollback_slot() {
    STATE.store(AVAILABLE, Ordering::Release);
}

fn commit_slot() -> bool {
    STATE
        .compare_exchange(PREPARED, COMMITTED, Ordering::AcqRel, Ordering::Acquire)
        .is_ok()
}

impl Drop for StartupCandidateTransactionRejection {
    fn drop(&mut self) {
        if let Some(owners) = self.owners.take() {
            rollback_all(owners);
        }
    }
}

fn rollback_all(owners: CandidateOwners) {
    let CandidateOwners {
        admission,
        database,
        service,
        runtime,
        observability,
    } = owners;
    let _ =
        saddle_observability::VerifiedObservabilitySourceCandidateOwner::rollback(observability);
    let _ = saddle_runtime::production_fact::rollback_runtime_source_candidate(runtime);
    let _ = saddle_service::internal::VerifiedServiceSourceCandidateOwner::rollback(service);
    let _ = saddle_db::internal::rollback_database_source_candidate(database);
    let set = saddle_admission::rollback_trusted_candidate_set_admission(admission);
    let _ = saddle_core::rollback_trusted_candidate_set(set);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn r11_listener_preclosure_slot_rolls_back_and_commit_is_not_replayable() {
        rollback_slot();
        assert!(prepare_slot());
        assert!(!prepare_slot(), "a second prepare cannot partially commit");
        rollback_slot();
        assert!(prepare_slot(), "rollback restores the original slot");
        assert!(commit_slot());
        assert!(!prepare_slot(), "a committed transaction cannot replay");

        // The reset is test-only and prevents this process-global fixture from
        // contaminating unrelated unit tests. Production has no reset path
        // after COMMITTED.
        rollback_slot();
    }
}