use crate::checkpoint::storage::CheckpointStorage;
use crate::error::{MeiliBridgeError, Result};
use crate::models::{Checkpoint, Position};
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::{debug, error, info};
#[derive(Debug, Clone)]
pub struct CheckpointTransaction {
pub transaction_id: String,
pub task_id: String,
pub position: Position,
pub prepared: bool,
pub committed: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
}
pub struct TransactionalCheckpoint {
storage: Arc<dyn CheckpointStorage>,
transactions: Arc<Mutex<Vec<CheckpointTransaction>>>,
}
impl TransactionalCheckpoint {
pub fn new(storage: Arc<dyn CheckpointStorage>) -> Self {
Self {
storage,
transactions: Arc::new(Mutex::new(Vec::new())),
}
}
pub async fn begin_transaction(
&self,
transaction_id: String,
task_id: String,
position: Position,
) -> Result<()> {
let mut txns = self.transactions.lock().await;
if txns.iter().any(|t| t.transaction_id == transaction_id) {
return Err(MeiliBridgeError::Pipeline(format!(
"Transaction {} already exists",
transaction_id
)));
}
let transaction = CheckpointTransaction {
transaction_id: transaction_id.clone(),
task_id,
position,
prepared: false,
committed: false,
created_at: chrono::Utc::now(),
};
txns.push(transaction);
info!("Started checkpoint transaction: {}", transaction_id);
Ok(())
}
pub async fn prepare(&self, transaction_id: &str) -> Result<bool> {
let mut txns = self.transactions.lock().await;
let txn = txns
.iter_mut()
.find(|t| t.transaction_id == transaction_id)
.ok_or_else(|| {
MeiliBridgeError::Pipeline(format!("Transaction {} not found", transaction_id))
})?;
if txn.prepared {
debug!("Transaction {} already prepared", transaction_id);
return Ok(true);
}
let checkpoint = Checkpoint {
id: uuid::Uuid::new_v4().to_string(),
task_id: txn.task_id.clone(),
position: txn.position.clone(),
created_at: chrono::Utc::now(),
stats: crate::models::ProgressStats::new(),
metadata: serde_json::json!({
"transaction_id": transaction_id,
"prepared": true,
"committed": false,
}),
};
match self.storage.save(&checkpoint).await {
Ok(_) => {
txn.prepared = true;
info!("Prepared checkpoint transaction: {}", transaction_id);
Ok(true)
}
Err(e) => {
error!(
"Failed to prepare checkpoint transaction {}: {}",
transaction_id, e
);
Ok(false)
}
}
}
pub async fn commit(&self, transaction_id: &str) -> Result<()> {
let mut txns = self.transactions.lock().await;
let txn_index = txns
.iter()
.position(|t| t.transaction_id == transaction_id)
.ok_or_else(|| {
MeiliBridgeError::Pipeline(format!("Transaction {} not found", transaction_id))
})?;
let txn = &mut txns[txn_index];
if !txn.prepared {
return Err(MeiliBridgeError::Pipeline(format!(
"Transaction {} not prepared",
transaction_id
)));
}
if txn.committed {
debug!("Transaction {} already committed", transaction_id);
return Ok(());
}
let checkpoint = self.storage.load(&txn.task_id).await?.ok_or_else(|| {
MeiliBridgeError::Pipeline(format!("Checkpoint not found for task {}", txn.task_id))
})?;
let mut updated_checkpoint = checkpoint;
updated_checkpoint.metadata = serde_json::json!({
"transaction_id": transaction_id,
"prepared": true,
"committed": true,
"committed_at": chrono::Utc::now(),
});
self.storage.save(&updated_checkpoint).await?;
txns.remove(txn_index);
info!("Committed checkpoint transaction: {}", transaction_id);
Ok(())
}
pub async fn rollback(&self, transaction_id: &str) -> Result<()> {
let mut txns = self.transactions.lock().await;
let txn_index = txns
.iter()
.position(|t| t.transaction_id == transaction_id)
.ok_or_else(|| {
MeiliBridgeError::Pipeline(format!("Transaction {} not found", transaction_id))
})?;
let txn = &txns[txn_index];
if txn.prepared && !txn.committed {
if let Err(e) = self.storage.delete(&txn.task_id).await {
error!("Failed to delete checkpoint during rollback: {}", e);
}
}
txns.remove(txn_index);
info!("Rolled back checkpoint transaction: {}", transaction_id);
Ok(())
}
pub async fn cleanup_stale_transactions(&self, timeout_secs: u64) -> Result<()> {
let mut txns = self.transactions.lock().await;
let now = chrono::Utc::now();
let timeout = chrono::Duration::seconds(timeout_secs as i64);
let stale_txns: Vec<String> = txns
.iter()
.filter(|t| now - t.created_at > timeout)
.map(|t| t.transaction_id.clone())
.collect();
for txn_id in stale_txns {
if let Some(index) = txns.iter().position(|t| t.transaction_id == txn_id) {
let txn = &txns[index];
error!("Cleaning up stale transaction: {}", txn_id);
if txn.prepared && !txn.committed {
if let Err(e) = self.storage.delete(&txn.task_id).await {
error!("Failed to cleanup stale checkpoint: {}", e);
}
}
txns.remove(index);
}
}
Ok(())
}
pub async fn get_latest_committed(&self, task_id: &str) -> Result<Option<Checkpoint>> {
let checkpoint = self.storage.load(task_id).await?;
if let Some(cp) = checkpoint {
if let Some(committed) = cp.metadata.get("committed").and_then(|v| v.as_bool()) {
if committed {
return Ok(Some(cp));
}
}
}
Ok(None)
}
}