use crate::error::{MeiliBridgeError, Result};
use crate::models::Position;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::time::{timeout, Duration};
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone, PartialEq)]
pub enum TransactionState {
Started,
Prepared,
Committed,
Aborted,
}
#[derive(Debug, Clone)]
pub struct Transaction {
pub id: String,
pub task_id: String,
pub state: TransactionState,
pub position: Option<Position>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub participants: Vec<String>,
}
#[derive(Debug)]
pub struct TwoPhaseCommit {
timeout_secs: u64,
transactions: Arc<RwLock<HashMap<String, Transaction>>>,
}
impl TwoPhaseCommit {
pub fn new(timeout_secs: u64) -> Self {
Self {
timeout_secs,
transactions: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn begin(&self, transaction_id: String, task_id: String) -> Result<()> {
let mut txns = self.transactions.write().await;
if txns.contains_key(&transaction_id) {
return Err(MeiliBridgeError::Pipeline(format!(
"Transaction {} already exists",
transaction_id
)));
}
let transaction = Transaction {
id: transaction_id.clone(),
task_id,
state: TransactionState::Started,
position: None,
created_at: chrono::Utc::now(),
participants: vec!["checkpoint".to_string(), "meilisearch".to_string()],
};
txns.insert(transaction_id.clone(), transaction);
info!("Started 2PC transaction: {}", transaction_id);
Ok(())
}
pub async fn prepare(&self, transaction_id: &str, position: Position) -> Result<bool> {
let mut txns = self.transactions.write().await;
let txn = txns.get_mut(transaction_id).ok_or_else(|| {
MeiliBridgeError::Pipeline(format!("Transaction {} not found", transaction_id))
})?;
if txn.state != TransactionState::Started {
return Err(MeiliBridgeError::Pipeline(format!(
"Transaction {} in invalid state: {:?}",
transaction_id, txn.state
)));
}
txn.position = Some(position);
txn.state = TransactionState::Prepared;
info!("Prepared 2PC transaction: {}", transaction_id);
Ok(true)
}
pub async fn commit(&self, transaction_id: &str) -> Result<()> {
let mut txns = self.transactions.write().await;
let txn = txns.get_mut(transaction_id).ok_or_else(|| {
MeiliBridgeError::Pipeline(format!("Transaction {} not found", transaction_id))
})?;
if txn.state != TransactionState::Prepared {
return Err(MeiliBridgeError::Pipeline(format!(
"Transaction {} not prepared: {:?}",
transaction_id, txn.state
)));
}
txn.state = TransactionState::Committed;
info!("Committed 2PC transaction: {}", transaction_id);
let transaction_id = transaction_id.to_string();
let transactions = self.transactions.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(5)).await;
let mut txns = transactions.write().await;
txns.remove(&transaction_id);
});
Ok(())
}
pub async fn abort(&self, transaction_id: &str) -> Result<()> {
let mut txns = self.transactions.write().await;
if let Some(txn) = txns.get_mut(transaction_id) {
txn.state = TransactionState::Aborted;
warn!("Aborted 2PC transaction: {}", transaction_id);
}
txns.remove(transaction_id);
Ok(())
}
pub async fn cleanup_stale_transactions(&self) -> Result<()> {
let mut txns = self.transactions.write().await;
let now = chrono::Utc::now();
let timeout = chrono::Duration::seconds(self.timeout_secs as i64);
let stale_txns: Vec<String> = txns
.iter()
.filter(|(_, txn)| {
now - txn.created_at > timeout && txn.state != TransactionState::Committed
})
.map(|(id, _)| id.clone())
.collect();
for txn_id in stale_txns {
error!("Aborting stale transaction: {}", txn_id);
txns.remove(&txn_id);
}
Ok(())
}
}
pub struct TransactionCoordinator {
two_phase_commit: TwoPhaseCommit,
checkpoint_handler: Arc<RwLock<Option<Arc<super::checkpoint::TransactionalCheckpoint>>>>,
}
impl TransactionCoordinator {
pub fn new(timeout_secs: u64) -> Self {
Self {
two_phase_commit: TwoPhaseCommit::new(timeout_secs),
checkpoint_handler: Arc::new(RwLock::new(None)),
}
}
pub async fn set_checkpoint_handler(
&self,
handler: Arc<super::checkpoint::TransactionalCheckpoint>,
) {
let mut checkpoint = self.checkpoint_handler.write().await;
*checkpoint = Some(handler);
}
pub async fn begin(&self, task_id: &str) -> Result<String> {
let transaction_id = format!("{}-{}", task_id, uuid::Uuid::new_v4());
self.two_phase_commit
.begin(transaction_id.clone(), task_id.to_string())
.await?;
debug!("Started coordinated transaction: {}", transaction_id);
Ok(transaction_id)
}
pub async fn prepare(&self, transaction_id: &str, position: Position) -> Result<bool> {
let checkpoint_handler = self.checkpoint_handler.read().await;
if let Some(handler) = checkpoint_handler.as_ref() {
handler
.begin_transaction(
transaction_id.to_string(),
transaction_id.split('-').next().unwrap_or("").to_string(),
position.clone(),
)
.await?;
let prepare_future = handler.prepare(transaction_id);
match timeout(Duration::from_secs(10), prepare_future).await {
Ok(Ok(true)) => {
debug!("Checkpoint prepared for transaction: {}", transaction_id);
}
Ok(Ok(false)) => {
warn!(
"Checkpoint prepare failed for transaction: {}",
transaction_id
);
return Ok(false);
}
Ok(Err(e)) => {
error!("Checkpoint prepare error: {}", e);
return Ok(false);
}
Err(_) => {
error!("Checkpoint prepare timeout");
return Ok(false);
}
}
}
self.two_phase_commit
.prepare(transaction_id, position)
.await?;
Ok(true)
}
pub async fn commit(&self, transaction_id: &str) -> Result<()> {
let checkpoint_handler = self.checkpoint_handler.read().await;
if let Some(handler) = checkpoint_handler.as_ref() {
handler.commit(transaction_id).await?;
}
self.two_phase_commit.commit(transaction_id).await?;
info!("Committed coordinated transaction: {}", transaction_id);
Ok(())
}
pub async fn rollback(&self, transaction_id: &str) -> Result<()> {
let checkpoint_handler = self.checkpoint_handler.read().await;
if let Some(handler) = checkpoint_handler.as_ref() {
if let Err(e) = handler.rollback(transaction_id).await {
error!("Failed to rollback checkpoint: {}", e);
}
}
self.two_phase_commit.abort(transaction_id).await?;
warn!("Rolled back coordinated transaction: {}", transaction_id);
Ok(())
}
pub async fn run_cleanup_task(&self) {
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
interval.tick().await;
if let Err(e) = self.two_phase_commit.cleanup_stale_transactions().await {
error!("Failed to cleanup stale transactions: {}", e);
}
let checkpoint_handler = self.checkpoint_handler.read().await;
if let Some(handler) = checkpoint_handler.as_ref() {
if let Err(e) = handler
.cleanup_stale_transactions(self.two_phase_commit.timeout_secs)
.await
{
error!("Failed to cleanup stale checkpoint transactions: {}", e);
}
}
}
}
}