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 }