Skip to main content

kcode_k1_groups_projection/
lib.rs

1use std::{
2    path::Path,
3    sync::{
4        Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard,
5        atomic::{AtomicBool, Ordering},
6    },
7    time::{Duration, Instant},
8};
9
10pub use kcode_k1_groups_domain::{
11    ALL_MODELS, ALL_USERS, ApplyOutcome, Group, GroupAction, GroupId, GroupMemberships, GroupName,
12    GroupRevision, GroupRole, GroupUser, LOCAL_MODELS, ModelId, SentinelGroup, UserId,
13};
14use kcode_k1_groups_sqlite::Store;
15use kcode_k1_groups_state::State;
16use kcode_k1_txn_ordering::K1TxnOrdering;
17pub use kcode_k1_txn_ordering::TxId;
18
19pub struct Projection {
20    state: RwLock<State>,
21    apply_lane: Mutex<()>,
22    store: Mutex<Store>,
23    unavailable: AtomicBool,
24}
25
26impl Projection {
27    pub fn open(root: &Path, ordering: &K1TxnOrdering) -> Result<(Self, Option<TxId>), String> {
28        let started = Instant::now();
29        let result = Self::open_inner(root, ordering);
30        let elapsed = started.elapsed();
31        if elapsed > Duration::from_millis(100) {
32            let outcome = if result.is_ok() { "ready" } else { "error" };
33            eprintln!(
34                "level=warn module=kcode-k1-groups-projection operation=open elapsed_us={} outcome={outcome}",
35                elapsed.as_micros()
36            );
37        }
38        result
39    }
40
41    fn open_inner(root: &Path, ordering: &K1TxnOrdering) -> Result<(Self, Option<TxId>), String> {
42        let opened = kcode_k1_groups_recovery::open(root, ordering)?;
43        let (store, state) = opened.into_parts();
44        let cursor = state.cursor();
45        Ok((
46            Self {
47                state: RwLock::new(state),
48                apply_lane: Mutex::new(()),
49                store: Mutex::new(store),
50                unavailable: AtomicBool::new(false),
51            },
52            cursor,
53        ))
54    }
55
56    pub fn apply(&self, callback_txid: TxId, action: GroupAction) -> Result<ApplyOutcome, String> {
57        self.ensure_available()?;
58        let _apply_lane = self.lock_apply_lane()?;
59        self.ensure_available()?;
60        let prepared = {
61            let mut state = self.lock_state_write()?;
62            state.prepare(callback_txid, action)?
63        };
64        let receipt = {
65            let mut store = self.lock_store()?;
66            match store.commit(callback_txid, prepared.change()) {
67                Ok(receipt) => receipt,
68                Err(error) => {
69                    self.mark_unavailable();
70                    return Err(error);
71                }
72            }
73        };
74        let published = {
75            let mut state = self.lock_state_write()?;
76            state.publish(callback_txid, prepared, &receipt)
77        };
78        if published.is_err() {
79            self.mark_unavailable();
80        }
81        published
82    }
83
84    pub fn get(&self, group: GroupId) -> Result<Option<Group>, String> {
85        self.ensure_available()?;
86        let state = self.lock_state_read()?;
87        self.ensure_available()?;
88        state.get(group)
89    }
90
91    pub fn groups_for_user(&self, user: UserId) -> Result<Vec<GroupId>, String> {
92        self.ensure_available()?;
93        let state = self.lock_state_read()?;
94        self.ensure_available()?;
95        state.groups_for_user(user)
96    }
97
98    pub fn groups_for_model(&self, model: ModelId) -> Result<Vec<GroupId>, String> {
99        self.ensure_available()?;
100        let state = self.lock_state_read()?;
101        self.ensure_available()?;
102        state.groups_for_model(model)
103    }
104
105    pub fn memberships(&self, user: UserId, model: ModelId) -> Result<GroupMemberships, String> {
106        self.ensure_available()?;
107        let state = self.lock_state_read()?;
108        self.ensure_available()?;
109        state.memberships(user, model)
110    }
111
112    pub fn clear(&self) -> Result<(), String> {
113        self.ensure_available()?;
114        let _apply_lane = self.lock_apply_lane()?;
115        self.ensure_available()?;
116        let receipt = {
117            let mut store = self.lock_store()?;
118            match store.clear() {
119                Ok(receipt) => receipt,
120                Err(error) => {
121                    self.mark_unavailable();
122                    return Err(error);
123                }
124            }
125        };
126        let mut state = self.lock_state_write()?;
127        state.clear_after_persist(&receipt);
128        Ok(())
129    }
130
131    fn ensure_available(&self) -> Result<(), String> {
132        if self.unavailable.load(Ordering::SeqCst) {
133            Err("projection is unavailable until reopen".to_owned())
134        } else {
135            Ok(())
136        }
137    }
138
139    fn mark_unavailable(&self) {
140        self.unavailable.store(true, Ordering::SeqCst);
141    }
142
143    fn lock_apply_lane(&self) -> Result<MutexGuard<'_, ()>, String> {
144        self.apply_lane.lock().map_err(|_| {
145            self.mark_unavailable();
146            "projection apply lane is unavailable until reopen".to_owned()
147        })
148    }
149
150    fn lock_store(&self) -> Result<MutexGuard<'_, Store>, String> {
151        self.store.lock().map_err(|_| {
152            self.mark_unavailable();
153            "projection database lane is unavailable until reopen".to_owned()
154        })
155    }
156
157    fn lock_state_read(&self) -> Result<RwLockReadGuard<'_, State>, String> {
158        self.state.read().map_err(|_| {
159            self.mark_unavailable();
160            "projection state is unavailable until reopen".to_owned()
161        })
162    }
163
164    fn lock_state_write(&self) -> Result<RwLockWriteGuard<'_, State>, String> {
165        self.state.write().map_err(|_| {
166            self.mark_unavailable();
167            "projection state is unavailable until reopen".to_owned()
168        })
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::Projection;
175
176    fn assert_send_sync<T: Send + Sync>() {}
177
178    #[test]
179    fn projection_is_send_and_sync() {
180        assert_send_sync::<Projection>();
181    }
182}