Skip to main content

zerodds_corba_ccm_lib/
persistence.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! PersistenceStorageComponent — Persistent State Service §10.
5//!
6//! Spec PSS 1.0 (`formal/2002-09-08`): definiert ein Storage-System
7//! fuer CCM-Entity-Components, mit `storagetype` + `storagehome`
8//! Datenmodell. Diese Component liefert eine produktionsreife
9//! In-Memory-Implementation; Production-Caller plugt eine
10//! persistente Backend-DB ein (PostgreSQL, RocksDB).
11//!
12//! Spec §10.2 — `StorageObject`: Lifecycle-State Active/Passive +
13//! Identity (oid).
14
15use alloc::boxed::Box;
16use alloc::collections::BTreeMap;
17use alloc::vec::Vec;
18
19use zerodds_corba_ccm::cif::{CifError, ComponentExecutor};
20use zerodds_corba_ccm::context::ComponentContext;
21
22/// Storage-Eintrag — Spec PSS §10.2.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct StorageEntry {
25    /// Storage-Object-Id (oid). Spec §10.2.1 — Primary-Key.
26    pub oid: Vec<u8>,
27    /// State-Bytes (CDR-encoded oder von Caller serialisiert).
28    pub state: Vec<u8>,
29    /// `dirty` — wurde modifiziert seit letztem `flush`.
30    pub dirty: bool,
31}
32
33/// Persistence-Component-Fehler.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum PersistenceError {
36    /// Object existiert nicht.
37    NotFound(Vec<u8>),
38    /// Object existiert bereits.
39    DuplicateOid(Vec<u8>),
40    /// CIF-Error.
41    Cif(CifError),
42}
43
44impl core::fmt::Display for PersistenceError {
45    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46        match self {
47            Self::NotFound(oid) => write!(f, "object not found: {oid:?}"),
48            Self::DuplicateOid(oid) => write!(f, "duplicate oid: {oid:?}"),
49            Self::Cif(e) => write!(f, "cif: {e:?}"),
50        }
51    }
52}
53
54#[cfg(feature = "std")]
55impl std::error::Error for PersistenceError {}
56
57/// PersistenceStorageComponent — production-ready CCM-Component.
58#[derive(Default)]
59pub struct PersistenceStorageComponent {
60    storage: BTreeMap<Vec<u8>, StorageEntry>,
61    activated: bool,
62    flush_count: u64,
63    ctx: Option<Box<dyn ComponentContext>>,
64}
65
66impl core::fmt::Debug for PersistenceStorageComponent {
67    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
68        f.debug_struct("PersistenceStorageComponent")
69            .field("entries", &self.storage.len())
70            .field("activated", &self.activated)
71            .field("flush_count", &self.flush_count)
72            .finish_non_exhaustive()
73    }
74}
75
76impl PersistenceStorageComponent {
77    /// Konstruktor.
78    #[must_use]
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// `create` — Spec PSS §10.3.2 (StorageHomeBase::create).
84    ///
85    /// # Errors
86    /// `DuplicateOid` wenn die oid schon existiert.
87    pub fn create(&mut self, oid: Vec<u8>, state: Vec<u8>) -> Result<(), PersistenceError> {
88        if self.storage.contains_key(&oid) {
89            return Err(PersistenceError::DuplicateOid(oid));
90        }
91        self.storage.insert(
92            oid.clone(),
93            StorageEntry {
94                oid,
95                state,
96                dirty: true,
97            },
98        );
99        Ok(())
100    }
101
102    /// `find` — Spec PSS §10.3.4 (StorageHomeBase::find_by_short_pid).
103    #[must_use]
104    pub fn find(&self, oid: &[u8]) -> Option<&StorageEntry> {
105        self.storage.get(oid)
106    }
107
108    /// `update` — modifiziert State, markiert `dirty`.
109    ///
110    /// # Errors
111    /// `NotFound` wenn die oid unbekannt ist.
112    pub fn update(&mut self, oid: &[u8], new_state: Vec<u8>) -> Result<(), PersistenceError> {
113        let entry = self
114            .storage
115            .get_mut(oid)
116            .ok_or_else(|| PersistenceError::NotFound(oid.to_vec()))?;
117        entry.state = new_state;
118        entry.dirty = true;
119        Ok(())
120    }
121
122    /// `destroy` — Spec PSS §10.3.5 (StorageObjectBase::destroy).
123    ///
124    /// # Errors
125    /// `NotFound` wenn die oid unbekannt ist.
126    pub fn destroy(&mut self, oid: &[u8]) -> Result<(), PersistenceError> {
127        if self.storage.remove(oid).is_none() {
128            return Err(PersistenceError::NotFound(oid.to_vec()));
129        }
130        Ok(())
131    }
132
133    /// `flush` — Spec PSS §10.4.1 (Catalog::flush). Resettet alle
134    /// `dirty`-Flags und inkrementiert den Flush-Counter.
135    pub fn flush(&mut self) -> u64 {
136        for entry in self.storage.values_mut() {
137            entry.dirty = false;
138        }
139        self.flush_count += 1;
140        self.flush_count
141    }
142
143    /// Anzahl der Storage-Entries.
144    #[must_use]
145    pub fn len(&self) -> usize {
146        self.storage.len()
147    }
148
149    /// Liefert `true` wenn keine Eintraege vorhanden sind.
150    #[must_use]
151    pub fn is_empty(&self) -> bool {
152        self.storage.is_empty()
153    }
154
155    /// Liefert die aktuelle Flush-Anzahl.
156    #[must_use]
157    pub fn flush_count(&self) -> u64 {
158        self.flush_count
159    }
160
161    /// Liefert die Anzahl `dirty` Eintraege.
162    #[must_use]
163    pub fn dirty_count(&self) -> usize {
164        self.storage.values().filter(|e| e.dirty).count()
165    }
166
167    /// Liefert `true` wenn aktiviert.
168    #[must_use]
169    pub fn is_active(&self) -> bool {
170        self.activated
171    }
172}
173
174impl ComponentExecutor for PersistenceStorageComponent {
175    fn set_context(&mut self, context: Box<dyn ComponentContext>) {
176        self.ctx = Some(context);
177    }
178
179    fn ccm_activate(&mut self) -> Result<(), CifError> {
180        self.activated = true;
181        Ok(())
182    }
183
184    fn ccm_passivate(&mut self) -> Result<(), CifError> {
185        // Spec PSS §10.4.1: vor Passivierung muessen dirty-Eintraege
186        // geflusht werden, sonst geht State verloren.
187        let _ = self.flush();
188        self.activated = false;
189        Ok(())
190    }
191
192    fn ccm_remove(&mut self) -> Result<(), CifError> {
193        self.activated = false;
194        self.storage.clear();
195        Ok(())
196    }
197}
198
199#[cfg(test)]
200#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn create_and_find_round_trip() {
206        let mut s = PersistenceStorageComponent::new();
207        s.create(b"oid1".to_vec(), b"state-A".to_vec()).unwrap();
208        let entry = s.find(b"oid1").unwrap();
209        assert_eq!(entry.state, b"state-A");
210        assert!(entry.dirty);
211    }
212
213    #[test]
214    fn duplicate_create_rejected() {
215        let mut s = PersistenceStorageComponent::new();
216        s.create(b"oid1".to_vec(), alloc::vec![]).unwrap();
217        assert!(matches!(
218            s.create(b"oid1".to_vec(), alloc::vec![]),
219            Err(PersistenceError::DuplicateOid(_))
220        ));
221    }
222
223    #[test]
224    fn update_changes_state_and_marks_dirty() {
225        let mut s = PersistenceStorageComponent::new();
226        s.create(b"oid".to_vec(), b"old".to_vec()).unwrap();
227        s.flush();
228        assert_eq!(s.dirty_count(), 0);
229        s.update(b"oid", b"new".to_vec()).unwrap();
230        assert_eq!(s.find(b"oid").unwrap().state, b"new");
231        assert_eq!(s.dirty_count(), 1);
232    }
233
234    #[test]
235    fn update_unknown_fails() {
236        let mut s = PersistenceStorageComponent::new();
237        assert!(matches!(
238            s.update(b"nope", alloc::vec![]),
239            Err(PersistenceError::NotFound(_))
240        ));
241    }
242
243    #[test]
244    fn destroy_removes_entry() {
245        let mut s = PersistenceStorageComponent::new();
246        s.create(b"oid".to_vec(), alloc::vec![]).unwrap();
247        s.destroy(b"oid").unwrap();
248        assert!(s.is_empty());
249    }
250
251    #[test]
252    fn flush_resets_dirty_and_increments_counter() {
253        let mut s = PersistenceStorageComponent::new();
254        s.create(b"a".to_vec(), alloc::vec![]).unwrap();
255        s.create(b"b".to_vec(), alloc::vec![]).unwrap();
256        assert_eq!(s.dirty_count(), 2);
257        let n = s.flush();
258        assert_eq!(n, 1);
259        assert_eq!(s.dirty_count(), 0);
260        assert_eq!(s.flush_count(), 1);
261    }
262
263    #[test]
264    fn passivate_flushes_dirty_entries() {
265        let mut s = PersistenceStorageComponent::new();
266        s.ccm_activate().unwrap();
267        s.create(b"a".to_vec(), alloc::vec![1]).unwrap();
268        assert_eq!(s.dirty_count(), 1);
269        s.ccm_passivate().unwrap();
270        assert_eq!(s.dirty_count(), 0);
271        assert!(!s.is_active());
272    }
273
274    #[test]
275    fn remove_clears_all() {
276        let mut s = PersistenceStorageComponent::new();
277        s.create(b"a".to_vec(), alloc::vec![]).unwrap();
278        s.ccm_remove().unwrap();
279        assert!(s.is_empty());
280    }
281}