Skip to main content

linkrs_transaction/
error.rs

1//! Transaction Error Type
2//!
3//! Transaction management related errors.
4//!
5//! ## Design
6//!
7//! `TransactionError` is a struct with boxed source error to keep size small (~24 bytes).
8//! This follows the same pattern as other error types for consistency.
9
10use std::error::Error;
11use std::time::Duration;
12
13use super::types::{SavepointId, TransactionId, TransactionState};
14use linkrs_core::error::BoxedError;
15
16/// Transaction operation result type alias
17pub type TransactionResult<T> = Result<T, TransactionError>;
18
19/// Transaction error kind enumeration
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum TransactionErrorKind {
22    BeginFailed,
23    CommitFailed,
24    AbortFailed,
25    TransactionNotFound,
26    TransactionNotOwner,
27    SavepointFailed,
28    SavepointNotFound,
29    SavepointNotActive,
30    NoSavepointsInTransaction,
31    InvalidStateTransition,
32    InvalidStateForCommit,
33    InvalidStateForAbort,
34    InvalidStateForExecution,
35    TransactionTimeout,
36    TransactionExpired,
37    RollbackFailed,
38    TooManyTransactions,
39    ReadOnlyTransaction,
40    WriteTransactionConflict,
41    RecoveryFailed,
42    PersistenceFailed,
43    SerializationFailed,
44    SyncFailed,
45    CheckpointInProgress,
46    CheckpointTimeout,
47    TransactionBudgetExceeded,
48    CommitVetoed,
49    SnapshotExpired,
50    Internal,
51}
52
53impl TransactionErrorKind {
54    pub fn as_str(&self) -> &'static str {
55        match self {
56            TransactionErrorKind::BeginFailed => "begin_failed",
57            TransactionErrorKind::CommitFailed => "commit_failed",
58            TransactionErrorKind::AbortFailed => "abort_failed",
59            TransactionErrorKind::TransactionNotFound => "transaction_not_found",
60            TransactionErrorKind::TransactionNotOwner => "transaction_not_owner",
61            TransactionErrorKind::SavepointFailed => "savepoint_failed",
62            TransactionErrorKind::SavepointNotFound => "savepoint_not_found",
63            TransactionErrorKind::SavepointNotActive => "savepoint_not_active",
64            TransactionErrorKind::NoSavepointsInTransaction => "no_savepoints_in_transaction",
65            TransactionErrorKind::InvalidStateTransition => "invalid_state_transition",
66            TransactionErrorKind::InvalidStateForCommit => "invalid_state_for_commit",
67            TransactionErrorKind::InvalidStateForAbort => "invalid_state_for_abort",
68            TransactionErrorKind::InvalidStateForExecution => "invalid_state_for_execution",
69            TransactionErrorKind::TransactionTimeout => "transaction_timeout",
70            TransactionErrorKind::TransactionExpired => "transaction_expired",
71            TransactionErrorKind::RollbackFailed => "rollback_failed",
72            TransactionErrorKind::TooManyTransactions => "too_many_transactions",
73            TransactionErrorKind::ReadOnlyTransaction => "read_only_transaction",
74            TransactionErrorKind::WriteTransactionConflict => "write_transaction_conflict",
75            TransactionErrorKind::RecoveryFailed => "recovery_failed",
76            TransactionErrorKind::PersistenceFailed => "persistence_failed",
77            TransactionErrorKind::SerializationFailed => "serialization_failed",
78            TransactionErrorKind::SyncFailed => "sync_failed",
79            TransactionErrorKind::CheckpointInProgress => "checkpoint_in_progress",
80            TransactionErrorKind::CheckpointTimeout => "checkpoint_timeout",
81            TransactionErrorKind::TransactionBudgetExceeded => "transaction_budget_exceeded",
82            TransactionErrorKind::CommitVetoed => "commit_vetoed",
83            TransactionErrorKind::SnapshotExpired => "snapshot_expired",
84            TransactionErrorKind::Internal => "internal",
85        }
86    }
87}
88
89/// Transaction Error Type
90///
91/// Design principles:
92/// 1. Small size: Uses boxed errors to keep struct size minimal (~24 bytes)
93/// 2. Full context: Preserves error chain
94/// 3. Clone support: Can be cloned for logging/propagation
95#[derive(Debug)]
96pub struct TransactionError {
97    kind: TransactionErrorKind,
98    message: String,
99    source: Option<BoxedError>,
100}
101
102impl TransactionError {
103    pub fn new(kind: TransactionErrorKind, message: impl Into<String>) -> Self {
104        Self {
105            kind,
106            message: message.into(),
107            source: None,
108        }
109    }
110
111    pub fn with_source(mut self, source: BoxedError) -> Self {
112        self.source = Some(source);
113        self
114    }
115
116    pub fn kind(&self) -> TransactionErrorKind {
117        self.kind
118    }
119
120    pub fn message(&self) -> &str {
121        &self.message
122    }
123
124    fn from_boxed<E: Error + Send + Sync + 'static>(kind: TransactionErrorKind, error: E) -> Self {
125        Self {
126            kind,
127            message: error.to_string(),
128            source: Some(Box::new(error)),
129        }
130    }
131
132    // Convenience constructors
133    pub fn begin_failed(message: impl Into<String>) -> Self {
134        Self::new(TransactionErrorKind::BeginFailed, message)
135    }
136
137    pub fn commit_failed(message: impl Into<String>) -> Self {
138        Self::new(TransactionErrorKind::CommitFailed, message)
139    }
140
141    pub fn commit_vetoed(reason: impl Into<String>) -> Self {
142        Self::new(TransactionErrorKind::CommitVetoed, reason)
143    }
144
145    pub fn abort_failed(message: impl Into<String>) -> Self {
146        Self::new(TransactionErrorKind::AbortFailed, message)
147    }
148
149    pub fn transaction_not_found(id: TransactionId) -> Self {
150        Self::new(
151            TransactionErrorKind::TransactionNotFound,
152            format!("Transaction not found: {}", id),
153        )
154    }
155
156    pub fn transaction_not_owner(id: TransactionId) -> Self {
157        Self::new(
158            TransactionErrorKind::TransactionNotOwner,
159            format!("Transaction {} is owned by another session", id),
160        )
161    }
162
163    pub fn savepoint_failed(message: impl Into<String>) -> Self {
164        Self::new(TransactionErrorKind::SavepointFailed, message)
165    }
166
167    pub fn savepoint_not_found(id: SavepointId) -> Self {
168        Self::new(
169            TransactionErrorKind::SavepointNotFound,
170            format!("Savepoint not found: {}", id),
171        )
172    }
173
174    pub fn savepoint_not_active(id: SavepointId) -> Self {
175        Self::new(
176            TransactionErrorKind::SavepointNotActive,
177            format!("Savepoint not active: {}", id),
178        )
179    }
180
181    pub fn no_savepoints_in_transaction() -> Self {
182        Self::new(
183            TransactionErrorKind::NoSavepointsInTransaction,
184            "No savepoints in transaction",
185        )
186    }
187
188    pub fn invalid_state_transition(from: TransactionState, to: TransactionState) -> Self {
189        Self::new(
190            TransactionErrorKind::InvalidStateTransition,
191            format!("Invalid state transition: from {} to {}", from, to),
192        )
193    }
194
195    pub fn invalid_state_for_commit(state: TransactionState) -> Self {
196        Self::new(
197            TransactionErrorKind::InvalidStateForCommit,
198            format!("Invalid state for commit: {}", state),
199        )
200    }
201
202    pub fn invalid_state_for_abort(state: TransactionState) -> Self {
203        Self::new(
204            TransactionErrorKind::InvalidStateForAbort,
205            format!("Invalid state for abort: {}", state),
206        )
207    }
208
209    pub fn invalid_state_for_execution(state: TransactionState) -> Self {
210        Self::new(
211            TransactionErrorKind::InvalidStateForExecution,
212            format!("Transaction not active, current state: {}", state),
213        )
214    }
215
216    pub fn transaction_timeout() -> Self {
217        Self::new(
218            TransactionErrorKind::TransactionTimeout,
219            "Transaction timeout",
220        )
221    }
222
223    pub fn transaction_expired() -> Self {
224        Self::new(
225            TransactionErrorKind::TransactionExpired,
226            "Transaction expired",
227        )
228    }
229
230    pub fn rollback_failed(message: impl Into<String>) -> Self {
231        Self::new(TransactionErrorKind::RollbackFailed, message)
232    }
233
234    pub fn too_many_transactions() -> Self {
235        Self::new(
236            TransactionErrorKind::TooManyTransactions,
237            "Too many concurrent transactions",
238        )
239    }
240
241    pub fn read_only_transaction() -> Self {
242        Self::new(
243            TransactionErrorKind::ReadOnlyTransaction,
244            "Read-only transaction",
245        )
246    }
247
248    pub fn write_transaction_conflict() -> Self {
249        Self::new(
250            TransactionErrorKind::WriteTransactionConflict,
251            "Write transaction conflict",
252        )
253    }
254
255    pub fn recovery_failed(message: impl Into<String>) -> Self {
256        Self::new(TransactionErrorKind::RecoveryFailed, message)
257    }
258
259    pub fn persistence_failed(message: impl Into<String>) -> Self {
260        Self::new(TransactionErrorKind::PersistenceFailed, message)
261    }
262
263    pub fn serialization_failed(message: impl Into<String>) -> Self {
264        Self::new(TransactionErrorKind::SerializationFailed, message)
265    }
266
267    pub fn sync_failed(message: impl Into<String>) -> Self {
268        Self::new(TransactionErrorKind::SyncFailed, message)
269    }
270
271    pub fn checkpoint_in_progress() -> Self {
272        Self::new(
273            TransactionErrorKind::CheckpointInProgress,
274            "New writes are paused during checkpoint",
275        )
276    }
277
278    pub fn checkpoint_timeout(active_writes: u64) -> Self {
279        Self::new(
280            TransactionErrorKind::CheckpointTimeout,
281            format!(
282                "Checkpoint timed out waiting for {} active writes to complete",
283                active_writes
284            ),
285        )
286    }
287
288    pub fn internal(message: impl Into<String>) -> Self {
289        Self::new(TransactionErrorKind::Internal, message)
290    }
291
292    pub fn transaction_budget_exceeded(resource: &str, current: u64, limit: u64) -> Self {
293        Self::new(
294            TransactionErrorKind::TransactionBudgetExceeded,
295            format!("Transaction budget exceeded: {resource} {current} > {limit}"),
296        )
297    }
298
299    pub fn snapshot_expired(holder: super::types::TransactionId, floor_ts: u64) -> Self {
300        Self::new(
301            TransactionErrorKind::SnapshotExpired,
302            format!(
303                "snapshot expired for long read txn={} floor_ts={floor_ts}; retry the read",
304                holder.0,
305            ),
306        )
307    }
308
309    pub fn is_timeout(&self) -> bool {
310        matches!(
311            self.kind,
312            TransactionErrorKind::TransactionTimeout | TransactionErrorKind::TransactionExpired
313        )
314    }
315
316    /// Returns true when this error indicates a transient failure that the client
317    /// may retry by re-executing the entire transaction.
318    pub fn is_retryable(&self) -> bool {
319        matches!(
320            self.kind,
321            TransactionErrorKind::WriteTransactionConflict
322                | TransactionErrorKind::SerializationFailed
323                | TransactionErrorKind::SnapshotExpired
324        )
325    }
326}
327
328/// Details for a retryable transaction failure.
329///
330/// Returned to the client when a transaction fails due to a conflict or
331/// serialization error, providing enough information to implement retry logic.
332#[derive(Debug, Clone)]
333pub struct RetryableTransactionError {
334    pub kind: TransactionErrorKind,
335    pub message: String,
336    /// Suggested backoff duration before retrying.
337    pub suggested_backoff: Duration,
338    /// Number of retries that have already been attempted.
339    pub attempt: u32,
340}
341
342impl RetryableTransactionError {
343    pub fn new(kind: TransactionErrorKind, message: impl Into<String>, attempt: u32) -> Self {
344        let suggested_backoff = Duration::from_millis(100 * 2_u64.pow(attempt.min(6)));
345        Self {
346            kind,
347            message: message.into(),
348            suggested_backoff,
349            attempt,
350        }
351    }
352
353    pub fn from_transaction_error(error: &TransactionError, attempt: u32) -> Option<Self> {
354        if error.is_retryable() {
355            Some(Self::new(
356                error.kind(),
357                error.message().to_string(),
358                attempt,
359            ))
360        } else {
361            None
362        }
363    }
364}
365
366impl std::fmt::Display for RetryableTransactionError {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        write!(
369            f,
370            "[{}] {} (attempt {}, retry after {:?})",
371            self.kind.as_str(),
372            self.message,
373            self.attempt,
374            self.suggested_backoff
375        )
376    }
377}
378
379impl std::error::Error for RetryableTransactionError {}
380
381impl std::fmt::Display for TransactionError {
382    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383        write!(f, "[{}] {}", self.kind.as_str(), self.message)?;
384        if let Some(ref source) = self.source {
385            write!(f, "\n  Caused by: {}", source)?;
386        }
387        Ok(())
388    }
389}
390
391impl Clone for TransactionError {
392    fn clone(&self) -> Self {
393        Self {
394            kind: self.kind,
395            message: self.message.clone(),
396            source: None,
397        }
398    }
399}
400
401impl std::error::Error for TransactionError {
402    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
403        self.source
404            .as_ref()
405            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
406    }
407}
408
409impl From<std::io::Error> for TransactionError {
410    fn from(e: std::io::Error) -> Self {
411        Self::from_boxed(TransactionErrorKind::Internal, e)
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_transaction_error_size() {
421        assert!(
422            std::mem::size_of::<TransactionError>() <= 64,
423            "TransactionError should be small, got {} bytes",
424            std::mem::size_of::<TransactionError>()
425        );
426    }
427
428    #[test]
429    fn test_transaction_error_creation() {
430        let err = TransactionError::transaction_not_found(TransactionId(123));
431        assert_eq!(err.kind(), TransactionErrorKind::TransactionNotFound);
432        assert!(err.message().contains("123"));
433    }
434}