use std::error::Error;
use std::time::Duration;
use super::types::{SavepointId, TransactionId, TransactionState};
use linkrs_core::error::BoxedError;
pub type TransactionResult<T> = Result<T, TransactionError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransactionErrorKind {
BeginFailed,
CommitFailed,
AbortFailed,
TransactionNotFound,
TransactionNotOwner,
SavepointFailed,
SavepointNotFound,
SavepointNotActive,
NoSavepointsInTransaction,
InvalidStateTransition,
InvalidStateForCommit,
InvalidStateForAbort,
InvalidStateForExecution,
TransactionTimeout,
TransactionExpired,
RollbackFailed,
TooManyTransactions,
ReadOnlyTransaction,
WriteTransactionConflict,
RecoveryFailed,
PersistenceFailed,
SerializationFailed,
SyncFailed,
CheckpointInProgress,
CheckpointTimeout,
TransactionBudgetExceeded,
CommitVetoed,
SnapshotExpired,
Internal,
}
impl TransactionErrorKind {
pub fn as_str(&self) -> &'static str {
match self {
TransactionErrorKind::BeginFailed => "begin_failed",
TransactionErrorKind::CommitFailed => "commit_failed",
TransactionErrorKind::AbortFailed => "abort_failed",
TransactionErrorKind::TransactionNotFound => "transaction_not_found",
TransactionErrorKind::TransactionNotOwner => "transaction_not_owner",
TransactionErrorKind::SavepointFailed => "savepoint_failed",
TransactionErrorKind::SavepointNotFound => "savepoint_not_found",
TransactionErrorKind::SavepointNotActive => "savepoint_not_active",
TransactionErrorKind::NoSavepointsInTransaction => "no_savepoints_in_transaction",
TransactionErrorKind::InvalidStateTransition => "invalid_state_transition",
TransactionErrorKind::InvalidStateForCommit => "invalid_state_for_commit",
TransactionErrorKind::InvalidStateForAbort => "invalid_state_for_abort",
TransactionErrorKind::InvalidStateForExecution => "invalid_state_for_execution",
TransactionErrorKind::TransactionTimeout => "transaction_timeout",
TransactionErrorKind::TransactionExpired => "transaction_expired",
TransactionErrorKind::RollbackFailed => "rollback_failed",
TransactionErrorKind::TooManyTransactions => "too_many_transactions",
TransactionErrorKind::ReadOnlyTransaction => "read_only_transaction",
TransactionErrorKind::WriteTransactionConflict => "write_transaction_conflict",
TransactionErrorKind::RecoveryFailed => "recovery_failed",
TransactionErrorKind::PersistenceFailed => "persistence_failed",
TransactionErrorKind::SerializationFailed => "serialization_failed",
TransactionErrorKind::SyncFailed => "sync_failed",
TransactionErrorKind::CheckpointInProgress => "checkpoint_in_progress",
TransactionErrorKind::CheckpointTimeout => "checkpoint_timeout",
TransactionErrorKind::TransactionBudgetExceeded => "transaction_budget_exceeded",
TransactionErrorKind::CommitVetoed => "commit_vetoed",
TransactionErrorKind::SnapshotExpired => "snapshot_expired",
TransactionErrorKind::Internal => "internal",
}
}
}
#[derive(Debug)]
pub struct TransactionError {
kind: TransactionErrorKind,
message: String,
source: Option<BoxedError>,
}
impl TransactionError {
pub fn new(kind: TransactionErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
source: None,
}
}
pub fn with_source(mut self, source: BoxedError) -> Self {
self.source = Some(source);
self
}
pub fn kind(&self) -> TransactionErrorKind {
self.kind
}
pub fn message(&self) -> &str {
&self.message
}
fn from_boxed<E: Error + Send + Sync + 'static>(kind: TransactionErrorKind, error: E) -> Self {
Self {
kind,
message: error.to_string(),
source: Some(Box::new(error)),
}
}
pub fn begin_failed(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::BeginFailed, message)
}
pub fn commit_failed(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::CommitFailed, message)
}
pub fn commit_vetoed(reason: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::CommitVetoed, reason)
}
pub fn abort_failed(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::AbortFailed, message)
}
pub fn transaction_not_found(id: TransactionId) -> Self {
Self::new(
TransactionErrorKind::TransactionNotFound,
format!("Transaction not found: {}", id),
)
}
pub fn transaction_not_owner(id: TransactionId) -> Self {
Self::new(
TransactionErrorKind::TransactionNotOwner,
format!("Transaction {} is owned by another session", id),
)
}
pub fn savepoint_failed(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::SavepointFailed, message)
}
pub fn savepoint_not_found(id: SavepointId) -> Self {
Self::new(
TransactionErrorKind::SavepointNotFound,
format!("Savepoint not found: {}", id),
)
}
pub fn savepoint_not_active(id: SavepointId) -> Self {
Self::new(
TransactionErrorKind::SavepointNotActive,
format!("Savepoint not active: {}", id),
)
}
pub fn no_savepoints_in_transaction() -> Self {
Self::new(
TransactionErrorKind::NoSavepointsInTransaction,
"No savepoints in transaction",
)
}
pub fn invalid_state_transition(from: TransactionState, to: TransactionState) -> Self {
Self::new(
TransactionErrorKind::InvalidStateTransition,
format!("Invalid state transition: from {} to {}", from, to),
)
}
pub fn invalid_state_for_commit(state: TransactionState) -> Self {
Self::new(
TransactionErrorKind::InvalidStateForCommit,
format!("Invalid state for commit: {}", state),
)
}
pub fn invalid_state_for_abort(state: TransactionState) -> Self {
Self::new(
TransactionErrorKind::InvalidStateForAbort,
format!("Invalid state for abort: {}", state),
)
}
pub fn invalid_state_for_execution(state: TransactionState) -> Self {
Self::new(
TransactionErrorKind::InvalidStateForExecution,
format!("Transaction not active, current state: {}", state),
)
}
pub fn transaction_timeout() -> Self {
Self::new(
TransactionErrorKind::TransactionTimeout,
"Transaction timeout",
)
}
pub fn transaction_expired() -> Self {
Self::new(
TransactionErrorKind::TransactionExpired,
"Transaction expired",
)
}
pub fn rollback_failed(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::RollbackFailed, message)
}
pub fn too_many_transactions() -> Self {
Self::new(
TransactionErrorKind::TooManyTransactions,
"Too many concurrent transactions",
)
}
pub fn read_only_transaction() -> Self {
Self::new(
TransactionErrorKind::ReadOnlyTransaction,
"Read-only transaction",
)
}
pub fn write_transaction_conflict() -> Self {
Self::new(
TransactionErrorKind::WriteTransactionConflict,
"Write transaction conflict",
)
}
pub fn recovery_failed(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::RecoveryFailed, message)
}
pub fn persistence_failed(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::PersistenceFailed, message)
}
pub fn serialization_failed(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::SerializationFailed, message)
}
pub fn sync_failed(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::SyncFailed, message)
}
pub fn checkpoint_in_progress() -> Self {
Self::new(
TransactionErrorKind::CheckpointInProgress,
"New writes are paused during checkpoint",
)
}
pub fn checkpoint_timeout(active_writes: u64) -> Self {
Self::new(
TransactionErrorKind::CheckpointTimeout,
format!(
"Checkpoint timed out waiting for {} active writes to complete",
active_writes
),
)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(TransactionErrorKind::Internal, message)
}
pub fn transaction_budget_exceeded(resource: &str, current: u64, limit: u64) -> Self {
Self::new(
TransactionErrorKind::TransactionBudgetExceeded,
format!("Transaction budget exceeded: {resource} {current} > {limit}"),
)
}
pub fn snapshot_expired(holder: super::types::TransactionId, floor_ts: u64) -> Self {
Self::new(
TransactionErrorKind::SnapshotExpired,
format!(
"snapshot expired for long read txn={} floor_ts={floor_ts}; retry the read",
holder.0,
),
)
}
pub fn is_timeout(&self) -> bool {
matches!(
self.kind,
TransactionErrorKind::TransactionTimeout | TransactionErrorKind::TransactionExpired
)
}
pub fn is_retryable(&self) -> bool {
matches!(
self.kind,
TransactionErrorKind::WriteTransactionConflict
| TransactionErrorKind::SerializationFailed
| TransactionErrorKind::SnapshotExpired
)
}
}
#[derive(Debug, Clone)]
pub struct RetryableTransactionError {
pub kind: TransactionErrorKind,
pub message: String,
pub suggested_backoff: Duration,
pub attempt: u32,
}
impl RetryableTransactionError {
pub fn new(kind: TransactionErrorKind, message: impl Into<String>, attempt: u32) -> Self {
let suggested_backoff = Duration::from_millis(100 * 2_u64.pow(attempt.min(6)));
Self {
kind,
message: message.into(),
suggested_backoff,
attempt,
}
}
pub fn from_transaction_error(error: &TransactionError, attempt: u32) -> Option<Self> {
if error.is_retryable() {
Some(Self::new(
error.kind(),
error.message().to_string(),
attempt,
))
} else {
None
}
}
}
impl std::fmt::Display for RetryableTransactionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[{}] {} (attempt {}, retry after {:?})",
self.kind.as_str(),
self.message,
self.attempt,
self.suggested_backoff
)
}
}
impl std::error::Error for RetryableTransactionError {}
impl std::fmt::Display for TransactionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.kind.as_str(), self.message)?;
if let Some(ref source) = self.source {
write!(f, "\n Caused by: {}", source)?;
}
Ok(())
}
}
impl Clone for TransactionError {
fn clone(&self) -> Self {
Self {
kind: self.kind,
message: self.message.clone(),
source: None,
}
}
}
impl std::error::Error for TransactionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
}
}
impl From<std::io::Error> for TransactionError {
fn from(e: std::io::Error) -> Self {
Self::from_boxed(TransactionErrorKind::Internal, e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transaction_error_size() {
assert!(
std::mem::size_of::<TransactionError>() <= 64,
"TransactionError should be small, got {} bytes",
std::mem::size_of::<TransactionError>()
);
}
#[test]
fn test_transaction_error_creation() {
let err = TransactionError::transaction_not_found(TransactionId(123));
assert_eq!(err.kind(), TransactionErrorKind::TransactionNotFound);
assert!(err.message().contains("123"));
}
}