kmp_adapter_embedded/adapter/
migration.rs1use std::fs;
30use std::path::{Path, PathBuf};
31
32use kmp_domain::{ContextUpdatedEvent, PortError, ProjectionMutation};
33use serde::{Deserialize, Serialize};
34use sha2::{Digest, Sha256};
35
36use super::engine::{Key, Table};
37use super::format_version::{self, StorageEngine};
38use super::store::EmbeddedKernelStore;
39
40const SOURCE_COPY_FILE: &str = "migration-source.redb";
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct StoreMigrationReceipt {
47 pub source_format: u32,
48 pub source_sha256: String,
49 pub destination_format: u32,
50 pub events_migrated: u64,
51 pub mutations_applied: u64,
52 pub kernel_version: String,
53}
54
55impl StoreMigrationReceipt {
56 pub const MIGRATION_ID: &'static str = "store-format-migration";
58}
59
60impl EmbeddedKernelStore {
61 pub async fn migrate_data_dir<F>(
68 source_dir: &Path,
69 destination_dir: &Path,
70 derive: F,
71 ) -> Result<(Self, StoreMigrationReceipt), PortError>
72 where
73 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
74 {
75 Self::migrate_data_dir_to(source_dir, destination_dir, StorageEngine::Redb, derive).await
76 }
77
78 pub async fn migrate_data_dir_to<F>(
86 source_dir: &Path,
87 destination_dir: &Path,
88 destination_engine: StorageEngine,
89 derive: F,
90 ) -> Result<(Self, StoreMigrationReceipt), PortError>
91 where
92 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
93 {
94 if same_file(source_dir, destination_dir) {
95 return Err(PortError::InvalidState(
96 "migration source and destination are the same data directory".to_string(),
97 ));
98 }
99 let source_format = format_version::read_stamped_version(source_dir)?;
100 if source_format > StorageEngine::NEWEST_KNOWN_FORMAT_VERSION {
101 return Err(PortError::InvalidState(format!(
102 "migration source `{}` uses format version {source_format}, newer than this \
103 binary supports ({}); upgrade the binary",
104 source_dir.display(),
105 StorageEngine::NEWEST_KNOWN_FORMAT_VERSION
106 )));
107 }
108 let source_engine =
111 StorageEngine::from_format_version(source_format).unwrap_or(StorageEngine::Redb);
112 if source_engine != StorageEngine::Redb {
118 return Err(PortError::Unavailable(format!(
119 "migration from a {source_engine} store is not supported yet; the source at `{}` \
120 is left untouched",
121 source_dir.display()
122 )));
123 }
124 let source_store_file = format_version::store_file_path_for(source_dir, source_engine);
125 if !source_store_file.exists() {
126 return Err(PortError::InvalidState(format!(
127 "migration source `{}` holds no store file at `{}`",
128 source_dir.display(),
129 source_store_file.display()
130 )));
131 }
132 let source_sha256 = sha256_of(&source_store_file)?;
133
134 if format_version::existing_store_file(destination_dir).is_some() {
135 let already = match Self::open(destination_dir) {
139 Ok(store) => store.migration_receipt().await.ok().flatten(),
140 Err(_) => None,
141 };
142 if let Some(receipt) = already
143 && receipt.source_sha256 == source_sha256
144 {
145 return Err(PortError::Conflict(format!(
146 "migration destination `{}` was already migrated from this exact \
147 source ({} events, source sha256 {}); nothing to do",
148 destination_dir.display(),
149 receipt.events_migrated,
150 receipt.source_sha256
151 )));
152 }
153 return Err(PortError::Conflict(format!(
154 "migration destination `{}` already holds a store; migrate into a new \
155 directory rather than over existing memory",
156 destination_dir.display()
157 )));
158 }
159 let events = read_source_events(&source_store_file, source_engine, destination_dir)?;
160
161 let destination = Self::open_with_engine(destination_dir, destination_engine)?;
162 let events_migrated = destination.replay_event_stream(events).await?;
163 let rebuild = destination.rebuild_projections(derive).await?;
164
165 let source_sha256_after = sha256_of(&source_store_file)?;
169 if source_sha256_after != source_sha256 {
170 return Err(PortError::InvalidState(format!(
171 "migration modified its source `{}`; refusing to report success",
172 source_store_file.display()
173 )));
174 }
175
176 let receipt = StoreMigrationReceipt {
177 source_format,
178 source_sha256,
179 destination_format: destination_engine.format_version(),
180 events_migrated,
181 mutations_applied: rebuild.mutations_applied,
182 kernel_version: env!("CARGO_PKG_VERSION").to_string(),
183 };
184 destination.write_migration_receipt(&receipt).await?;
185 Ok((destination, receipt))
186 }
187
188 pub async fn open_or_migrate_data_dir<F>(
194 source_dir: &Path,
195 destination_dir: &Path,
196 derive: F,
197 ) -> Result<(Self, Option<StoreMigrationReceipt>), PortError>
198 where
199 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
200 {
201 Self::open_or_migrate_data_dir_to(source_dir, destination_dir, StorageEngine::Redb, derive)
202 .await
203 }
204
205 pub async fn open_or_migrate_data_dir_to<F>(
210 source_dir: &Path,
211 destination_dir: &Path,
212 destination_engine: StorageEngine,
213 derive: F,
214 ) -> Result<(Self, Option<StoreMigrationReceipt>), PortError>
215 where
216 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
217 {
218 if format_version::existing_store_file(destination_dir).is_some() {
219 let store = Self::open(destination_dir)?;
220 let receipt = store.migration_receipt().await?;
221 return Ok((store, receipt));
222 }
223 let (store, receipt) =
224 Self::migrate_data_dir_to(source_dir, destination_dir, destination_engine, derive)
225 .await?;
226 Ok((store, Some(receipt)))
227 }
228
229 pub async fn migration_receipt(&self) -> Result<Option<StoreMigrationReceipt>, PortError> {
231 self.run(|store| {
232 let tx = store.begin_read()?;
233 let Some(raw) = tx.get(
236 Table::Migrations,
237 Key::Str(StoreMigrationReceipt::MIGRATION_ID),
238 )?
239 else {
240 return Ok(None);
241 };
242 let receipt = serde_json::from_slice(&raw).map_err(|error| {
243 PortError::InvalidState(format!("migration receipt is unreadable: {error}"))
244 })?;
245 Ok(Some(receipt))
246 })
247 .await
248 }
249
250 async fn write_migration_receipt(
251 &self,
252 receipt: &StoreMigrationReceipt,
253 ) -> Result<(), PortError> {
254 let encoded = serde_json::to_vec(receipt).map_err(|error| {
255 PortError::InvalidState(format!("migration receipt is not encodable: {error}"))
256 })?;
257 self.run(move |store| {
258 let mut tx = store.begin_write()?;
259 tx.insert(
260 Table::Migrations,
261 Key::Str(StoreMigrationReceipt::MIGRATION_ID),
262 &encoded,
263 )?;
264 tx.commit()
265 })
266 .await
267 }
268}
269
270fn read_source_events(
276 source_store_file: &Path,
277 source_engine: StorageEngine,
278 destination_dir: &Path,
279) -> Result<Vec<ContextUpdatedEvent>, PortError> {
280 fs::create_dir_all(destination_dir).map_err(|error| {
281 PortError::Unavailable(format!(
282 "migration could not create destination `{}`: {error}",
283 destination_dir.display()
284 ))
285 })?;
286 let copy_path: PathBuf = destination_dir.join(SOURCE_COPY_FILE);
287 fs::copy(source_store_file, ©_path).map_err(|error| {
288 PortError::Unavailable(format!(
289 "migration could not copy the source store to `{}`: {error}",
290 copy_path.display()
291 ))
292 })?;
293
294 let events = {
295 let source = EmbeddedKernelStore::open_store_file(©_path, source_engine)?;
296 source.read_event_log_blocking()
297 };
298
299 let _ = fs::remove_file(©_path);
302 events
303}
304
305fn sha256_of(path: &Path) -> Result<String, PortError> {
306 let bytes = fs::read(path).map_err(|error| {
307 PortError::Unavailable(format!(
308 "migration could not read `{}`: {error}",
309 path.display()
310 ))
311 })?;
312 let mut hasher = Sha256::new();
313 hasher.update(&bytes);
314 Ok(format!("{:x}", hasher.finalize()))
315}
316
317fn same_file(left: &Path, right: &Path) -> bool {
318 match (fs::canonicalize(left), fs::canonicalize(right)) {
319 (Ok(left), Ok(right)) => left == right,
320 _ => left == right,
323 }
324}