rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

runtime_foundation.rs (16927B)


      1 //! Existing-state-only RHI runtime foundation.
      2 
      3 use core::fmt;
      4 use std::{error::Error, sync::Arc};
      5 
      6 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity};
      7 
      8 use crate::{
      9     RhiConfigDocumentV1, RhiDecryptedIdentity, RhiIdentityEnvelopeBinding, RhiRuntimeAdapters,
     10     RhiRuntimeContext, RhiStateHost, RhiStateMetadata, open_rhi_state_read_write_from_config,
     11 };
     12 
     13 #[cfg(test)]
     14 const RUNTIME_FOUNDATION_CONTRACT: &str =
     15     include_str!("../contracts/services_hardening/runtime_foundation.v1.json");
     16 
     17 /// Exact version of the RHI runtime-foundation contract.
     18 pub const RHI_RUNTIME_FOUNDATION_CONTRACT_VERSION: u32 = 1;
     19 
     20 /// Closed startup conditions required before the RHI service may be ready.
     21 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
     22 pub enum RhiRuntimePrerequisite {
     23     ExistingState,
     24     DurableConfiguration,
     25     VerifiedIdentity,
     26     ReconciliationRecovery,
     27     RequiredSourceConnectivity,
     28     RequiredSourceSubscription,
     29     PublicationRecovery,
     30     AdminListener,
     31     OperationsListener,
     32     PresenceDesiredState,
     33 }
     34 
     35 impl RhiRuntimePrerequisite {
     36     /// Returns the exact machine-contract spelling.
     37     #[must_use]
     38     pub const fn as_str(self) -> &'static str {
     39         match self {
     40             Self::ExistingState => "existing_state",
     41             Self::DurableConfiguration => "durable_configuration",
     42             Self::VerifiedIdentity => "verified_identity",
     43             Self::ReconciliationRecovery => "reconciliation_recovery",
     44             Self::RequiredSourceConnectivity => "required_source_connectivity",
     45             Self::RequiredSourceSubscription => "required_source_subscription",
     46             Self::PublicationRecovery => "publication_recovery",
     47             Self::AdminListener => "admin_listener",
     48             Self::OperationsListener => "operations_listener",
     49             Self::PresenceDesiredState => "presence_desired_state",
     50         }
     51     }
     52 
     53     const fn reason(self) -> RhiRuntimeReadinessReason {
     54         match self {
     55             Self::ExistingState => RhiRuntimeReadinessReason::DatabaseUnavailable,
     56             Self::DurableConfiguration => RhiRuntimeReadinessReason::ConfigurationNotDurable,
     57             Self::VerifiedIdentity => RhiRuntimeReadinessReason::IdentityUnavailable,
     58             Self::ReconciliationRecovery => RhiRuntimeReadinessReason::RecoveryIncomplete,
     59             Self::RequiredSourceConnectivity => RhiRuntimeReadinessReason::SourceUnavailable,
     60             Self::RequiredSourceSubscription => RhiRuntimeReadinessReason::SubscriptionInactive,
     61             Self::PublicationRecovery => RhiRuntimeReadinessReason::PublicationRecoveryIncomplete,
     62             Self::AdminListener => RhiRuntimeReadinessReason::AdminListenerUnavailable,
     63             Self::OperationsListener => RhiRuntimeReadinessReason::OperationsListenerUnavailable,
     64             Self::PresenceDesiredState => RhiRuntimeReadinessReason::PresenceStateUnavailable,
     65         }
     66     }
     67 }
     68 
     69 /// Closed stable reason vocabulary for an unsatisfied prerequisite.
     70 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
     71 pub enum RhiRuntimeReadinessReason {
     72     AdminListenerUnavailable,
     73     ConfigurationNotDurable,
     74     DatabaseUnavailable,
     75     IdentityUnavailable,
     76     OperationsListenerUnavailable,
     77     PresenceStateUnavailable,
     78     PublicationRecoveryIncomplete,
     79     RecoveryIncomplete,
     80     SourceUnavailable,
     81     SubscriptionInactive,
     82 }
     83 
     84 impl RhiRuntimeReadinessReason {
     85     /// Returns the exact machine-contract spelling.
     86     #[must_use]
     87     pub const fn as_str(self) -> &'static str {
     88         match self {
     89             Self::AdminListenerUnavailable => "admin_listener_unavailable",
     90             Self::ConfigurationNotDurable => "configuration_not_durable",
     91             Self::DatabaseUnavailable => "database_unavailable",
     92             Self::IdentityUnavailable => "identity_unavailable",
     93             Self::OperationsListenerUnavailable => "operations_listener_unavailable",
     94             Self::PresenceStateUnavailable => "presence_state_unavailable",
     95             Self::PublicationRecoveryIncomplete => "publication_recovery_incomplete",
     96             Self::RecoveryIncomplete => "recovery_incomplete",
     97             Self::SourceUnavailable => "source_unavailable",
     98             Self::SubscriptionInactive => "subscription_inactive",
     99         }
    100     }
    101 }
    102 
    103 /// Immutable passive readiness evidence derived at startup.
    104 #[derive(Clone, PartialEq, Eq)]
    105 pub struct RhiRuntimeReadiness {
    106     required: Box<[RhiRuntimePrerequisite]>,
    107     satisfied: Box<[RhiRuntimePrerequisite]>,
    108     reasons: Box<[RhiRuntimeReadinessReason]>,
    109 }
    110 
    111 impl RhiRuntimeReadiness {
    112     /// Returns true only after every exact prerequisite is satisfied.
    113     #[must_use]
    114     pub fn is_ready(&self) -> bool {
    115         self.required.len() == self.satisfied.len()
    116             && self
    117                 .required
    118                 .iter()
    119                 .all(|required| self.satisfied.contains(required))
    120     }
    121 
    122     /// Returns the exact ordered prerequisite inventory.
    123     #[must_use]
    124     pub fn required(&self) -> &[RhiRuntimePrerequisite] {
    125         &self.required
    126     }
    127 
    128     /// Returns the exact ordered prerequisites already proven.
    129     #[must_use]
    130     pub fn satisfied(&self) -> &[RhiRuntimePrerequisite] {
    131         &self.satisfied
    132     }
    133 
    134     /// Returns bounded stable reasons for missing prerequisites.
    135     #[must_use]
    136     pub const fn reasons(&self) -> &[RhiRuntimeReadinessReason] {
    137         &self.reasons
    138     }
    139 }
    140 
    141 impl fmt::Debug for RhiRuntimeReadiness {
    142     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    143         formatter
    144             .debug_struct("RhiRuntimeReadiness")
    145             .field("ready", &self.is_ready())
    146             .field("required", &self.required)
    147             .field("satisfied", &self.satisfied)
    148             .field("reasons", &self.reasons)
    149             .finish()
    150     }
    151 }
    152 
    153 /// Stable source-free runtime-foundation failure class.
    154 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    155 pub enum RhiRuntimeFoundationErrorKind {
    156     StateOpen,
    157     IdentityBinding,
    158     IdentityAccess,
    159     Readiness,
    160     TaskFailure,
    161     Close,
    162 }
    163 
    164 impl RhiRuntimeFoundationErrorKind {
    165     /// Returns the stable machine-facing safe code.
    166     #[must_use]
    167     pub const fn code(self) -> &'static str {
    168         match self {
    169             Self::StateOpen => "runtime_state_open_failed",
    170             Self::IdentityBinding => "runtime_identity_binding_invalid",
    171             Self::IdentityAccess => "runtime_identity_access_failed",
    172             Self::Readiness => "runtime_readiness_invalid",
    173             Self::TaskFailure => "runtime_task_failed",
    174             Self::Close => "runtime_close_failed",
    175         }
    176     }
    177 }
    178 
    179 /// One redacted source-free runtime-foundation failure.
    180 #[derive(Clone, Copy, PartialEq, Eq)]
    181 pub struct RhiRuntimeFoundationError {
    182     kind: RhiRuntimeFoundationErrorKind,
    183 }
    184 
    185 impl RhiRuntimeFoundationError {
    186     const fn new(kind: RhiRuntimeFoundationErrorKind) -> Self {
    187         Self { kind }
    188     }
    189 
    190     /// Returns the stable failure kind.
    191     #[must_use]
    192     pub const fn kind(self) -> RhiRuntimeFoundationErrorKind {
    193         self.kind
    194     }
    195 
    196     /// Returns the stable machine-facing safe code.
    197     #[must_use]
    198     pub const fn code(self) -> &'static str {
    199         self.kind.code()
    200     }
    201 }
    202 
    203 impl fmt::Debug for RhiRuntimeFoundationError {
    204     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    205         formatter
    206             .debug_struct("RhiRuntimeFoundationError")
    207             .field("kind", &self.kind)
    208             .finish()
    209     }
    210 }
    211 
    212 impl fmt::Display for RhiRuntimeFoundationError {
    213     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    214         formatter.write_str(match self.kind {
    215             RhiRuntimeFoundationErrorKind::StateOpen => "RHI existing state could not be opened",
    216             RhiRuntimeFoundationErrorKind::IdentityBinding => "RHI identity binding is invalid",
    217             RhiRuntimeFoundationErrorKind::IdentityAccess => "RHI identity startup failed",
    218             RhiRuntimeFoundationErrorKind::Readiness => "RHI readiness prerequisites are invalid",
    219             RhiRuntimeFoundationErrorKind::TaskFailure => "RHI supervised task failed",
    220             RhiRuntimeFoundationErrorKind::Close => "RHI runtime foundation could not close",
    221         })
    222     }
    223 }
    224 
    225 impl Error for RhiRuntimeFoundationError {}
    226 
    227 /// Existing-only RHI foundation with sealed state, identity, adapters, and task ownership.
    228 #[must_use = "the runtime foundation must be shut down so state and tasks are joined"]
    229 pub struct RhiRuntimeFoundation {
    230     runtime: RhiRuntimeContext,
    231     configuration: Arc<RhiConfigDocumentV1>,
    232     metadata: RhiStateMetadata,
    233     state: Arc<RhiStateHost>,
    234     identity: Arc<RhiDecryptedIdentity>,
    235     adapters: RhiRuntimeAdapters,
    236     readiness: RhiRuntimeReadiness,
    237 }
    238 
    239 impl RhiRuntimeFoundation {
    240     /// Returns the immutable canonical instance context.
    241     #[must_use]
    242     pub const fn runtime_context(&self) -> &RhiRuntimeContext {
    243         &self.runtime
    244     }
    245 
    246     /// Returns the admitted immutable configuration.
    247     #[must_use]
    248     pub fn configuration(&self) -> &RhiConfigDocumentV1 {
    249         self.configuration.as_ref()
    250     }
    251 
    252     /// Returns metadata discovered and proven under retained state authority.
    253     #[must_use]
    254     pub const fn metadata(&self) -> &RhiStateMetadata {
    255         &self.metadata
    256     }
    257 
    258     /// Returns passive startup-readiness evidence without performing I/O.
    259     #[must_use]
    260     pub const fn readiness(&self) -> &RhiRuntimeReadiness {
    261         &self.readiness
    262     }
    263 
    264     #[cfg(any(target_os = "linux", target_os = "macos"))]
    265     pub(crate) fn state(&self) -> Arc<RhiStateHost> {
    266         Arc::clone(&self.state)
    267     }
    268 
    269     #[cfg(any(target_os = "linux", target_os = "macos"))]
    270     pub(crate) fn configuration_arc(&self) -> Arc<RhiConfigDocumentV1> {
    271         Arc::clone(&self.configuration)
    272     }
    273 
    274     #[cfg(any(target_os = "linux", target_os = "macos"))]
    275     pub(crate) const fn adapters(&self) -> &RhiRuntimeAdapters {
    276         &self.adapters
    277     }
    278 
    279     #[cfg(any(target_os = "linux", target_os = "macos"))]
    280     pub(crate) fn identity(&self) -> &RhiDecryptedIdentity {
    281         self.identity.as_ref()
    282     }
    283 
    284     #[cfg(any(target_os = "linux", target_os = "macos"))]
    285     pub(crate) fn identity_arc(&self) -> Arc<RhiDecryptedIdentity> {
    286         Arc::clone(&self.identity)
    287     }
    288 
    289     #[cfg(any(target_os = "linux", target_os = "macos"))]
    290     pub(crate) fn supervisor_mut(&mut self) -> &mut radroots_service_host::TaskSupervisor {
    291         self.adapters.supervisor_mut()
    292     }
    293 
    294     /// Requests cancellation, joins owned tasks, and explicitly closes state.
    295     pub async fn shutdown(mut self) -> Result<(), RhiRuntimeFoundationError> {
    296         let supervised = self.adapters.shutdown().await;
    297         let state = Arc::try_unwrap(self.state).map_err(|_| {
    298             RhiRuntimeFoundationError::new(RhiRuntimeFoundationErrorKind::TaskFailure)
    299         })?;
    300         let closed = state.close().await;
    301         if supervised.is_err() {
    302             Err(RhiRuntimeFoundationError::new(
    303                 RhiRuntimeFoundationErrorKind::TaskFailure,
    304             ))
    305         } else if closed.is_err() {
    306             Err(RhiRuntimeFoundationError::new(
    307                 RhiRuntimeFoundationErrorKind::Close,
    308             ))
    309         } else {
    310             Ok(())
    311         }
    312     }
    313 }
    314 
    315 impl fmt::Debug for RhiRuntimeFoundation {
    316     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    317         let _ = &self.identity;
    318         formatter
    319             .debug_struct("RhiRuntimeFoundation")
    320             .field("runtime", &"[redacted]")
    321             .field("configuration", &"[redacted]")
    322             .field("metadata", &"[redacted]")
    323             .field("state", &"[sealed]")
    324             .field("identity", &"[redacted]")
    325             .field("adapters", &"[sealed]")
    326             .field("readiness", &self.readiness)
    327             .finish()
    328     }
    329 }
    330 
    331 /// Opens existing state and composes the non-I/O RHI startup foundation.
    332 ///
    333 /// The durable configuration binding is verified before credential or identity
    334 /// access. No source, subscription, or publication adapter is invoked, and no
    335 /// final service task graph, signal handler, logger, runtime, or process-exit
    336 /// authority is created here.
    337 pub async fn open_rhi_runtime_foundation(
    338     runtime: RhiRuntimeContext,
    339     configuration: RhiConfigDocumentV1,
    340     adapters: RhiRuntimeAdapters,
    341     applied_at: MigrationAppliedAtUnixSeconds,
    342     build: &MigrationBuildIdentity,
    343 ) -> Result<RhiRuntimeFoundation, RhiRuntimeFoundationError> {
    344     let state = open_rhi_state_read_write_from_config(&runtime, &configuration, applied_at, build)
    345         .await
    346         .map_err(|_| RhiRuntimeFoundationError::new(RhiRuntimeFoundationErrorKind::StateOpen))?;
    347     let metadata = state.metadata().clone();
    348     let binding = match RhiIdentityEnvelopeBinding::from_configuration(&configuration, &metadata) {
    349         Ok(binding) => binding,
    350         Err(_) => {
    351             return Err(close_failure(state, RhiRuntimeFoundationErrorKind::IdentityBinding).await);
    352         }
    353     };
    354     let identity = match adapters
    355         .identity_credential()
    356         .open_existing(&runtime, &binding)
    357     {
    358         Ok(identity) => identity,
    359         Err(_) => {
    360             return Err(close_failure(state, RhiRuntimeFoundationErrorKind::IdentityAccess).await);
    361         }
    362     };
    363     let readiness = match startup_readiness(&configuration) {
    364         Ok(readiness) => readiness,
    365         Err(error) => return Err(close_failure(state, error.kind()).await),
    366     };
    367     Ok(RhiRuntimeFoundation {
    368         runtime,
    369         configuration: Arc::new(configuration),
    370         metadata,
    371         state: Arc::new(state),
    372         identity: Arc::new(identity),
    373         adapters,
    374         readiness,
    375     })
    376 }
    377 
    378 async fn close_failure(
    379     state: RhiStateHost,
    380     fallback: RhiRuntimeFoundationErrorKind,
    381 ) -> RhiRuntimeFoundationError {
    382     if state.close().await.is_err() {
    383         RhiRuntimeFoundationError::new(RhiRuntimeFoundationErrorKind::Close)
    384     } else {
    385         RhiRuntimeFoundationError::new(fallback)
    386     }
    387 }
    388 
    389 fn startup_readiness(
    390     configuration: &RhiConfigDocumentV1,
    391 ) -> Result<RhiRuntimeReadiness, RhiRuntimeFoundationError> {
    392     let normalized = configuration.normalized();
    393     let operations_enabled = normalized
    394         .pointer("/operations/enabled")
    395         .and_then(serde_json::Value::as_bool)
    396         .ok_or_else(|| RhiRuntimeFoundationError::new(RhiRuntimeFoundationErrorKind::Readiness))?;
    397     let presence_enabled = normalized
    398         .pointer("/presence/enabled")
    399         .and_then(serde_json::Value::as_bool)
    400         .ok_or_else(|| RhiRuntimeFoundationError::new(RhiRuntimeFoundationErrorKind::Readiness))?;
    401     let mut required = vec![
    402         RhiRuntimePrerequisite::ExistingState,
    403         RhiRuntimePrerequisite::DurableConfiguration,
    404         RhiRuntimePrerequisite::VerifiedIdentity,
    405         RhiRuntimePrerequisite::ReconciliationRecovery,
    406         RhiRuntimePrerequisite::RequiredSourceConnectivity,
    407         RhiRuntimePrerequisite::RequiredSourceSubscription,
    408         RhiRuntimePrerequisite::PublicationRecovery,
    409         RhiRuntimePrerequisite::AdminListener,
    410     ];
    411     if operations_enabled {
    412         required.push(RhiRuntimePrerequisite::OperationsListener);
    413     }
    414     if presence_enabled {
    415         required.push(RhiRuntimePrerequisite::PresenceDesiredState);
    416     }
    417     let satisfied = vec![
    418         RhiRuntimePrerequisite::ExistingState,
    419         RhiRuntimePrerequisite::DurableConfiguration,
    420         RhiRuntimePrerequisite::VerifiedIdentity,
    421     ];
    422     let mut reasons = required
    423         .iter()
    424         .filter(|item| !satisfied.contains(item))
    425         .map(|item| item.reason())
    426         .collect::<Vec<_>>();
    427     reasons.sort_unstable();
    428     reasons.dedup();
    429     Ok(RhiRuntimeReadiness {
    430         required: required.into_boxed_slice(),
    431         satisfied: satisfied.into_boxed_slice(),
    432         reasons: reasons.into_boxed_slice(),
    433     })
    434 }
    435 
    436 #[cfg(test)]
    437 mod tests {
    438     use super::*;
    439 
    440     #[test]
    441     fn contract_is_exact_and_errors_are_source_free() {
    442         let contract: serde_json::Value =
    443             serde_json::from_str(RUNTIME_FOUNDATION_CONTRACT).expect("foundation contract");
    444         assert_eq!(contract["schema_version"], 1);
    445         assert_eq!(contract["state_open"]["initialize_if_missing"], false);
    446         assert_eq!(contract["transport"]["invoked_during_foundation"], false);
    447         for kind in [
    448             RhiRuntimeFoundationErrorKind::StateOpen,
    449             RhiRuntimeFoundationErrorKind::IdentityBinding,
    450             RhiRuntimeFoundationErrorKind::IdentityAccess,
    451             RhiRuntimeFoundationErrorKind::Readiness,
    452             RhiRuntimeFoundationErrorKind::TaskFailure,
    453             RhiRuntimeFoundationErrorKind::Close,
    454         ] {
    455             let error = RhiRuntimeFoundationError::new(kind);
    456             assert!(!error.code().is_empty());
    457             assert!(!error.to_string().is_empty());
    458             assert!(Error::source(&error).is_none());
    459         }
    460     }
    461 }