lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

storage.rs (10883B)


      1 //! Canonical storage capability composition.
      2 
      3 /// Backend-neutral storage status owned by `radroots_storage`.
      4 pub type Status = radroots_storage::StorageStatus;
      5 
      6 /// Backend-neutral integrity status owned by `radroots_storage`.
      7 pub type IntegrityStatus = radroots_storage::status::IntegrityStatus;
      8 
      9 /// Validated SQLite open configuration; this contains no connection or pool.
     10 #[cfg(feature = "sqlite")]
     11 pub type SqliteOptions = radroots_storage_sqlite::OpenOptions;
     12 
     13 /// Explicit SQLite lifecycle mode.
     14 #[cfg(feature = "sqlite")]
     15 pub type SqliteOpenMode = radroots_storage_sqlite::OpenMode;
     16 
     17 /// Validated SQLite-owned paths; this contains no backend handle.
     18 #[cfg(feature = "sqlite")]
     19 pub type SqlitePaths = radroots_storage_sqlite::Paths;
     20 
     21 use radroots_storage::backup::{
     22     BackupCapabilityError, BackupId, BackupManifest, BackupOperation, BackupPlan, BackupTransition,
     23     ReliabilityRevision, RestoreCapabilityError, RestoreMemberStatus, RestoreOperation,
     24     RestorePlan, RestoreTransition, StorageReliability,
     25 };
     26 
     27 /// Borrowed reliability operations over the canonical backend-neutral SPI.
     28 #[derive(Clone, Copy)]
     29 pub struct Operations<'a> {
     30     storage: &'a dyn radroots_storage::Storage,
     31 }
     32 
     33 impl<'a> Operations<'a> {
     34     pub(crate) const fn new(storage: &'a dyn radroots_storage::Storage) -> Self {
     35         Self { storage }
     36     }
     37 
     38     /// Waits for earlier owner writes, including cancelled caller work. Hold
     39     /// application write exclusion before this call until inventory/capture
     40     /// completes; this method does not itself stop new application commands.
     41     pub async fn settle_backup_writes(&self) -> Result<(), BackupCapabilityError> {
     42         StorageReliability::settle_backup_writes(self.storage).await
     43     }
     44 
     45     /// Captures actual members through the canonical owner. Related application
     46     /// state and media still require host coordination; no metadata transition
     47     /// is accepted as evidence of a snapshot.
     48     pub async fn capture_backup(
     49         &self,
     50         plan: BackupPlan,
     51     ) -> Result<BackupManifest, BackupCapabilityError> {
     52         StorageReliability::capture_backup(self.storage, plan).await
     53     }
     54 
     55     /// Verifies the owner's exact staged bundle without finalizing it.
     56     pub async fn verify_backup(
     57         &self,
     58         plan: BackupPlan,
     59         manifest: BackupManifest,
     60     ) -> Result<(), BackupCapabilityError> {
     61         StorageReliability::verify_backup(self.storage, plan, manifest).await
     62     }
     63 
     64     /// Verifies and finalizes the owner's bundle, retaining its opaque identity.
     65     pub async fn finalize_backup(
     66         &self,
     67         plan: BackupPlan,
     68         manifest: BackupManifest,
     69     ) -> Result<(), BackupCapabilityError> {
     70         StorageReliability::finalize_backup(self.storage, plan, manifest).await
     71     }
     72 
     73     /// Stages actual verified members through the canonical owner without
     74     /// changing live state. The host owns identity, media and command exclusion.
     75     pub async fn stage_restore(
     76         &self,
     77         plan: RestorePlan,
     78     ) -> Result<Vec<RestoreMemberStatus>, RestoreCapabilityError> {
     79         StorageReliability::stage_restore(self.storage, plan).await
     80     }
     81 
     82     /// Verifies staging, closes the canonical owner and installs its retained
     83     /// snapshot. Reopen explicitly after an installation attempt, then reconcile
     84     /// historical operations before allowing delivery. No path is returned.
     85     pub async fn finalize_restore(&self, plan: RestorePlan) -> Result<(), RestoreCapabilityError> {
     86         StorageReliability::finalize_restore(self.storage, plan).await
     87     }
     88 
     89     /// Begins or resumes one idempotent backup plan.
     90     pub async fn begin_backup(
     91         &self,
     92         plan: BackupPlan,
     93     ) -> Result<BackupOperation, radroots_storage::Error> {
     94         StorageReliability::begin_backup(self.storage, plan).await
     95     }
     96 
     97     /// Applies one optimistic backup transition.
     98     pub async fn transition_backup(
     99         &self,
    100         backup_id: BackupId,
    101         expected_revision: ReliabilityRevision,
    102         transition: BackupTransition,
    103         at_unix_ms: u64,
    104     ) -> Result<BackupOperation, radroots_storage::Error> {
    105         StorageReliability::transition_backup(
    106             self.storage,
    107             backup_id,
    108             expected_revision,
    109             transition,
    110             at_unix_ms,
    111         )
    112         .await
    113     }
    114 
    115     /// Begins or resumes one staged restore plan.
    116     pub async fn begin_restore(
    117         &self,
    118         plan: RestorePlan,
    119     ) -> Result<RestoreOperation, radroots_storage::Error> {
    120         StorageReliability::begin_restore(self.storage, plan).await
    121     }
    122 
    123     /// Applies one optimistic staged restore transition.
    124     pub async fn transition_restore(
    125         &self,
    126         backup_id: BackupId,
    127         expected_revision: ReliabilityRevision,
    128         transition: RestoreTransition,
    129         at_unix_ms: u64,
    130     ) -> Result<RestoreOperation, radroots_storage::Error> {
    131         StorageReliability::transition_restore(
    132             self.storage,
    133             backup_id,
    134             expected_revision,
    135             transition,
    136             at_unix_ms,
    137         )
    138         .await
    139     }
    140 
    141     /// Returns passive backend status without initiating recovery work.
    142     pub async fn status(&self) -> Result<Status, radroots_storage::Error> {
    143         StorageReliability::status(self.storage).await
    144     }
    145 
    146     /// Runs backend-owned integrity inspection.
    147     pub async fn integrity(&self) -> Result<IntegrityStatus, radroots_storage::Error> {
    148         StorageReliability::integrity(self.storage).await
    149     }
    150 }
    151 
    152 impl std::fmt::Debug for Operations<'_> {
    153     fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    154         formatter
    155             .debug_struct("Operations")
    156             .field("storage", &"<borrowed canonical storage>")
    157             .finish()
    158     }
    159 }
    160 
    161 #[cfg(all(test, any(feature = "memory", feature = "sqlite")))]
    162 mod backup_tests;
    163 
    164 #[cfg(all(test, any(feature = "memory", feature = "sqlite")))]
    165 mod restore_tests;
    166 
    167 #[cfg(all(test, feature = "memory"))]
    168 mod tests {
    169     use std::sync::Arc;
    170 
    171     use radroots_storage::{
    172         backup::{
    173             BackupFormatVersion, BackupManifest, BackupMember, BackupMemberKind,
    174             BackupSecretPolicy, BackupStage, MemberDigest, MemberVerification, RestoreMemberStatus,
    175             RestoreStage,
    176         },
    177         event::SourceGeneration,
    178         memory::MemoryStorage,
    179         status::IntegrityHealth,
    180     };
    181 
    182     use crate::ClientBuilder;
    183 
    184     use super::*;
    185 
    186     fn manifest(backup_id: BackupId) -> BackupManifest {
    187         BackupManifest::new(
    188             BackupFormatVersion::V1,
    189             backup_id,
    190             1_800_000_000_100,
    191             BackupSecretPolicy::ExcludeProtectedStorage,
    192             vec![
    193                 BackupMember::new(
    194                     "runtime/events.bin",
    195                     BackupMemberKind::Runtime,
    196                     16,
    197                     MemberDigest::new([3; 32]),
    198                 )
    199                 .expect("member"),
    200             ],
    201         )
    202         .expect("manifest")
    203     }
    204 
    205     #[tokio::test]
    206     async fn memory_reliability_preserves_staging_interruption_integrity_and_native_states() {
    207         let storage = Arc::new(MemoryStorage::new(
    208             SourceGeneration::new([8; 32]).expect("generation"),
    209         ));
    210         let client = ClientBuilder::new()
    211             .storage(storage)
    212             .build()
    213             .expect("client");
    214         let operations = client.storage_operations().expect("operations");
    215         let backup_id = BackupId::new([9; 16]).expect("backup id");
    216         let plan = BackupPlan::new(
    217             backup_id,
    218             BackupFormatVersion::V1,
    219             BackupSecretPolicy::ExcludeProtectedStorage,
    220             1_800_000_000_000,
    221         )
    222         .expect("plan");
    223 
    224         drop(operations.begin_backup(plan.clone()));
    225         let planned = operations.begin_backup(plan).await.expect("planned");
    226         assert_eq!(planned.stage(), BackupStage::Planned);
    227         let captured = operations
    228             .transition_backup(
    229                 backup_id,
    230                 planned.revision(),
    231                 BackupTransition::Captured(manifest(backup_id)),
    232                 1_800_000_000_200,
    233             )
    234             .await
    235             .expect("captured");
    236         let verified = operations
    237             .transition_backup(
    238                 backup_id,
    239                 captured.revision(),
    240                 BackupTransition::Verified,
    241                 1_800_000_000_300,
    242             )
    243             .await
    244             .expect("verified");
    245         let finalized = operations
    246             .transition_backup(
    247                 backup_id,
    248                 verified.revision(),
    249                 BackupTransition::Finalize,
    250                 1_800_000_000_400,
    251             )
    252             .await
    253             .expect("finalized");
    254         assert_eq!(finalized.stage(), BackupStage::Finalized);
    255 
    256         let restore_plan = RestorePlan::new(
    257             manifest(backup_id),
    258             BackupSecretPolicy::ExcludeProtectedStorage,
    259             1_800_000_001_000,
    260         )
    261         .expect("restore plan");
    262         let staging = operations
    263             .begin_restore(restore_plan.clone())
    264             .await
    265             .expect("staging");
    266         assert_eq!(staging.stage(), RestoreStage::Staging);
    267         let replayed = operations
    268             .begin_restore(restore_plan)
    269             .await
    270             .expect("resume staging");
    271         assert_eq!(replayed, staging);
    272         let verifying = operations
    273             .transition_restore(
    274                 backup_id,
    275                 staging.revision(),
    276                 RestoreTransition::Staged,
    277                 1_800_000_001_100,
    278             )
    279             .await
    280             .expect("verifying");
    281         let finalizing = operations
    282             .transition_restore(
    283                 backup_id,
    284                 verifying.revision(),
    285                 RestoreTransition::Verified(vec![
    286                     RestoreMemberStatus::new("runtime/events.bin", MemberVerification::Verified)
    287                         .expect("member status"),
    288                 ]),
    289                 1_800_000_001_200,
    290             )
    291             .await
    292             .expect("finalizing");
    293         let restored = operations
    294             .transition_restore(
    295                 backup_id,
    296                 finalizing.revision(),
    297                 RestoreTransition::Finalize,
    298                 1_800_000_001_300,
    299             )
    300             .await
    301             .expect("restored");
    302         assert_eq!(restored.stage(), RestoreStage::Finalized);
    303         assert_eq!(
    304             operations.integrity().await.expect("integrity").health(),
    305             IntegrityHealth::Healthy
    306         );
    307         assert_eq!(
    308             operations
    309                 .status()
    310                 .await
    311                 .expect("status")
    312                 .integrity()
    313                 .health(),
    314             IntegrityHealth::Healthy
    315         );
    316     }
    317 }