1use super::*;
2
3impl StoreSession<'_> {
4 pub(crate) fn prepare_received_snapshot_circles(
5 &mut self,
6 selection: &crate::StagedCircleRestore,
7 receiver_wall_ms: u64,
8 ) -> Result<(), DbError> {
9 let root = self.required_root_authority()?;
10 let transaction = self.conn.unchecked_transaction()?;
11 StoreTransaction::new(&transaction, self.store_dir).restore_device_join_snapshot_circles(
12 &root,
13 selection,
14 self.synced_tables,
15 self.blob_decls,
16 receiver_wall_ms,
17 )?;
18 transaction.commit()?;
19 self.verified_store_authority
20 .forget_superseded_replay_baseline();
21 Ok(())
22 }
23}
24
25pub struct PreparedStoreSnapshot {
28 image: crate::SnapshotDatabaseImage,
29 directory: SnapshotPreparationDirectory,
30}
31
32impl PreparedStoreSnapshot {
33 pub(crate) fn seal(core: crate::DatabaseCore) -> Result<Self, DbError> {
34 let (bytes, directory) = core.serialize_and_close_snapshot()?;
35 let image =
36 crate::SnapshotDatabaseImage::create(directory.path.join("prepared.sqlite"), &bytes)
37 .map_err(DbError::from);
38 match image {
39 Ok(image) => Ok(Self { image, directory }),
40 Err(error) => directory.finish(Err(error)),
41 }
42 }
43
44 pub(crate) fn install_on(
45 self,
46 receiver: &mut crate::store::StoreSession<'_>,
47 expected: crate::StorePublicationBoundary,
48 materializations: Vec<crate::PreparedMergeMaterialization>,
49 accepted: crate::AcceptedStorePublicationInterval,
50 mut snapshots: Vec<coven_protocol::store_commit::RetainedReplaySnapshotAuthority>,
51 membership: coven_protocol::membership::LocalStoreMembership,
52 routing_encryption: Option<&coven_keys::encryption::EncryptionService>,
53 routing_key: Option<coven_protocol::circle::RowRoutingKey>,
54 receiver_wall_ms: u64,
55 ) -> Result<
56 (
57 crate::store::AppliedMergeMaterialization,
58 Vec<coven_protocol::store_commit::StoreBatchCommitRef>,
59 ),
60 DbError,
61 > {
62 let Self { image, directory } = self;
63 let outcome = (|| {
64 let bytes = image.read_and_discard().map_err(DbError::from)?;
65 let mut source = rusqlite::Connection::open_in_memory()?;
66 crate::connection_io::deserialize_database_image_into(&mut source, &bytes)?;
67 let source_dir = coven_foundation::store_dir::StoreDir::new_ephemeral(&directory.path);
68 let source_version: u32 =
69 source.pragma_query_value(None, "user_version", |row| row.get(0))?;
70 let source_routing = crate::database_open::load_coven_metadata(&source)?;
71 if source_version != receiver.schema_version
72 || source_routing.hash() != receiver.sync_routing_hash
73 {
74 return Err(DbError::Message(
75 "prepared checkpoint differs from the receiver schema".into(),
76 ));
77 }
78 let root = receiver.required_root_authority()?;
79 let source_records = StoreRecords::new(&source, &source_dir);
80 let mut source_authority = VerifiedStoreAuthority::default();
81 if source_authority.required_root_authority_on(source_records)? != root {
82 return Err(DbError::Message(
83 "prepared checkpoint belongs to another Store".into(),
84 ));
85 }
86 let baseline =
87 retained_replay::load_replay_baseline_on(source_records)?.ok_or_else(|| {
88 DbError::Message("prepared checkpoint has no replay baseline".into())
89 })?;
90 let crate::RetainedReplayAuthority::InstalledSnapshot(checkpoint) = &baseline.authority
91 else {
92 return Err(DbError::Message(
93 "prepared checkpoint has genesis authority".into(),
94 ));
95 };
96 let snapshot = accepted
97 .interval()
98 .current()
99 .latest_snapshot()
100 .cloned()
101 .ok_or_else(|| {
102 DbError::Message("received interval has no accepted checkpoint".into())
103 })?;
104 if checkpoint.snapshot != snapshot.snapshot {
105 return Err(DbError::Message(
106 "prepared image is not the received current checkpoint".into(),
107 ));
108 }
109 snapshots.push(checkpoint.clone());
110 let inputs = source_authority
111 .retained_replay_inputs_on(source_records, &root)
112 .map_err(|error| DbError::context("checkpoint source retained inputs", error))?;
113 let source_floor = crate::connection_io::parse_seed(
114 crate::get_protocol_state_on(&source, coven_protocol::hlc::HIGHWATER_STATE_KEY)?,
115 "received checkpoint clock floor",
116 )?;
117 let row_floor = crate::connection_io::parse_seed(
118 crate::connection_io::scan_max_updated_at(
119 &source,
120 receiver.synced_tables,
121 receiver_wall_ms.saturating_add(coven_protocol::hlc::MAX_FUTURE_SKEW_MS),
122 )?,
123 "received checkpoint row clock",
124 )?;
125 let schema_version = receiver.schema_version;
126 let routing_hash = receiver.sync_routing_hash;
127 receiver.verified_store_transaction(|transaction| {
128 if observed_store_publication::load_store_current_publication_on(
129 transaction.store.transaction,
130 )? != expected
131 {
132 return Err(DbError::StorePublicationChanged);
133 }
134 let frontier = coven_protocol::store_commit::CommitFrontier::from_refs(
135 materialized_commit_index::materialized_frontier_on(
136 transaction.store.transaction,
137 None,
138 )?,
139 )?;
140 let (local_image, folded) = transaction
141 .capture_replay_baseline_at_cut(
142 &root,
143 &frontier,
144 &frontier,
145 snapshot.snapshot.snapshot_hash,
146 routing_encryption,
147 )
148 .map_err(|error| {
149 DbError::context("checkpoint receiver replay capture", error)
150 })?;
151 let covered_suffix = StoreDatabase::covered_replay_suffix_on(
152 StoreRecords::new(transaction.store.transaction, transaction.store.store_dir),
153 &baseline,
154 &folded,
155 )?;
156 let image = source_records
157 .received_snapshot_image_with_local_rows(
158 &local_image,
159 transaction.gates,
160 &covered_suffix,
161 )
162 .map_err(|error| DbError::context("checkpoint Local rows", error))?;
163 transaction
164 .store
165 .import_snapshot_device_states(source_records)
166 .map_err(|error| DbError::context("checkpoint device states", error))?;
167 let installed_baseline = transaction
168 .store
169 .replace_received_snapshot_baseline(
170 source_records,
171 &baseline,
172 image,
173 &folded,
174 &inputs,
175 transaction.blob_decls,
176 )
177 .map_err(|error| DbError::context("checkpoint baseline replacement", error))?;
178 observed_store_publication::install_store_checkpoint_publication_on(
179 transaction.store.transaction,
180 &expected,
181 &accepted,
182 checkpoint,
183 )
184 .map_err(|error| DbError::context("checkpoint publication boundary", error))?;
185 transaction
186 .store
187 .import_received_snapshot_inputs(source_records, &inputs, &installed_baseline)
188 .map_err(|error| {
189 DbError::context("checkpoint retained inputs import", error)
190 })?;
191 transaction
192 .store
193 .import_received_snapshot_blob_inventory(source_records)
194 .map_err(|error| DbError::context("checkpoint blob inventory", error))?;
195 transaction
199 .authority
200 .replace_installed_replay_baseline(installed_baseline)?;
201 transaction.clock_floor = source_floor.clone().max(row_floor.clone());
202 if let Some(floor) = &transaction.clock_floor {
203 transaction.clock.advance_past(floor);
204 }
205 let replay = accepted.interval().clone();
206 let applied = transaction
207 .apply_received_store_publication_interval(
208 materializations,
209 accepted,
210 replay,
211 snapshots,
212 membership,
213 schema_version,
214 routing_hash,
215 routing_encryption,
216 routing_key,
217 receiver_wall_ms,
218 Some(snapshot),
219 )
220 .map_err(|error| DbError::context("checkpoint received interval", error))?;
221 if matches!(applied.0.outcome, crate::MaterializationOutcome::Applied(_)) {
222 transaction.store.retain_snapshot_device_states(
223 &mut *transaction.authority,
224 &root,
225 checkpoint.metadata.coverage.clone().into_refs(),
226 )?;
227 let installed_records = StoreRecords::new(
228 transaction.store.transaction,
229 transaction.store.store_dir,
230 );
231 let installed = retained_replay::load_replay_baseline_on(installed_records)?
232 .ok_or_else(|| {
233 DbError::Message(
234 "received checkpoint installation lost its baseline".into(),
235 )
236 })?;
237 if installed.authority != baseline.authority {
238 return Err(DbError::Message(
239 "received checkpoint installation changed its accepted authority"
240 .into(),
241 ));
242 }
243 installed_records.declared_store_device_state(
244 &checkpoint.metadata.history_summary.post_state,
245 )?;
246 installed_records.store_device_state_for_history_cut(
247 &coven_protocol::store_commit::StoreHistoryCut(
248 coven_protocol::store_commit::CommitFrontier::from_refs(
249 installed_records.materialized_frontier()?,
250 )?
251 .0,
252 ),
253 )?;
254 Ok(StoreTransactionOutcome::Commit(applied))
255 } else {
256 Ok(StoreTransactionOutcome::Rollback(applied))
257 }
258 })
259 })();
260 directory.finish(outcome)
261 }
262
263 #[cfg(test)]
265 pub(crate) fn discard(self) -> Result<(), DbError> {
266 self.finish(Ok(()))
267 }
268
269 #[cfg(test)]
270 fn finish<T>(self, outcome: Result<T, DbError>) -> Result<T, DbError> {
271 let outcome = match self.image.finish_operation(outcome) {
272 Ok(value) => Ok(value),
273 Err(crate::SnapshotImageOperationError::Operation(error)) => Err(error),
274 Err(crate::SnapshotImageOperationError::Cleanup { path, cleanup }) => {
275 Err(crate::SnapshotImageError::Cleanup { path, cleanup }.into())
276 }
277 Err(crate::SnapshotImageOperationError::CleanupAfterFailure {
278 path,
279 cleanup,
280 cause,
281 }) => Err(crate::SnapshotImageError::CleanupAfterFailure {
282 path,
283 cleanup,
284 cause: Box::new(crate::SnapshotImageError::from(cause)),
285 }
286 .into()),
287 };
288 self.directory.finish(outcome)
289 }
290}
291
292pub(crate) struct SnapshotPreparationDirectory {
295 path: std::path::PathBuf,
296 armed: bool,
297}
298
299impl SnapshotPreparationDirectory {
300 pub(crate) fn after_close<T>(
301 self,
302 outcome: Result<T, DbError>,
303 closed: Result<(), DbError>,
304 ) -> Result<(T, Self), DbError> {
305 let outcome = match (outcome, closed) {
306 (Ok(value), Ok(())) => Ok(value),
307 (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
308 (Err(operation), Err(close)) => Err(crate::SnapshotImageError::CleanupAfterFailure {
309 path: self.path.clone(),
310 cleanup: close.to_string(),
311 cause: Box::new(crate::SnapshotImageError::from(operation)),
312 }
313 .into()),
314 };
315 match outcome {
316 Ok(value) => Ok((value, self)),
317 Err(error) => self.finish(Err(error)),
318 }
319 }
320
321 pub(crate) fn create(path: std::path::PathBuf) -> Result<Self, DbError> {
322 std::fs::create_dir(&path)
323 .map_err(|error| DbError::context("create snapshot preparation directory", error))?;
324 Ok(Self { path, armed: true })
325 }
326
327 pub(crate) fn finish<T>(mut self, outcome: Result<T, DbError>) -> Result<T, DbError> {
328 let cleanup = std::fs::remove_dir_all(&self.path);
329 self.armed = false;
330 match (outcome, cleanup) {
331 (Ok(value), Ok(())) => Ok(value),
332 (Err(error), Ok(())) => Err(error),
333 (Ok(_), Err(cleanup)) => Err(crate::SnapshotImageError::Cleanup {
334 path: self.path.clone(),
335 cleanup: cleanup.to_string(),
336 }
337 .into()),
338 (Err(operation), Err(cleanup)) => Err(crate::SnapshotImageError::CleanupAfterFailure {
339 path: self.path.clone(),
340 cleanup: cleanup.to_string(),
341 cause: Box::new(crate::SnapshotImageError::from(operation)),
342 }
343 .into()),
344 }
345 }
346}
347
348impl Drop for SnapshotPreparationDirectory {
349 fn drop(&mut self) {
350 if self.armed {
351 if let Err(error) = std::fs::remove_dir_all(&self.path) {
352 tracing::warn!(path = %self.path.display(), %error, "could not remove abandoned snapshot preparation");
353 }
354 }
355 }
356}
357
358#[cfg(test)]
359#[path = "received_snapshot_tests.rs"]
360mod tests;