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,
}
#[doc(hidden)]
pub struct StartupCandidateTransactionRejection {
owners: Option<CandidateOwners>,
}
#[doc(hidden)]
pub(crate) struct PreparedStartupCandidateTransaction {
owners: Option<CandidateOwners>,
}
#[doc(hidden)]
pub(crate) struct CommittedStartupCandidateOwner {
_owners: CandidateOwners,
}
impl PreparedStartupCandidateTransaction {
#[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 }
}
}
#[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");
rollback_slot();
}
}