rhi

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

status_v1.rs (39002B)


      1 //! Passive, latest-value Rhi lifecycle and detailed-status publication.
      2 
      3 use core::fmt;
      4 use std::{error::Error, sync::Arc, time::Duration};
      5 
      6 use radroots_service_host::{
      7     BoundedMetricsSnapshot, BuildInfo as HostBuildInfo,
      8     BuildInfoEnvironment as HostBuildInfoEnvironment, BuildMode as HostBuildMode,
      9     CachedServiceState, CachedServiceStatePublisher, CachedServiceStateReader, CommonMetricGroup,
     10     ConfigurationIdentity as HostConfigurationIdentity,
     11     ConfigurationSource as HostConfigurationSource, ContractVersions as HostContractVersions,
     12     InstanceId, IntegrityState as HostIntegrityState, MetricDescriptor, MetricKind, MetricLabel,
     13     MetricLabelKey, MetricName, MetricSample, MetricValue,
     14     PersistenceHealth as HostPersistenceHealth, PersistenceSummary as HostPersistenceSummary,
     15     Readiness as HostReadiness, ReasonCode as HostReasonCode, ReasonCodes as HostReasonCodes,
     16     ServiceId, ServiceOperationalState as HostServiceOperationalState,
     17     ServicePhase as HostServicePhase, ServiceStatus, ServiceStatusDetail,
     18     Sha256Digest as HostSha256Digest, StatusContractError, StatusEncodingError, StatusModelError,
     19     UptimeMillis as HostUptimeMillis, cached_service_state,
     20 };
     21 use serde::Serialize;
     22 
     23 /// Exact version of the passive Rhi status-cache contract.
     24 pub const RHI_STATUS_CACHE_CONTRACT_VERSION: u32 = 1;
     25 
     26 /// Maximum encoded byte length of one detailed Rhi status response.
     27 pub const RHI_DETAILED_STATUS_MAX_UTF8_BYTES: usize =
     28     radroots_service_host::SERVICE_STATUS_MAX_UTF8_BYTES;
     29 
     30 /// Number of stable reason codes admitted by detailed Rhi status.
     31 pub const RHI_STATUS_REASON_CODE_COUNT: usize = 13;
     32 
     33 /// One closed stable source-free status reason code.
     34 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
     35 #[serde(rename_all = "snake_case")]
     36 pub enum RhiStatusReasonCode {
     37     IdentityUnavailable,
     38     DatabaseSchemaMismatch,
     39     DatabaseReadOnly,
     40     DatabaseLowDisk,
     41     SourceUnavailable,
     42     SubscriptionInactive,
     43     RecoveryIncomplete,
     44     PublicationRecoveryIncomplete,
     45     PresenceStateUnavailable,
     46     ReconciliationBacklogExceeded,
     47     AdminListenerFailed,
     48     OperationsListenerFailed,
     49     ShutdownInProgress,
     50 }
     51 
     52 impl RhiStatusReasonCode {
     53     pub fn new(value: impl AsRef<str>) -> Result<Self, RhiStatusError> {
     54         match value.as_ref() {
     55             "identity_unavailable" => Ok(Self::IdentityUnavailable),
     56             "database_schema_mismatch" => Ok(Self::DatabaseSchemaMismatch),
     57             "database_read_only" => Ok(Self::DatabaseReadOnly),
     58             "database_low_disk" => Ok(Self::DatabaseLowDisk),
     59             "source_unavailable" => Ok(Self::SourceUnavailable),
     60             "subscription_inactive" => Ok(Self::SubscriptionInactive),
     61             "recovery_incomplete" => Ok(Self::RecoveryIncomplete),
     62             "publication_recovery_incomplete" => Ok(Self::PublicationRecoveryIncomplete),
     63             "presence_state_unavailable" => Ok(Self::PresenceStateUnavailable),
     64             "reconciliation_backlog_exceeded" => Ok(Self::ReconciliationBacklogExceeded),
     65             "admin_listener_failed" => Ok(Self::AdminListenerFailed),
     66             "operations_listener_failed" => Ok(Self::OperationsListenerFailed),
     67             "shutdown_in_progress" => Ok(Self::ShutdownInProgress),
     68             _ => Err(RhiStatusError::new(RhiStatusErrorKind::InvalidReasonCode)),
     69         }
     70     }
     71 
     72     #[must_use]
     73     pub const fn as_str(self) -> &'static str {
     74         match self {
     75             Self::IdentityUnavailable => "identity_unavailable",
     76             Self::DatabaseSchemaMismatch => "database_schema_mismatch",
     77             Self::DatabaseReadOnly => "database_read_only",
     78             Self::DatabaseLowDisk => "database_low_disk",
     79             Self::SourceUnavailable => "source_unavailable",
     80             Self::SubscriptionInactive => "subscription_inactive",
     81             Self::RecoveryIncomplete => "recovery_incomplete",
     82             Self::PublicationRecoveryIncomplete => "publication_recovery_incomplete",
     83             Self::PresenceStateUnavailable => "presence_state_unavailable",
     84             Self::ReconciliationBacklogExceeded => "reconciliation_backlog_exceeded",
     85             Self::AdminListenerFailed => "admin_listener_failed",
     86             Self::OperationsListenerFailed => "operations_listener_failed",
     87             Self::ShutdownInProgress => "shutdown_in_progress",
     88         }
     89     }
     90 }
     91 
     92 /// Canonically ordered, unique, bounded status reasons.
     93 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
     94 #[serde(transparent)]
     95 pub struct RhiStatusReasonCodes(Vec<RhiStatusReasonCode>);
     96 
     97 impl RhiStatusReasonCodes {
     98     #[must_use]
     99     pub const fn empty() -> Self {
    100         Self(Vec::new())
    101     }
    102 
    103     pub fn new(
    104         values: impl IntoIterator<Item = RhiStatusReasonCode>,
    105     ) -> Result<Self, RhiStatusError> {
    106         let mut bounded = Vec::with_capacity(RHI_STATUS_REASON_CODE_COUNT);
    107         for value in values.into_iter().take(RHI_STATUS_REASON_CODE_COUNT + 1) {
    108             if bounded.len() == RHI_STATUS_REASON_CODE_COUNT {
    109                 return Err(RhiStatusError::new(RhiStatusErrorKind::TooManyReasonCodes));
    110             }
    111             bounded.push(value);
    112         }
    113         bounded.sort_unstable();
    114         bounded.dedup();
    115         Ok(Self(bounded))
    116     }
    117 
    118     #[must_use]
    119     pub fn as_slice(&self) -> &[RhiStatusReasonCode] {
    120         &self.0
    121     }
    122 
    123     fn into_host(self) -> Result<HostReasonCodes, RhiStatusError> {
    124         let values = self
    125             .0
    126             .into_iter()
    127             .map(|value| HostReasonCode::new(value.as_str()).map_err(map_contract_error))
    128             .collect::<Result<Vec<_>, _>>()?;
    129         HostReasonCodes::new(values).map_err(map_contract_error)
    130     }
    131 }
    132 
    133 /// Closed common lifecycle phase used by the Rhi public boundary.
    134 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
    135 #[serde(rename_all = "snake_case")]
    136 pub enum RhiServicePhase {
    137     Starting,
    138     Ready,
    139     Degraded,
    140     Unready,
    141     Stopping,
    142     Failed,
    143 }
    144 
    145 impl RhiServicePhase {
    146     const fn into_host(self) -> HostServicePhase {
    147         match self {
    148             Self::Starting => HostServicePhase::Starting,
    149             Self::Ready => HostServicePhase::Ready,
    150             Self::Degraded => HostServicePhase::Degraded,
    151             Self::Unready => HostServicePhase::Unready,
    152             Self::Stopping => HostServicePhase::Stopping,
    153             Self::Failed => HostServicePhase::Failed,
    154         }
    155     }
    156 
    157     const fn from_host(value: HostServicePhase) -> Self {
    158         match value {
    159             HostServicePhase::Starting => Self::Starting,
    160             HostServicePhase::Ready => Self::Ready,
    161             HostServicePhase::Degraded => Self::Degraded,
    162             HostServicePhase::Unready => Self::Unready,
    163             HostServicePhase::Stopping => Self::Stopping,
    164             HostServicePhase::Failed => Self::Failed,
    165         }
    166     }
    167 }
    168 
    169 /// Build-metadata admission mode for Rhi status identity.
    170 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    171 pub enum RhiStatusBuildMode {
    172     Development,
    173     Release,
    174 }
    175 
    176 /// Complete deterministic build identity retained behind the Rhi boundary.
    177 pub struct RhiStatusBuildInfoV1 {
    178     inner: HostBuildInfo,
    179 }
    180 
    181 impl RhiStatusBuildInfoV1 {
    182     /// Validates the complete build/source-lock identity with fixed Rhi contracts.
    183     pub fn new(
    184         mode: RhiStatusBuildMode,
    185         service_version: Option<&str>,
    186         service_commit: Option<&str>,
    187         lib_revision: Option<&str>,
    188         rust_version: Option<&str>,
    189         target: Option<&str>,
    190         feature_profile: Option<&str>,
    191     ) -> Result<Self, RhiStatusError> {
    192         let contract_versions = HostContractVersions::new(
    193             crate::RHI_CONFIG_SCHEMA_VERSION,
    194             crate::RHI_STATE_SCHEMA_VERSION,
    195             crate::RHI_ADMIN_CONTRACT_VERSION,
    196             crate::RHI_STATUS_CONTRACT_VERSION,
    197             crate::RHI_PROVIDER_CONTRACT_VERSION,
    198         )
    199         .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::InvalidBuildInfo))?;
    200         HostBuildInfo::from_compile_time(
    201             match mode {
    202                 RhiStatusBuildMode::Development => HostBuildMode::Development,
    203                 RhiStatusBuildMode::Release => HostBuildMode::Release,
    204             },
    205             HostBuildInfoEnvironment {
    206                 service_version,
    207                 service_commit,
    208                 lib_revision,
    209                 rust_version,
    210                 target,
    211                 feature_profile,
    212                 contract_versions,
    213             },
    214         )
    215         .map(|inner| Self { inner })
    216         .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::InvalidBuildInfo))
    217     }
    218 }
    219 
    220 impl fmt::Debug for RhiStatusBuildInfoV1 {
    221     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    222         formatter.write_str("RhiStatusBuildInfoV1([redacted])")
    223     }
    224 }
    225 
    226 /// Exact configuration-source vocabulary exposed by detailed status.
    227 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    228 pub enum RhiStatusConfigurationSource {
    229     ExplicitConfig,
    230     DerivedRepoLocal,
    231 }
    232 
    233 /// Safe configuration identity retained behind the Rhi boundary.
    234 pub struct RhiStatusConfigurationIdentityV1 {
    235     inner: HostConfigurationIdentity,
    236 }
    237 
    238 impl RhiStatusConfigurationIdentityV1 {
    239     pub fn new(
    240         digest: impl AsRef<str>,
    241         source: RhiStatusConfigurationSource,
    242     ) -> Result<Self, RhiStatusError> {
    243         let service = ServiceId::new("rhi")
    244             .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::InvalidConfiguration))?;
    245         let digest = HostSha256Digest::new(digest)
    246             .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::InvalidConfiguration))?;
    247         HostConfigurationIdentity::for_service(
    248             &service,
    249             digest,
    250             match source {
    251                 RhiStatusConfigurationSource::ExplicitConfig => {
    252                     HostConfigurationSource::ExplicitConfig
    253                 }
    254                 RhiStatusConfigurationSource::DerivedRepoLocal => {
    255                     HostConfigurationSource::DerivedRepoLocal
    256                 }
    257             },
    258         )
    259         .map(|inner| Self { inner })
    260         .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::InvalidConfiguration))
    261     }
    262 }
    263 
    264 impl fmt::Debug for RhiStatusConfigurationIdentityV1 {
    265     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    266         formatter.write_str("RhiStatusConfigurationIdentityV1([redacted])")
    267     }
    268 }
    269 
    270 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    271 pub enum RhiPersistenceHealthV1 {
    272     Ready,
    273     ReadOnly,
    274     RepairRequired,
    275     Unavailable,
    276 }
    277 
    278 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    279 pub enum RhiIntegrityStateV1 {
    280     Verified,
    281     VerificationRequired,
    282     Failed,
    283 }
    284 
    285 /// Validated persistence summary retained behind the Rhi boundary.
    286 pub struct RhiPersistenceStatusV1 {
    287     inner: HostPersistenceSummary,
    288     ready: bool,
    289 }
    290 
    291 impl RhiPersistenceStatusV1 {
    292     pub fn new(
    293         health: RhiPersistenceHealthV1,
    294         schema_version: u32,
    295         generation: u64,
    296         integrity: RhiIntegrityStateV1,
    297         reason_codes: RhiStatusReasonCodes,
    298     ) -> Result<Self, RhiStatusError> {
    299         let reason_codes = reason_codes.into_host()?;
    300         let ready =
    301             health == RhiPersistenceHealthV1::Ready && integrity == RhiIntegrityStateV1::Verified;
    302         HostPersistenceSummary::new(
    303             match health {
    304                 RhiPersistenceHealthV1::Ready => HostPersistenceHealth::Ready,
    305                 RhiPersistenceHealthV1::ReadOnly => HostPersistenceHealth::ReadOnly,
    306                 RhiPersistenceHealthV1::RepairRequired => HostPersistenceHealth::RepairRequired,
    307                 RhiPersistenceHealthV1::Unavailable => HostPersistenceHealth::Unavailable,
    308             },
    309             schema_version,
    310             generation,
    311             match integrity {
    312                 RhiIntegrityStateV1::Verified => HostIntegrityState::Verified,
    313                 RhiIntegrityStateV1::VerificationRequired => {
    314                     HostIntegrityState::VerificationRequired
    315                 }
    316                 RhiIntegrityStateV1::Failed => HostIntegrityState::Failed,
    317             },
    318             reason_codes,
    319         )
    320         .map(|inner| Self { inner, ready })
    321         .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::InvalidPersistence))
    322     }
    323 }
    324 
    325 impl fmt::Debug for RhiPersistenceStatusV1 {
    326     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    327         formatter.write_str("RhiPersistenceStatusV1([redacted])")
    328     }
    329 }
    330 
    331 /// One validated Unix timestamp used only for an oldest pending work item.
    332 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
    333 #[serde(transparent)]
    334 pub struct RhiStatusUnixSeconds(u64);
    335 
    336 impl RhiStatusUnixSeconds {
    337     /// Constructs a timestamp representable by SQLite and the frozen wire contract.
    338     pub fn new(value: u64) -> Result<Self, RhiStatusError> {
    339         if value > i64::MAX as u64 {
    340             return Err(RhiStatusError::new(RhiStatusErrorKind::InvalidTime));
    341         }
    342         Ok(Self(value))
    343     }
    344 
    345     /// Returns exact whole Unix seconds.
    346     #[must_use]
    347     pub const fn get(self) -> u64 {
    348         self.0
    349     }
    350 }
    351 
    352 /// Passive availability of one configured Rhi identity role.
    353 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
    354 pub struct RhiIdentityHealthV1 {
    355     configured: bool,
    356     available: bool,
    357     reason_codes: RhiStatusReasonCodes,
    358 }
    359 
    360 impl RhiIdentityHealthV1 {
    361     /// Constructs one role observation, rejecting availability without configuration.
    362     pub fn new(
    363         configured: bool,
    364         available: bool,
    365         reason_codes: RhiStatusReasonCodes,
    366     ) -> Result<Self, RhiStatusError> {
    367         if available && !configured {
    368             return Err(RhiStatusError::new(
    369                 RhiStatusErrorKind::InvalidIdentityHealth,
    370             ));
    371         }
    372         Ok(Self {
    373             configured,
    374             available,
    375             reason_codes,
    376         })
    377     }
    378 
    379     #[must_use]
    380     pub const fn is_configured(&self) -> bool {
    381         self.configured
    382     }
    383 
    384     #[must_use]
    385     pub const fn is_available(&self) -> bool {
    386         self.available
    387     }
    388 
    389     #[must_use]
    390     pub const fn reason_codes(&self) -> &RhiStatusReasonCodes {
    391         &self.reason_codes
    392     }
    393 }
    394 
    395 /// Passive projection for the single configured RHI service identity.
    396 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
    397 pub struct RhiProviderStatusV1 {
    398     health: RhiProviderHealthV1,
    399     identity: RhiIdentityHealthV1,
    400     reason_codes: RhiStatusReasonCodes,
    401 }
    402 
    403 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
    404 #[serde(rename_all = "snake_case")]
    405 pub enum RhiProviderHealthV1 {
    406     Ready,
    407     Unavailable,
    408 }
    409 
    410 impl RhiProviderStatusV1 {
    411     /// Derives provider health from the sole configured service identity.
    412     pub fn new(
    413         identity: RhiIdentityHealthV1,
    414         reason_codes: RhiStatusReasonCodes,
    415     ) -> Result<Self, RhiStatusError> {
    416         if !identity.configured {
    417             return Err(RhiStatusError::new(
    418                 RhiStatusErrorKind::InvalidProviderState,
    419             ));
    420         }
    421         let health = if identity.available {
    422             RhiProviderHealthV1::Ready
    423         } else {
    424             RhiProviderHealthV1::Unavailable
    425         };
    426         Ok(Self {
    427             health,
    428             identity,
    429             reason_codes,
    430         })
    431     }
    432 
    433     #[must_use]
    434     pub const fn health(&self) -> RhiProviderHealthV1 {
    435         self.health
    436     }
    437 
    438     #[must_use]
    439     pub const fn identity(&self) -> &RhiIdentityHealthV1 {
    440         &self.identity
    441     }
    442 
    443     #[must_use]
    444     pub const fn reason_codes(&self) -> &RhiStatusReasonCodes {
    445         &self.reason_codes
    446     }
    447 }
    448 
    449 /// Passive evidence-source transport projection for detailed status.
    450 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
    451 pub struct RhiEvidenceTransportStatusV1 {
    452     health: RhiTransportHealthV1,
    453     required_sources_ready: bool,
    454     subscriber_active: bool,
    455     configured_source_count: u64,
    456     reachable_source_count: u64,
    457     reason_codes: RhiStatusReasonCodes,
    458 }
    459 
    460 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
    461 #[serde(rename_all = "snake_case")]
    462 pub enum RhiTransportHealthV1 {
    463     Ready,
    464     Degraded,
    465     Unavailable,
    466 }
    467 
    468 impl RhiEvidenceTransportStatusV1 {
    469     /// Constructs one transport observation, rejecting contradictory ready state.
    470     pub fn new(
    471         health: RhiTransportHealthV1,
    472         required_sources_ready: bool,
    473         subscriber_active: bool,
    474         configured_source_count: u64,
    475         reachable_source_count: u64,
    476         reason_codes: RhiStatusReasonCodes,
    477     ) -> Result<Self, RhiStatusError> {
    478         if reachable_source_count > configured_source_count
    479             || (health == RhiTransportHealthV1::Ready
    480                 && (!required_sources_ready
    481                     || !subscriber_active
    482                     || configured_source_count == 0
    483                     || reachable_source_count == 0))
    484         {
    485             return Err(RhiStatusError::new(
    486                 RhiStatusErrorKind::InvalidTransportState,
    487             ));
    488         }
    489         Ok(Self {
    490             health,
    491             required_sources_ready,
    492             subscriber_active,
    493             configured_source_count,
    494             reachable_source_count,
    495             reason_codes,
    496         })
    497     }
    498 
    499     #[must_use]
    500     pub const fn health(&self) -> RhiTransportHealthV1 {
    501         self.health
    502     }
    503 
    504     #[must_use]
    505     pub const fn required_sources_ready(&self) -> bool {
    506         self.required_sources_ready
    507     }
    508 
    509     #[must_use]
    510     pub const fn subscriber_active(&self) -> bool {
    511         self.subscriber_active
    512     }
    513 
    514     #[must_use]
    515     pub const fn configured_source_count(&self) -> u64 {
    516         self.configured_source_count
    517     }
    518 
    519     #[must_use]
    520     pub const fn reachable_source_count(&self) -> u64 {
    521         self.reachable_source_count
    522     }
    523 
    524     #[must_use]
    525     pub const fn reason_codes(&self) -> &RhiStatusReasonCodes {
    526         &self.reason_codes
    527     }
    528 }
    529 
    530 /// Passive bounded reconciliation-work summary.
    531 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
    532 pub struct RhiReconciliationStatusV1 {
    533     pending: u64,
    534     leased: u64,
    535     exhausted: u64,
    536     #[serde(skip_serializing_if = "Option::is_none")]
    537     oldest_pending_at_utc: Option<RhiStatusUnixSeconds>,
    538 }
    539 
    540 impl RhiReconciliationStatusV1 {
    541     #[must_use]
    542     pub const fn new(
    543         pending: u64,
    544         leased: u64,
    545         exhausted: u64,
    546         oldest_pending_at_utc: Option<RhiStatusUnixSeconds>,
    547     ) -> Self {
    548         Self {
    549             pending,
    550             leased,
    551             exhausted,
    552             oldest_pending_at_utc,
    553         }
    554     }
    555 
    556     #[must_use]
    557     pub const fn pending(self) -> u64 {
    558         self.pending
    559     }
    560 
    561     #[must_use]
    562     pub const fn leased(self) -> u64 {
    563         self.leased
    564     }
    565 
    566     #[must_use]
    567     pub const fn exhausted(self) -> u64 {
    568         self.exhausted
    569     }
    570 
    571     #[must_use]
    572     pub const fn oldest_pending_at_utc(self) -> Option<RhiStatusUnixSeconds> {
    573         self.oldest_pending_at_utc
    574     }
    575 }
    576 
    577 /// Passive publication-outbox summary derived before cache publication.
    578 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
    579 pub struct RhiPublicationStatusV1 {
    580     pending: u64,
    581     unknown: u64,
    582     #[serde(skip_serializing_if = "Option::is_none")]
    583     oldest_pending_at_utc: Option<RhiStatusUnixSeconds>,
    584 }
    585 
    586 impl RhiPublicationStatusV1 {
    587     #[must_use]
    588     pub const fn new(
    589         pending: u64,
    590         unknown: u64,
    591         oldest_pending_at_utc: Option<RhiStatusUnixSeconds>,
    592     ) -> Self {
    593         Self {
    594             pending,
    595             unknown,
    596             oldest_pending_at_utc,
    597         }
    598     }
    599 
    600     #[must_use]
    601     pub const fn pending(self) -> u64 {
    602         self.pending
    603     }
    604 
    605     #[must_use]
    606     pub const fn unknown(self) -> u64 {
    607         self.unknown
    608     }
    609 
    610     #[must_use]
    611     pub const fn oldest_pending_at_utc(self) -> Option<RhiStatusUnixSeconds> {
    612         self.oldest_pending_at_utc
    613     }
    614 }
    615 
    616 /// Passive desired-presence publication summary.
    617 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
    618 pub struct RhiPresenceStatusV1 {
    619     pending: u64,
    620     unknown: u64,
    621 }
    622 
    623 impl RhiPresenceStatusV1 {
    624     #[must_use]
    625     pub const fn new(pending: u64, unknown: u64) -> Self {
    626         Self { pending, unknown }
    627     }
    628 
    629     #[must_use]
    630     pub const fn pending(self) -> u64 {
    631         self.pending
    632     }
    633 
    634     #[must_use]
    635     pub const fn unknown(self) -> u64 {
    636         self.unknown
    637     }
    638 }
    639 
    640 #[derive(Serialize)]
    641 struct RhiStatusDetailV1 {
    642     identity: RhiIdentityHealthV1,
    643     reconciliation: RhiReconciliationStatusV1,
    644     publication: RhiPublicationStatusV1,
    645     presence: RhiPresenceStatusV1,
    646 }
    647 
    648 impl ServiceStatusDetail for RhiStatusDetailV1 {
    649     type Provider = RhiProviderStatusV1;
    650     type Transport = RhiEvidenceTransportStatusV1;
    651 
    652     const FIELD_NAME: &'static str = "rhi";
    653 }
    654 
    655 /// Validated common fields shared by one detailed status publication.
    656 ///
    657 /// Construction and publication perform validation and bounded encoding only.
    658 /// They do not query SQLite, providers, sources, relays, DNS, credentials, or the clock.
    659 pub struct RhiStatusCommonV1 {
    660     operational: HostServiceOperationalState,
    661     uptime: HostUptimeMillis,
    662     build: HostBuildInfo,
    663     configuration: HostConfigurationIdentity,
    664     persistence: HostPersistenceSummary,
    665     persistence_ready: bool,
    666 }
    667 
    668 impl RhiStatusCommonV1 {
    669     /// Validates the common lifecycle and detailed-status envelope fields.
    670     pub fn new(
    671         phase: RhiServicePhase,
    672         ready: bool,
    673         reason_codes: RhiStatusReasonCodes,
    674         uptime_millis: u64,
    675         build: RhiStatusBuildInfoV1,
    676         configuration: RhiStatusConfigurationIdentityV1,
    677         persistence: RhiPersistenceStatusV1,
    678     ) -> Result<Self, RhiStatusError> {
    679         let operational = HostServiceOperationalState::new(
    680             phase.into_host(),
    681             if ready {
    682                 HostReadiness::READY
    683             } else {
    684                 HostReadiness::NOT_READY
    685             },
    686             reason_codes.into_host()?,
    687         )
    688         .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::InvalidLifecycle))?;
    689         let uptime = HostUptimeMillis::from_duration(Duration::from_millis(uptime_millis))
    690             .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::InvalidTime))?;
    691         Ok(Self {
    692             operational,
    693             uptime,
    694             build: build.inner,
    695             configuration: configuration.inner,
    696             persistence: persistence.inner,
    697             persistence_ready: persistence.ready,
    698         })
    699     }
    700 }
    701 
    702 impl fmt::Debug for RhiStatusCommonV1 {
    703     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    704         formatter.write_str("RhiStatusCommonV1([redacted])")
    705     }
    706 }
    707 
    708 /// One complete, already-observed status publication input.
    709 pub struct RhiStatusObservationV1 {
    710     common: RhiStatusCommonV1,
    711     provider: RhiProviderStatusV1,
    712     transport: RhiEvidenceTransportStatusV1,
    713     reconciliation: RhiReconciliationStatusV1,
    714     publication: RhiPublicationStatusV1,
    715     presence: RhiPresenceStatusV1,
    716 }
    717 
    718 impl RhiStatusObservationV1 {
    719     #[must_use]
    720     pub fn new(
    721         common: RhiStatusCommonV1,
    722         provider: RhiProviderStatusV1,
    723         transport: RhiEvidenceTransportStatusV1,
    724         reconciliation: RhiReconciliationStatusV1,
    725         publication: RhiPublicationStatusV1,
    726         presence: RhiPresenceStatusV1,
    727     ) -> Self {
    728         Self {
    729             common,
    730             provider,
    731             transport,
    732             reconciliation,
    733             publication,
    734             presence,
    735         }
    736     }
    737 
    738     fn into_cached(self, instance: &InstanceId) -> Result<PreparedRhiStatus, RhiStatusError> {
    739         let operational = self.common.operational.clone();
    740         if operational.readiness().is_ready()
    741             && (!self.common.persistence_ready
    742                 || self.provider.health != RhiProviderHealthV1::Ready
    743                 || !self.transport.required_sources_ready
    744                 || !self.transport.subscriber_active)
    745         {
    746             return Err(RhiStatusError::new(RhiStatusErrorKind::InvalidLifecycle));
    747         }
    748         if operational.phase() == HostServicePhase::Ready
    749             && self.transport.health != RhiTransportHealthV1::Ready
    750         {
    751             return Err(RhiStatusError::new(RhiStatusErrorKind::InvalidLifecycle));
    752         }
    753         let operations_metrics = bounded_operations_metrics(&operational)?;
    754         let detail = RhiStatusDetailV1 {
    755             identity: self.provider.identity.clone(),
    756             reconciliation: self.reconciliation,
    757             publication: self.publication,
    758             presence: self.presence,
    759         };
    760         let service = ServiceId::new("rhi")
    761             .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::InvalidModel))?;
    762         let status = ServiceStatus::new(
    763             service,
    764             instance.clone(),
    765             self.common.operational,
    766             self.common.uptime,
    767             self.common.build,
    768             self.common.configuration,
    769             self.common.persistence,
    770             self.provider,
    771             self.transport,
    772             detail,
    773         )
    774         .map_err(map_model_error)?;
    775         let json = status.to_bounded_json().map_err(map_encoding_error)?;
    776         Ok(PreparedRhiStatus {
    777             detail: CachedServiceState::new(
    778                 operational.clone(),
    779                 RhiCachedStatus {
    780                     json: json.into_boxed_slice(),
    781                 },
    782             ),
    783             operations: CachedServiceState::new(operational, operations_metrics),
    784         })
    785     }
    786 }
    787 
    788 impl fmt::Debug for RhiStatusObservationV1 {
    789     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    790         formatter.write_str("RhiStatusObservationV1([redacted])")
    791     }
    792 }
    793 
    794 struct RhiCachedStatus {
    795     json: Box<[u8]>,
    796 }
    797 
    798 struct PreparedRhiStatus {
    799     detail: CachedServiceState<RhiCachedStatus>,
    800     operations: CachedServiceState<BoundedMetricsSnapshot>,
    801 }
    802 
    803 fn bounded_operations_metrics(
    804     operational: &HostServiceOperationalState,
    805 ) -> Result<BoundedMetricsSnapshot, RhiStatusError> {
    806     let phase_name = MetricName::new("radroots_rhi_service_phase").map_err(map_metrics_error)?;
    807     let ready_name = MetricName::new("radroots_rhi_service_ready").map_err(map_metrics_error)?;
    808     let descriptors = [
    809         MetricDescriptor::new(
    810             CommonMetricGroup::Phase,
    811             phase_name.clone(),
    812             "Current cached Rhi service phase.",
    813             MetricKind::Gauge,
    814             [MetricLabelKey::Phase],
    815         )
    816         .map_err(map_metrics_error)?,
    817         MetricDescriptor::new(
    818             CommonMetricGroup::Phase,
    819             ready_name.clone(),
    820             "Current cached Rhi readiness bit.",
    821             MetricKind::Gauge,
    822             [],
    823         )
    824         .map_err(map_metrics_error)?,
    825     ];
    826     let samples = [
    827         MetricSample::new(
    828             phase_name,
    829             MetricValue::Gauge(1),
    830             [MetricLabel::phase(operational.phase())],
    831         )
    832         .map_err(map_metrics_error)?,
    833         MetricSample::new(
    834             ready_name,
    835             MetricValue::Gauge(i64::from(operational.readiness().is_ready())),
    836             [],
    837         )
    838         .map_err(map_metrics_error)?,
    839     ];
    840     BoundedMetricsSnapshot::new(descriptors, samples).map_err(map_metrics_error)
    841 }
    842 
    843 fn map_metrics_error(_: radroots_service_host::MetricsContractError) -> RhiStatusError {
    844     RhiStatusError::new(RhiStatusErrorKind::InvalidModel)
    845 }
    846 
    847 impl fmt::Debug for RhiCachedStatus {
    848     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    849         formatter
    850             .debug_struct("RhiCachedStatus")
    851             .field("json_utf8_bytes", &self.json.len())
    852             .finish()
    853     }
    854 }
    855 
    856 /// Sole publication authority for one process-local Rhi status cache.
    857 ///
    858 /// This type deliberately does not implement `Clone`. A successful publish
    859 /// atomically replaces the one retained snapshot. A failed encoding or illegal
    860 /// lifecycle transition leaves the previous snapshot unchanged.
    861 pub struct RhiStatusPublisher {
    862     instance: InstanceId,
    863     inner: CachedServiceStatePublisher<RhiCachedStatus>,
    864     operations: CachedServiceStatePublisher<BoundedMetricsSnapshot>,
    865 }
    866 
    867 impl RhiStatusPublisher {
    868     /// Encodes one observation, publishes its passive operations projection,
    869     /// and then atomically replaces the detailed-status snapshot.
    870     pub fn publish(&mut self, next: RhiStatusObservationV1) -> Result<(), RhiStatusError> {
    871         let next = next.into_cached(&self.instance)?;
    872         self.operations
    873             .publish(next.operations)
    874             .map_err(map_contract_error)?;
    875         self.inner.publish(next.detail).map_err(map_contract_error)
    876     }
    877 
    878     /// Creates another passive reader without sharing publication authority.
    879     #[must_use]
    880     pub fn subscribe(&self) -> RhiStatusReader {
    881         RhiStatusReader {
    882             inner: self.inner.subscribe(),
    883             operations: self.operations.subscribe(),
    884         }
    885     }
    886 }
    887 
    888 impl fmt::Debug for RhiStatusPublisher {
    889     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    890         formatter.write_str("RhiStatusPublisher([sealed])")
    891     }
    892 }
    893 
    894 /// Cloneable passive reader of the latest Rhi lifecycle and detailed status.
    895 pub struct RhiStatusReader {
    896     inner: CachedServiceStateReader<RhiCachedStatus>,
    897     operations: CachedServiceStateReader<BoundedMetricsSnapshot>,
    898 }
    899 
    900 impl Clone for RhiStatusReader {
    901     fn clone(&self) -> Self {
    902         Self {
    903             inner: self.inner.clone(),
    904             operations: self.operations.clone(),
    905         }
    906     }
    907 }
    908 
    909 impl RhiStatusReader {
    910     /// Returns the latest immutable snapshot without awaiting or probing.
    911     #[must_use]
    912     pub fn snapshot(&self) -> RhiStatusSnapshot {
    913         RhiStatusSnapshot {
    914             inner: self.inner.snapshot(),
    915         }
    916     }
    917 
    918     /// Waits for a later publication and returns the newest retained value.
    919     pub async fn changed(&mut self) -> Result<RhiStatusSnapshot, RhiStatusError> {
    920         self.inner
    921             .changed()
    922             .await
    923             .map(|inner| RhiStatusSnapshot { inner })
    924             .map_err(|_| RhiStatusError::new(RhiStatusErrorKind::PublisherDropped))
    925     }
    926 
    927     pub(crate) fn operations_cache(&self) -> CachedServiceStateReader<BoundedMetricsSnapshot> {
    928         self.operations.clone()
    929     }
    930 }
    931 
    932 impl fmt::Debug for RhiStatusReader {
    933     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    934         formatter.write_str("RhiStatusReader([passive])")
    935     }
    936 }
    937 
    938 /// One immutable point-in-time status snapshot backed by the retained cache `Arc`.
    939 pub struct RhiStatusSnapshot {
    940     inner: Arc<CachedServiceState<RhiCachedStatus>>,
    941 }
    942 
    943 impl RhiStatusSnapshot {
    944     #[must_use]
    945     pub fn phase(&self) -> RhiServicePhase {
    946         RhiServicePhase::from_host(self.inner.operational().phase())
    947     }
    948 
    949     #[must_use]
    950     pub fn is_ready(&self) -> bool {
    951         self.inner.operational().readiness().is_ready()
    952     }
    953 
    954     /// Returns the already-bounded canonical detailed-status JSON bytes.
    955     #[must_use]
    956     pub fn detailed_status_json(&self) -> &[u8] {
    957         &self.inner.metrics().json
    958     }
    959 }
    960 
    961 impl fmt::Debug for RhiStatusSnapshot {
    962     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    963         formatter
    964             .debug_struct("RhiStatusSnapshot")
    965             .field("phase", &self.phase())
    966             .field("ready", &self.is_ready())
    967             .field("json_utf8_bytes", &self.detailed_status_json().len())
    968             .finish()
    969     }
    970 }
    971 
    972 /// Creates the single-writer, one-latest-value Rhi status cache.
    973 pub fn rhi_status_cache(
    974     instance: InstanceId,
    975     initial: RhiStatusObservationV1,
    976 ) -> Result<(RhiStatusPublisher, RhiStatusReader), RhiStatusError> {
    977     let initial = initial.into_cached(&instance)?;
    978     let (inner, reader) = cached_service_state(initial.detail);
    979     let (operations, operations_reader) = cached_service_state(initial.operations);
    980     Ok((
    981         RhiStatusPublisher {
    982             instance,
    983             inner,
    984             operations,
    985         },
    986         RhiStatusReader {
    987             inner: reader,
    988             operations: operations_reader,
    989         },
    990     ))
    991 }
    992 
    993 /// Stable source-free status failure category.
    994 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    995 pub enum RhiStatusErrorKind {
    996     InvalidReasonCode,
    997     TooManyReasonCodes,
    998     InvalidLifecycle,
    999     InvalidBuildInfo,
   1000     InvalidConfiguration,
   1001     InvalidPersistence,
   1002     InvalidIdentityHealth,
   1003     InvalidProviderState,
   1004     InvalidTransportState,
   1005     InvalidTime,
   1006     InvalidModel,
   1007     Encoding,
   1008     ResponseTooLarge,
   1009     InvalidTransition,
   1010     PublisherDropped,
   1011 }
   1012 
   1013 impl RhiStatusErrorKind {
   1014     #[must_use]
   1015     pub const fn code(self) -> &'static str {
   1016         match self {
   1017             Self::InvalidReasonCode => "status_reason_code_invalid",
   1018             Self::TooManyReasonCodes => "status_reason_count_exceeded",
   1019             Self::InvalidLifecycle => "status_lifecycle_invalid",
   1020             Self::InvalidBuildInfo => "status_build_info_invalid",
   1021             Self::InvalidConfiguration => "status_configuration_invalid",
   1022             Self::InvalidPersistence => "status_persistence_invalid",
   1023             Self::InvalidIdentityHealth => "status_identity_health_invalid",
   1024             Self::InvalidProviderState => "status_provider_state_invalid",
   1025             Self::InvalidTransportState => "status_transport_state_invalid",
   1026             Self::InvalidTime => "status_time_invalid",
   1027             Self::InvalidModel => "status_model_invalid",
   1028             Self::Encoding => "status_encoding_failed",
   1029             Self::ResponseTooLarge => "status_response_too_large",
   1030             Self::InvalidTransition => "status_transition_invalid",
   1031             Self::PublisherDropped => "status_publisher_dropped",
   1032         }
   1033     }
   1034 
   1035     const fn message(self) -> &'static str {
   1036         match self {
   1037             Self::InvalidReasonCode => "Rhi status reason code is invalid",
   1038             Self::TooManyReasonCodes => "Rhi status has too many reason codes",
   1039             Self::InvalidLifecycle => "Rhi lifecycle status is invalid",
   1040             Self::InvalidBuildInfo => "Rhi status build identity is invalid",
   1041             Self::InvalidConfiguration => "Rhi status configuration identity is invalid",
   1042             Self::InvalidPersistence => "Rhi persistence status is invalid",
   1043             Self::InvalidIdentityHealth => "Rhi identity health is invalid",
   1044             Self::InvalidProviderState => "Rhi provider status is invalid",
   1045             Self::InvalidTransportState => "Rhi transport status is invalid",
   1046             Self::InvalidTime => "Rhi status time is invalid",
   1047             Self::InvalidModel => "Rhi detailed status is invalid",
   1048             Self::Encoding => "Rhi detailed status encoding failed",
   1049             Self::ResponseTooLarge => "Rhi detailed status exceeds its byte limit",
   1050             Self::InvalidTransition => "Rhi lifecycle transition is invalid",
   1051             Self::PublisherDropped => "Rhi status publisher is unavailable",
   1052         }
   1053     }
   1054 }
   1055 
   1056 /// One redacted source-free status failure.
   1057 #[derive(Clone, Copy, PartialEq, Eq)]
   1058 pub struct RhiStatusError {
   1059     kind: RhiStatusErrorKind,
   1060 }
   1061 
   1062 impl RhiStatusError {
   1063     const fn new(kind: RhiStatusErrorKind) -> Self {
   1064         Self { kind }
   1065     }
   1066 
   1067     #[must_use]
   1068     pub const fn kind(self) -> RhiStatusErrorKind {
   1069         self.kind
   1070     }
   1071 
   1072     #[must_use]
   1073     pub const fn code(self) -> &'static str {
   1074         self.kind.code()
   1075     }
   1076 }
   1077 
   1078 impl fmt::Debug for RhiStatusError {
   1079     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
   1080         formatter
   1081             .debug_struct("RhiStatusError")
   1082             .field("kind", &self.kind)
   1083             .finish()
   1084     }
   1085 }
   1086 
   1087 impl fmt::Display for RhiStatusError {
   1088     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
   1089         formatter.write_str(self.kind.message())
   1090     }
   1091 }
   1092 
   1093 impl Error for RhiStatusError {}
   1094 
   1095 const fn map_model_error(_error: StatusModelError) -> RhiStatusError {
   1096     RhiStatusError::new(RhiStatusErrorKind::InvalidModel)
   1097 }
   1098 
   1099 const fn map_encoding_error(error: StatusEncodingError) -> RhiStatusError {
   1100     match error {
   1101         StatusEncodingError::EncodingFailed => RhiStatusError::new(RhiStatusErrorKind::Encoding),
   1102         StatusEncodingError::ResponseTooLarge => {
   1103             RhiStatusError::new(RhiStatusErrorKind::ResponseTooLarge)
   1104         }
   1105     }
   1106 }
   1107 
   1108 const fn map_contract_error(_error: StatusContractError) -> RhiStatusError {
   1109     RhiStatusError::new(RhiStatusErrorKind::InvalidTransition)
   1110 }
   1111 
   1112 #[cfg(test)]
   1113 mod tests {
   1114     use super::*;
   1115 
   1116     #[test]
   1117     fn invalid_status_inputs_fail_with_safe_source_free_errors() {
   1118         assert_eq!(
   1119             RhiIdentityHealthV1::new(false, true, RhiStatusReasonCodes::empty()),
   1120             Err(RhiStatusError::new(
   1121                 RhiStatusErrorKind::InvalidIdentityHealth
   1122             ))
   1123         );
   1124         assert_eq!(
   1125             RhiProviderStatusV1::new(
   1126                 RhiIdentityHealthV1::new(false, false, RhiStatusReasonCodes::empty()).unwrap(),
   1127                 RhiStatusReasonCodes::empty(),
   1128             ),
   1129             Err(RhiStatusError::new(
   1130                 RhiStatusErrorKind::InvalidProviderState
   1131             ))
   1132         );
   1133         assert_eq!(
   1134             RhiEvidenceTransportStatusV1::new(
   1135                 RhiTransportHealthV1::Ready,
   1136                 false,
   1137                 true,
   1138                 1,
   1139                 0,
   1140                 RhiStatusReasonCodes::empty(),
   1141             ),
   1142             Err(RhiStatusError::new(
   1143                 RhiStatusErrorKind::InvalidTransportState
   1144             ))
   1145         );
   1146         assert_eq!(
   1147             RhiEvidenceTransportStatusV1::new(
   1148                 RhiTransportHealthV1::Ready,
   1149                 true,
   1150                 true,
   1151                 1,
   1152                 2,
   1153                 RhiStatusReasonCodes::empty(),
   1154             ),
   1155             Err(RhiStatusError::new(
   1156                 RhiStatusErrorKind::InvalidTransportState
   1157             ))
   1158         );
   1159         assert_eq!(
   1160             RhiStatusUnixSeconds::new(i64::MAX as u64 + 1),
   1161             Err(RhiStatusError::new(RhiStatusErrorKind::InvalidTime))
   1162         );
   1163         for kind in [
   1164             RhiStatusErrorKind::InvalidReasonCode,
   1165             RhiStatusErrorKind::TooManyReasonCodes,
   1166             RhiStatusErrorKind::InvalidLifecycle,
   1167             RhiStatusErrorKind::InvalidBuildInfo,
   1168             RhiStatusErrorKind::InvalidConfiguration,
   1169             RhiStatusErrorKind::InvalidPersistence,
   1170             RhiStatusErrorKind::InvalidIdentityHealth,
   1171             RhiStatusErrorKind::InvalidProviderState,
   1172             RhiStatusErrorKind::InvalidTransportState,
   1173             RhiStatusErrorKind::InvalidTime,
   1174             RhiStatusErrorKind::InvalidModel,
   1175             RhiStatusErrorKind::Encoding,
   1176             RhiStatusErrorKind::ResponseTooLarge,
   1177             RhiStatusErrorKind::InvalidTransition,
   1178             RhiStatusErrorKind::PublisherDropped,
   1179         ] {
   1180             let error = RhiStatusError::new(kind);
   1181             assert!(!error.code().is_empty());
   1182             assert!(Error::source(&error).is_none());
   1183             assert!(!format!("{error} {error:?}").contains("source"));
   1184         }
   1185     }
   1186 }