1use crate::DbError;
2use coven_foundation::store_dir::{PathTokenError, StoreDir};
3use coven_keys::encryption::EncryptionService;
4use coven_protocol::blob::BlobRef;
5use coven_protocol::write::WriteReceipt;
6
7use super::{SqlContext, StoreDatabase, StoreSession};
8
9pub struct WriteBatch {
10 new_blobs: Vec<NewBlob>,
11 deleted_blobs: Vec<BlobRef>,
12}
13
14impl WriteBatch {
15 pub fn new() -> Self {
16 Self {
17 new_blobs: Vec::new(),
18 deleted_blobs: Vec::new(),
19 }
20 }
21
22 pub fn put_blob(
23 &mut self,
24 namespace: impl Into<String>,
25 id: impl Into<String>,
26 bytes: impl Into<Vec<u8>>,
27 ) {
28 self.new_blobs.push(NewBlob {
29 namespace: namespace.into(),
30 id: id.into(),
31 bytes: bytes.into(),
32 });
33 }
34
35 pub fn delete_blob(&mut self, blob: BlobRef) {
36 self.deleted_blobs.push(blob);
37 }
38}
39
40impl Default for WriteBatch {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46#[derive(Debug)]
49pub struct BlobFileFailure {
50 pub namespace: String,
51 pub id: String,
52 pub reason: coven_foundation::atomic_file::FileError,
53}
54
55impl std::fmt::Display for BlobFileFailure {
56 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(formatter, "{}/{}: {}", self.namespace, self.id, self.reason)
58 }
59}
60
61#[derive(Debug)]
63pub struct BlobFileFailures(pub Vec<BlobFileFailure>);
64
65impl std::fmt::Display for BlobFileFailures {
66 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 for (index, failure) in self.0.iter().enumerate() {
68 if index > 0 {
69 formatter.write_str("; ")?;
70 }
71 write!(formatter, "{failure}")?;
72 }
73 Ok(())
74 }
75}
76
77pub(crate) struct NewBlob {
78 namespace: String,
79 id: String,
80 bytes: Vec<u8>,
81}
82
83struct StagedBlob {
84 namespace: String,
85 id: String,
86 staged: Option<coven_foundation::local_file::AtomicStagedFile>,
87 published: Option<coven_foundation::local_file::PublishedAtomicFile>,
88}
89
90pub(crate) struct StagedBlobBatch {
91 blobs: Vec<StagedBlob>,
92}
93
94impl StagedBlob {
95 async fn stage<E>(store_dir: &StoreDir, blob: NewBlob) -> Result<Self, HostWriteError<E>> {
96 let destination = store_dir.local_blob_path(&blob.namespace, &blob.id)?;
97 let staged = store_dir
98 .stage_atomic_file(&destination)
99 .await
100 .map_err(HostWriteError::Blob)?;
101 let mut staged_blob = Self {
102 namespace: blob.namespace,
103 id: blob.id,
104 staged: Some(staged),
105 published: None,
106 };
107 if let Err(operation) = staged_blob.staged_mut().write_bytes(&blob.bytes).await {
108 let namespace = staged_blob.namespace.clone();
109 let id = staged_blob.id.clone();
110 return match staged_blob.discard().await {
111 Ok(()) => Err(HostWriteError::Blob(operation)),
112 Err(reason) => Err(HostWriteError::BlobCleanupFailed {
113 operation: Box::new(HostWriteError::Blob(operation)),
114 cleanup: BlobFileFailures(vec![BlobFileFailure {
115 namespace,
116 id,
117 reason,
118 }]),
119 }),
120 };
121 }
122 Ok(staged_blob)
123 }
124
125 fn staged_mut(&mut self) -> &mut coven_foundation::local_file::AtomicStagedFile {
126 self.staged.as_mut().expect("blob is staged")
127 }
128
129 fn publish(&mut self) -> Result<(), coven_foundation::atomic_file::FileError> {
130 let staged = self.staged.take().expect("blob is staged");
131 self.published = Some(staged.publish_for_transaction()?);
132 Ok(())
133 }
134
135 async fn discard(mut self) -> Result<(), coven_foundation::atomic_file::FileError> {
136 match self.staged.take() {
137 Some(staged) => staged.discard().await,
138 None => Ok(()),
139 }
140 }
141
142 fn rollback(mut self) -> Vec<coven_foundation::atomic_file::FileError> {
143 let mut failures = Vec::new();
144 if let Some(published) = self.published.take() {
145 if let Err(error) = published.rollback() {
146 failures.push(error);
147 }
148 }
149 if let Some(staged) = self.staged.take() {
150 if let Err(error) = staged.discard_blocking() {
151 failures.push(error);
152 }
153 }
154 failures
155 }
156
157 fn commit(mut self) {
158 assert!(self.staged.is_none(), "committed blob remains staged");
159 assert!(self.published.take().is_some(), "blob was not published");
160 }
161}
162
163impl StagedBlobBatch {
164 pub(crate) async fn stage<E>(
165 store_dir: &StoreDir,
166 blobs: Vec<NewBlob>,
167 ) -> Result<Self, HostWriteError<E>> {
168 let mut staged = Vec::new();
169 for blob in blobs {
170 match StagedBlob::stage(store_dir, blob).await {
171 Ok(blob) => staged.push(blob),
172 Err(error) => {
173 return Err(Self { blobs: staged }
174 .discard_after_stage_failure(error)
175 .await);
176 }
177 }
178 }
179 Ok(Self { blobs: staged })
180 }
181
182 async fn discard_after_stage_failure<E>(
183 self,
184 operation: HostWriteError<E>,
185 ) -> HostWriteError<E> {
186 let mut failures = Vec::new();
187 for blob in self.blobs {
188 let (namespace, id) = (blob.namespace.clone(), blob.id.clone());
189 if let Err(reason) = blob.discard().await {
190 failures.push(BlobFileFailure {
191 namespace: namespace.clone(),
192 id,
193 reason,
194 });
195 }
196 }
197 if failures.is_empty() {
198 operation
199 } else {
200 HostWriteError::BlobCleanupFailed {
201 operation: Box::new(operation),
202 cleanup: BlobFileFailures(failures),
203 }
204 }
205 }
206
207 pub(super) fn publish<E>(
208 &mut self,
209 mut validate: impl FnMut(&str, &str) -> Result<(), HostWriteError<E>>,
210 ) -> Result<(), HostWriteError<E>> {
211 for blob in &mut self.blobs {
212 validate(&blob.namespace, &blob.id)?;
213 blob.publish().map_err(HostWriteError::Blob)?;
214 }
215 Ok(())
216 }
217
218 pub(super) fn commit(self) {
219 for blob in self.blobs {
220 blob.commit();
221 }
222 }
223
224 pub(super) fn rollback<E>(self, write: HostWriteError<E>) -> HostWriteError<E> {
225 let mut failures = Vec::new();
226 for blob in self.blobs.into_iter().rev() {
227 let (namespace, id) = (blob.namespace.clone(), blob.id.clone());
228 for reason in blob.rollback() {
229 failures.push(BlobFileFailure {
230 namespace: namespace.clone(),
231 id: id.clone(),
232 reason,
233 });
234 }
235 }
236 if failures.is_empty() {
237 write
238 } else {
239 HostWriteError::WriteRollbackFailed {
240 write: Box::new(write),
241 rollback: BlobFileFailures(failures),
242 }
243 }
244 }
245}
246
247pub(super) type HostSql<R, E> = Box<
248 dyn for<'context, 'connection> FnOnce(SqlContext<'context, 'connection>) -> Result<R, E> + Send,
249>;
250
251pub struct HostWriteOperation<R, E> {
252 batch: WriteBatch,
253 sql: HostSql<R, E>,
254}
255
256impl<R, E> HostWriteOperation<R, E> {
257 pub fn new(
258 batch: WriteBatch,
259 sql: impl for<'context, 'connection> FnOnce(SqlContext<'context, 'connection>) -> Result<R, E>
260 + Send
261 + 'static,
262 ) -> Self {
263 Self {
264 batch,
265 sql: Box::new(sql),
266 }
267 }
268}
269
270#[derive(Debug, thiserror::Error)]
271pub enum HostWriteError<E> {
272 #[error("host write closure failed: {0}")]
273 Host(#[source] E),
274 #[error("database write failed: {0}")]
275 Database(#[source] DbError),
276 #[error("local blob write failed: {0}")]
277 Blob(#[source] coven_foundation::atomic_file::FileError),
278 #[error("blob declaration failed: {0}")]
279 BlobDeclaration(#[source] crate::BlobDeclError),
280 #[error("unsafe blob path: {0}")]
281 UnsafeBlobPath(#[source] PathTokenError),
282 #[error("the host write closure panicked")]
283 WriteClosurePanicked,
284 #[error(
285 "write failed: {write}; failed to remove installed local blobs during rollback: {rollback}"
286 )]
287 WriteRollbackFailed {
288 #[source]
289 write: Box<Self>,
290 rollback: BlobFileFailures,
291 },
292 #[error("write failed: {operation}; failed to remove unpublished local blobs: {cleanup}")]
293 BlobCleanupFailed {
294 #[source]
295 operation: Box<Self>,
296 cleanup: BlobFileFailures,
297 },
298 #[error("blob {namespace}/{id} is still referenced by a row after the write")]
299 BlobStillReferenced { namespace: String, id: String },
300 #[error("blob {namespace}/{id} is already referenced by a row")]
301 BlobAlreadyReferenced { namespace: String, id: String },
302 #[error("blob {namespace}/{id} is owned by an unpublished write")]
303 BlobOwnedByPendingWrite { namespace: String, id: String },
304 #[error("host write I/O failed: {0}")]
305 Io(#[source] std::io::Error),
306}
307
308impl<E> From<DbError> for HostWriteError<E> {
309 fn from(value: DbError) -> Self {
310 Self::Database(value)
311 }
312}
313
314impl<E> From<PathTokenError> for HostWriteError<E> {
315 fn from(value: PathTokenError) -> Self {
316 Self::UnsafeBlobPath(value)
317 }
318}
319
320impl<E> From<std::io::Error> for HostWriteError<E> {
321 fn from(value: std::io::Error) -> Self {
322 Self::Io(value)
323 }
324}
325
326#[derive(Clone)]
327pub struct StoreRowWrites {
328 database: StoreDatabase,
329}
330
331impl StoreSession<'_> {
332 fn execute_host_write<R, E>(
333 &mut self,
334 staged: StagedBlobBatch,
335 deleted: Vec<BlobRef>,
336 sql: HostSql<R, E>,
337 routing_encryption: Option<EncryptionService>,
338 blob_staging: Option<Box<dyn crate::AudienceBlobMoveStaging>>,
339 write_id: coven_protocol::write::WriteId,
340 ) -> Result<Result<WriteReceipt<R>, HostWriteError<E>>, DbError> {
341 let verified_authority = &mut *self.verified_store_authority;
342 let stamper = coven_protocol::hlc::UpdatedAtStamper::new(self.hlc.clone());
343 let result = super::host_write_capture::CapturedStoreWriteTransaction::begin_host(
344 self.conn,
345 self.store_dir,
346 self.synced_tables,
347 self.gates,
348 self.blob_decls,
349 routing_encryption.as_ref(),
350 blob_staging.as_deref(),
351 verified_authority,
352 write_id,
353 )
354 .map_err(HostWriteError::from)
355 .and_then(|transaction| transaction.execute_host(staged, deleted, sql, stamper));
356 Ok(result)
357 }
358}
359
360impl StoreRowWrites {
361 pub fn new(database: StoreDatabase) -> Self {
362 Self { database }
363 }
364
365 pub fn requires_routing_encryption(&self) -> bool {
366 self.database.has_scoped_graph()
367 }
368
369 pub fn subscribe_committed_changes(
370 &self,
371 ) -> tokio::sync::broadcast::Receiver<std::sync::Arc<crate::CommittedChanges>> {
372 self.database.subscribe_committed_changes()
373 }
374
375 pub async fn pending_writes(
376 &self,
377 ) -> Result<Vec<coven_protocol::write::PendingWrite>, DbError> {
378 self.database.pending_writes().await
379 }
380
381 pub async fn blocked_writes(
382 &self,
383 ) -> Result<Vec<coven_protocol::write::PendingWrite>, DbError> {
384 self.database.blocked_writes().await
385 }
386
387 pub async fn retry_blocked_write(
388 &self,
389 write_id: &coven_protocol::write::WriteId,
390 ) -> Result<Vec<coven_protocol::write::WriteId>, DbError> {
391 self.database.retry_blocked_write(write_id).await
392 }
393
394 pub async fn discard_blocked_write(
395 &self,
396 write_id: &coven_protocol::write::WriteId,
397 ) -> Result<super::BlockedWriteDiscard, DbError> {
398 self.database.discard_blocked_write(write_id).await
399 }
400
401 pub async fn write_status(
402 &self,
403 write_id: &coven_protocol::write::WriteId,
404 ) -> Result<coven_protocol::write::WriteStatus, DbError> {
405 self.database.write_status(write_id).await
406 }
407
408 pub async fn subscribe_write_status(
409 &self,
410 write_id: &coven_protocol::write::WriteId,
411 ) -> Result<tokio::sync::watch::Receiver<coven_protocol::write::WriteStatus>, DbError> {
412 self.database.subscribe_write_status(write_id).await
413 }
414
415 pub async fn execute<R, E>(
416 &self,
417 operation: HostWriteOperation<R, E>,
418 routing_encryption: Option<EncryptionService>,
419 blob_staging: Option<Box<dyn crate::AudienceBlobMoveStaging>>,
420 ) -> Result<WriteReceipt<R>, HostWriteError<E>>
421 where
422 R: Send + 'static,
423 E: Send + 'static,
424 {
425 let database = &self.database;
426 let HostWriteOperation { batch, sql } = operation;
427 let staged = database.stage_host_write_blobs(batch.new_blobs).await?;
428 let write_id = database.new_store_write_id();
429 let deleted = batch.deleted_blobs;
430
431 let outcome = database
432 .call_store(move |session| {
433 session.execute_host_write(
434 staged,
435 deleted,
436 sql,
437 routing_encryption,
438 blob_staging,
439 write_id,
440 )
441 })
442 .await;
443
444 let receipt = match outcome {
445 Ok(Ok(receipt)) => receipt,
446 Ok(Err(error)) => return Err(error),
447 Err(error) => return Err(HostWriteError::Database(error)),
448 };
449
450 if let Err(error) = super::local_blob_cleanup::LocalBlobCleanup::new(database)
451 .drain()
452 .await
453 {
454 tracing::warn!(
455 error = %error,
456 "failed to drain local blob cleanup intents after write commit"
457 );
458 }
459 Ok(receipt)
460 }
461
462 #[cfg(any(test, feature = "test-utils"))]
463 pub async fn store_write_partition_for_test(
464 &self,
465 write_id: &coven_protocol::write::WriteId,
466 ) -> Result<Vec<u8>, DbError> {
467 self.database.store_write_partition_for_test(write_id).await
468 }
469
470 #[cfg(any(test, feature = "test-utils"))]
471 pub async fn write_blob_lease_count_for_test(
472 &self,
473 write_id: &coven_protocol::write::WriteId,
474 ) -> Result<i64, DbError> {
475 self.database
476 .write_blob_lease_count_for_test(write_id)
477 .await
478 }
479
480 #[cfg(any(test, feature = "test-utils"))]
481 pub async fn store_write_journal_counts_for_test(&self) -> Result<(i64, i64), DbError> {
482 self.database.store_write_journal_counts_for_test().await
483 }
484
485 #[cfg(any(test, feature = "test-utils"))]
486 pub async fn cleanup_intent_count_for_test(
487 &self,
488 namespace: &str,
489 blob_id: &str,
490 ) -> Result<i64, DbError> {
491 self.database
492 .cleanup_intent_count_for_test(namespace, blob_id)
493 .await
494 }
495
496 #[cfg(any(test, feature = "test-utils"))]
497 pub async fn coven_table_exists_for_test(
498 &self,
499 table: crate::DatabaseTestTable,
500 ) -> Result<bool, DbError> {
501 self.database.coven_table_exists_for_test(table).await
502 }
503
504 #[cfg(any(test, feature = "test-utils"))]
505 pub async fn install_store_write_failure_trigger_for_test(&self) -> Result<(), DbError> {
506 self.database
507 .install_store_write_failure_trigger_for_test()
508 .await
509 }
510
511 #[cfg(any(test, feature = "test-utils"))]
512 pub async fn remove_store_write_failure_trigger_for_test(&self) -> Result<(), DbError> {
513 self.database
514 .remove_store_write_failure_trigger_for_test()
515 .await
516 }
517
518 #[cfg(any(test, feature = "test-utils"))]
519 pub async fn write_blob_facts_for_test(
520 &self,
521 write_id: coven_protocol::write::WriteId,
522 ) -> Result<String, DbError> {
523 self.database.write_blob_facts_for_test(write_id).await
524 }
525}