1use std::error::Error;
11use std::time::Duration;
12
13use super::types::{SavepointId, TransactionId, TransactionState};
14use linkrs_core::error::BoxedError;
15
16pub type TransactionResult<T> = Result<T, TransactionError>;
18
19#[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#[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 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 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#[derive(Debug, Clone)]
333pub struct RetryableTransactionError {
334 pub kind: TransactionErrorKind,
335 pub message: String,
336 pub suggested_backoff: Duration,
338 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}