Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 39 additions & 21 deletions cas-storage/src/cas/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,8 @@ impl CasFS {
// - if the block does not exist, we need to write it to the storage
// 2. write the actual block to disk
//
// we commit the meta database transaction after writing the block to disk
// we commit the meta database transaction BEFORE writing the block to disk
// to avoid holding the lock during slow I/O operations.
//
// IMPORTANT: In multi-user mode, use shared MetaStore for block transactions
// to ensure blocks are written to the shared _BLOCKS tree, not user-specific tree
Expand All @@ -593,6 +594,7 @@ impl CasFS {
// the block already exists, no need to write it to the storage
pm.block_ignored();

tracing::debug!(target: "cas_storage::locks", "Committing metadata transaction (block exists)");
Box::new(store_tx).commit().unwrap();

if let Err(e) = tx.unbounded_send(Ok((idx, block_hash))) {
Expand All @@ -603,18 +605,48 @@ impl CasFS {
Ok((true, block)) => {
// the block does not exist, we need to write it to the storage
pm.block_pending();

// COMMIT IMMEDIATELY to release lock
tracing::debug!(target: "cas_storage::locks", "Committing metadata transaction (new block)");
Box::new(store_tx).commit().unwrap();

block
}
};

let mut store_tx = Some(store_tx);
// write the actual block to disk
// if the disk operation fails, the database transaction is rolled back.
// if the disk operation fails, we must manually rollback (compensating transaction)
let block_path = block.disk_path(self.root.clone());
if let Err(e) = self.async_fs.create_dir_all(block_path.parent().unwrap()) {
if let Some(store_tx) = store_tx.take() {
Box::new(store_tx).rollback();

// Helper to cleanup on failure
let cleanup_on_failure = || {
// We need to delete the block we just added.
// Since we just added it with rc=1, we can just delete it.
// We accept potential data leakage here if this cleanup fails,
// as per the design principles (leakage is better than data loss).

// We need to access the block tree to remove the block.
// In multi-user mode, this is in the shared store.
let block_tree = match &self.shared_meta_store {
Some(shared_store) => shared_store.get_block_tree(),
None => self.user_meta_store.get_block_tree(),
};

if let Ok(tree) = block_tree {
// We can try to remove it directly from the tree.
// This bypasses the transaction for deletion, but since we know
// we are the only ones who just added it (rc=1), and we are failing,
// it should be safe to remove.
if let Err(e) = tree.remove(&block_hash) {
tracing::warn!(block = %hex_string(&block_hash), error = %e, "Failed to cleanup orphan block metadata");
} else {
tracing::debug!(block = %hex_string(&block_hash), "Cleaned up orphan block metadata");
}
}
};

if let Err(e) = self.async_fs.create_dir_all(block_path.parent().unwrap()) {
cleanup_on_failure();

if let Err(e) = tx.unbounded_send(Err(e)) {
pm.block_write_error();
Expand All @@ -623,9 +655,7 @@ impl CasFS {
}
}
if let Err(e) = self.async_fs.write(&block_path, &bytes) {
if let Some(store_tx) = store_tx.take() {
Box::new(store_tx).rollback();
}
cleanup_on_failure();

if let Err(e) = tx.unbounded_send(Err(e)) {
pm.block_write_error();
Expand All @@ -634,18 +664,6 @@ impl CasFS {
}
}

// commit the database transaction
if let Some(store_tx) = store_tx.take() {
if let Err(err) = Box::new(store_tx).commit() {
// TODO FIXME if the transaction fails, we need to delete the block from the storage
if let Err(e) = tx.unbounded_send(Err(err.into())) {
pm.block_write_error();
tracing::error!(error = %e, "Could not send transaction error");
}
return;
}
}

pm.block_written(bytes.len());

if let Err(e) = tx.unbounded_send(Ok((idx, block_hash))) {
Expand Down
6 changes: 3 additions & 3 deletions cas-storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
//! Default::default(), // metrics
//! StorageEngine::Fjall,
//! None, // inline_metadata_size
//! Some(Durability::Immediate),
//! Some(Durability::Fsync),
//! );
//!
//! // Create bucket
Expand Down Expand Up @@ -56,7 +56,7 @@
//! PathBuf::from("./data/meta"),
//! StorageEngine::Fjall,
//! None,
//! Some(Durability::Immediate),
//! Some(Durability::Fsync),
//! )?);
//!
//! // Create per-user CasFS instances
Expand All @@ -70,7 +70,7 @@
//! Default::default(),
//! StorageEngine::Fjall,
//! None,
//! Some(Durability::Immediate),
//! Some(Durability::Fsync),
//! );
//! # Ok(())
//! # }
Expand Down
2 changes: 1 addition & 1 deletion cas-storage/src/metastore/meta_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ impl BlockTree {
///
/// # Returns
/// Success or an error if the removal fails
fn remove(&self, key: &[u8]) -> Result<(), MetaError> {
pub fn remove(&self, key: &[u8]) -> Result<(), MetaError> {
self.tree.remove(key)
}

Expand Down
7 changes: 6 additions & 1 deletion cas-storage/src/metastore/stores/fjall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl Store for FjallStore {
}

fn begin_transaction(&self) -> Transaction {
tracing::debug!(target: "cas_storage::locks", "Transaction started");
// Use unsafe to extend lifetime to 'static since the transaction
// won't outlive the store
let tx = unsafe {
Expand Down Expand Up @@ -160,7 +161,10 @@ unsafe impl Sync for FjallTransaction {}
impl TransactionBackend for FjallTransaction {
fn commit(&mut self) -> Result<(), MetaError> {
if let Some(tx) = self.tx.take() {
self.store.commit_persist(tx)
tracing::debug!(target: "cas_storage::locks", "Transaction commit started");
let res = self.store.commit_persist(tx);
tracing::debug!(target: "cas_storage::locks", "Transaction commit finished");
res
} else {
Err(MetaError::TransactionError(
"Transaction already rolled back".to_string(),
Expand All @@ -170,6 +174,7 @@ impl TransactionBackend for FjallTransaction {

fn rollback(&mut self) {
if let Some(tx) = self.tx.take() {
tracing::debug!(target: "cas_storage::locks", "Transaction rollback");
tx.rollback();
}
}
Expand Down
Loading