kmp_adapter_embedded/adapter/
store.rs1use std::fs;
2use std::path::Path;
3use std::sync::Arc;
4
5use kmp_domain::PortError;
6use redb::{Database, TableDefinition};
7
8use super::format_version;
9
10pub(crate) const NODES: TableDefinition<&str, &[u8]> = TableDefinition::new("nodes");
12pub(crate) const RELATIONS: TableDefinition<(&str, &str, &str), &[u8]> =
14 TableDefinition::new("relations_by_source");
15pub(crate) const RELATIONS_BY_TARGET: TableDefinition<(&str, &str, &str), ()> =
17 TableDefinition::new("relations_by_target");
18pub(crate) const DETAILS: TableDefinition<&str, &[u8]> = TableDefinition::new("details");
20pub(crate) const ANCHORS: TableDefinition<&str, ()> = TableDefinition::new("memory_anchors");
22pub(crate) const EVENT_LOG: TableDefinition<u64, &[u8]> = TableDefinition::new("event_log");
24pub(crate) const AGGREGATES: TableDefinition<&str, &[u8]> = TableDefinition::new("aggregates");
26pub(crate) const IDEMPOTENCY: TableDefinition<&str, &[u8]> = TableDefinition::new("idempotency");
28pub(crate) const PROCESSED: TableDefinition<(&str, &str), ()> =
30 TableDefinition::new("processed_events");
31pub(crate) const CHECKPOINTS: TableDefinition<(&str, &str), &[u8]> =
33 TableDefinition::new("projection_checkpoints");
34pub(crate) const SNAPSHOTS: TableDefinition<(&str, &str), &[u8]> =
36 TableDefinition::new("snapshots");
37
38#[derive(Debug, Clone)]
44pub struct EmbeddedKernelStore {
45 database: Arc<Database>,
46}
47
48impl EmbeddedKernelStore {
49 pub fn open(data_dir: &Path) -> Result<Self, PortError> {
52 fs::create_dir_all(data_dir).map_err(|error| {
53 PortError::Unavailable(format!(
54 "embedded store could not create data dir `{}`: {error}",
55 data_dir.display()
56 ))
57 })?;
58 format_version::check_or_stamp(data_dir)?;
59
60 let store_file = format_version::store_file_path(data_dir);
61 fs::create_dir_all(store_file.parent().expect("store file has a parent")).map_err(
62 |error| {
63 PortError::Unavailable(format!(
64 "embedded store could not create store dir under `{}`: {error}",
65 data_dir.display()
66 ))
67 },
68 )?;
69
70 Self::open_store_file(&store_file)
71 }
72
73 pub(crate) fn open_store_file(store_file: &Path) -> Result<Self, PortError> {
78 let database = Database::create(store_file).map_err(|error| {
79 PortError::Unavailable(format!(
80 "embedded store could not open `{}`: {error}",
81 store_file.display()
82 ))
83 })?;
84
85 let store = Self {
86 database: Arc::new(database),
87 };
88 store.initialize_tables()?;
89 Ok(store)
90 }
91
92 fn initialize_tables(&self) -> Result<(), PortError> {
95 let tx = self.begin_write()?;
96 {
97 tx.open_table(NODES).map_err(table_error)?;
98 tx.open_table(RELATIONS).map_err(table_error)?;
99 tx.open_table(RELATIONS_BY_TARGET).map_err(table_error)?;
100 tx.open_table(DETAILS).map_err(table_error)?;
101 tx.open_table(ANCHORS).map_err(table_error)?;
102 tx.open_table(EVENT_LOG).map_err(table_error)?;
103 tx.open_table(AGGREGATES).map_err(table_error)?;
104 tx.open_table(IDEMPOTENCY).map_err(table_error)?;
105 tx.open_table(PROCESSED).map_err(table_error)?;
106 tx.open_table(CHECKPOINTS).map_err(table_error)?;
107 tx.open_table(SNAPSHOTS).map_err(table_error)?;
108 }
109 tx.commit().map_err(commit_error)
110 }
111
112 pub(crate) fn begin_write(&self) -> Result<redb::WriteTransaction, PortError> {
113 self.database.begin_write().map_err(|error| {
114 PortError::Unavailable(format!("embedded store write transaction failed: {error}"))
115 })
116 }
117
118 pub(crate) fn begin_read(&self) -> Result<redb::ReadTransaction, PortError> {
119 use redb::ReadableDatabase;
120 self.database.begin_read().map_err(|error| {
121 PortError::Unavailable(format!("embedded store read transaction failed: {error}"))
122 })
123 }
124
125 pub(crate) async fn run<T, F>(&self, task: F) -> Result<T, PortError>
128 where
129 T: Send + 'static,
130 F: FnOnce(&EmbeddedKernelStore) -> Result<T, PortError> + Send + 'static,
131 {
132 let store = self.clone();
133 tokio::task::spawn_blocking(move || task(&store))
134 .await
135 .map_err(|error| {
136 PortError::Unavailable(format!("embedded store worker failed: {error}"))
137 })?
138 }
139
140 pub async fn event_log_stats(&self) -> Result<(u64, u64), PortError> {
143 self.run(|store| {
144 let tx = store.begin_read()?;
145 let log = tx.open_table(EVENT_LOG).map_err(table_error)?;
146 let mut count = 0u64;
147 let mut last_sequence = 0u64;
148 for row in redb::ReadableTable::iter(&log).map_err(range_error)? {
149 let (key, _) = row.map_err(range_error)?;
150 count += 1;
151 last_sequence = key.value();
152 }
153 Ok((count, last_sequence))
154 })
155 .await
156 }
157}
158
159pub(crate) fn aggregate_key(root_node_id: &str, role: &str) -> String {
160 format!("{root_node_id}\u{1f}{role}")
161}
162
163pub(crate) fn table_error(error: redb::TableError) -> PortError {
164 PortError::Unavailable(format!("embedded store table access failed: {error}"))
165}
166
167pub(crate) fn storage_error(error: redb::StorageError) -> PortError {
168 PortError::Unavailable(format!("embedded store storage access failed: {error}"))
169}
170
171pub(crate) fn range_error(error: impl std::fmt::Display) -> PortError {
172 PortError::Unavailable(format!("embedded store range read failed: {error}"))
173}
174
175pub(crate) fn commit_error(error: redb::CommitError) -> PortError {
176 PortError::Unavailable(format!("embedded store commit failed: {error}"))
177}
178
179impl EmbeddedKernelStore {
180 pub fn compact_data_dir(data_dir: &Path) -> Result<bool, PortError> {
185 format_version::check_or_stamp(data_dir)?;
186 let store_file = format_version::store_file_path(data_dir);
187 let mut database = Database::create(&store_file).map_err(|error| {
188 PortError::Unavailable(format!(
189 "embedded store could not open `{}` for compaction: {error}",
190 store_file.display()
191 ))
192 })?;
193 database.compact().map_err(|error| {
194 PortError::Unavailable(format!(
195 "embedded store compaction failed for `{}`: {error}",
196 store_file.display()
197 ))
198 })
199 }
200}