1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
//! Error types for Persistent Adaptive Radix Trie
//!
//! This module defines all error types that can occur during persistent
//! dictionary operations, including:
//!
//! - I/O errors (file operations, memory mapping)
//! - Format errors (invalid magic, unsupported version)
//! - Corruption errors (checksum mismatch, invalid structure)
//! - Concurrency errors (lock poisoning, swizzle failures)
//! - Resource errors (out of space, buffer pool exhausted)
use std::io;
use thiserror::Error;
/// Result type alias for persistent ARTrie operations
pub type Result<T> = std::result::Result<T, PersistentARTrieError>;
/// Errors that can occur during persistent ARTrie operations
#[derive(Error, Debug)]
pub enum PersistentARTrieError {
/// I/O error during file operations
#[error("I/O error during {operation} on '{path}': {source}")]
IoError {
/// What operation was being performed
operation: String,
/// Path to the file (if applicable)
path: String,
/// Underlying I/O error
#[source]
source: io::Error,
},
/// Memory mapping error
#[error("Memory map error during {operation}: {source}")]
MmapError {
/// What operation was being performed
operation: String,
/// Underlying I/O error
#[source]
source: io::Error,
},
/// Invalid magic number in file header
#[error("Invalid magic number: expected 0x{expected:016X}, found 0x{found:016X}")]
InvalidMagic {
/// Expected magic number
expected: u64,
/// Actual magic number found
found: u64,
},
/// Unsupported file format version
#[error("Unsupported file version: max supported {max_supported}, found {found}")]
UnsupportedVersion {
/// Maximum version we can read
max_supported: u32,
/// Version found in file
found: u32,
},
/// File corruption detected
#[error("Corrupted file: {reason}")]
CorruptedFile {
/// Description of corruption
reason: String,
},
/// Checksum verification failed
#[error(
"Checksum mismatch in block {block_id}: expected 0x{expected:016X}, found 0x{found:016X}"
)]
ChecksumMismatch {
/// Block that failed verification
block_id: u32,
/// Expected checksum
expected: u64,
/// Actual checksum found
found: u64,
},
/// Invalid block ID
#[error("Invalid block ID {block_id}: {reason}")]
InvalidBlockId {
/// The invalid block ID
block_id: u32,
/// Why it's invalid
reason: String,
},
/// Out of disk space or block count limit reached
#[error("Out of space: {current_blocks}/{max_blocks} blocks used")]
OutOfSpace {
/// Current number of blocks
current_blocks: u32,
/// Maximum number of blocks
max_blocks: u32,
},
/// Buffer pool exhausted (all pages pinned)
#[error("Buffer pool exhausted: {pinned_pages}/{total_pages} pages pinned")]
BufferPoolExhausted {
/// Number of pinned pages
pinned_pages: usize,
/// Total pages in pool
total_pages: usize,
},
/// Lock was poisoned (panic occurred while holding lock)
#[error("Lock poisoned for {resource}")]
LockPoisoned {
/// What resource the lock protected
resource: String,
},
/// Swizzle operation failed
#[error("Swizzle error: {0}")]
SwizzleError(#[from] SwizzleError),
/// Node type mismatch
#[error("Node type mismatch: expected {expected}, found {found}")]
NodeTypeMismatch {
/// Expected node type
expected: String,
/// Actual node type found
found: String,
},
/// Key too long
#[error("Key too long: {length} bytes (max {max_length})")]
KeyTooLong {
/// Actual key length
length: usize,
/// Maximum allowed length
max_length: usize,
},
/// Value too large
#[error("Value too large: {size} bytes (max {max_size})")]
ValueTooLarge {
/// Actual value size
size: usize,
/// Maximum allowed size
max_size: usize,
},
/// Bucket overflow (too many entries for bucket)
#[error("Bucket overflow in block {block_id}: {entries} entries")]
BucketOverflow {
/// Block containing the bucket
block_id: u32,
/// Number of entries
entries: usize,
},
/// WAL (Write-Ahead Log) error
#[error("WAL error: {reason}")]
WalError {
/// Description of WAL error
reason: String,
},
/// Checkpoint verification failed
///
/// Returned when re-reading data after checkpoint fails verification.
/// The WAL should NOT be truncated when this error occurs, allowing
/// recovery on the next open.
#[error("Checkpoint verification failed: {reason}")]
CheckpointVerificationFailed {
/// Description of verification failure
reason: String,
},
/// Recovery error during startup
#[error("Recovery error: {reason}")]
RecoveryError {
/// Description of recovery error
reason: String,
},
/// Arena checksum mismatch (for char arena V3+)
#[error(
"Arena checksum mismatch in arena {arena_id}: expected {expected:#x}, found {found:#x}"
)]
ArenaChecksumMismatch {
/// Arena block ID
arena_id: u32,
/// Expected checksum
expected: u32,
/// Actual checksum found
found: u32,
},
/// Arena is truncated (incomplete write)
#[error("Arena {arena_id} is truncated: expected at least {expected} bytes, found {actual}")]
TruncatedArena {
/// Arena block ID
arena_id: u32,
/// Expected minimum size
expected: usize,
/// Actual size found
actual: usize,
},
/// Operation not supported in read-only mode
#[error("Operation not supported in read-only mode: {operation}")]
ReadOnlyMode {
/// The operation that was attempted
operation: String,
},
/// Invalid operation (e.g., operating on a committed/aborted transaction)
#[error("Invalid operation: {0}")]
InvalidOperation(String),
/// Concurrent modification detected (for optimistic locking)
#[error("Concurrent modification detected on block {block_id}")]
ConcurrentModification {
/// Block that was modified
block_id: u32,
},
/// Path compression prefix mismatch
#[error("Prefix mismatch at depth {depth}: expected '{expected}', found '{found}'")]
PrefixMismatch {
/// Depth in the trie where mismatch occurred
depth: usize,
/// Expected prefix (hex encoded)
expected: String,
/// Found prefix (hex encoded)
found: String,
},
/// Internal error (should not happen, indicates bug)
#[error("Internal error: {message}")]
InternalError {
/// Description of the error
message: String,
},
/// Group commit channel was closed
#[error("Group commit channel closed")]
GroupCommitChannelClosed,
/// io_uring I/O error (Linux-only, requires `io-uring-backend` feature)
#[cfg(feature = "io-uring-backend")]
#[error("io_uring error during {operation}: {source}")]
IoUringError {
/// What operation was being performed
operation: String,
/// Underlying I/O error
#[source]
source: io::Error,
},
/// WAL error (simplified string variant for group commit)
#[error("WAL error: {0}")]
Wal(String),
}
/// Errors specific to pointer swizzling operations
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwizzleError {
/// Attempted to swizzle a pointer that's already swizzled (in memory)
#[error("Pointer is already swizzled (in memory)")]
AlreadySwizzled,
/// Attempted to unswizzle a pointer that's not swizzled (still on disk)
#[error("Pointer is not swizzled (on disk)")]
AlreadyUnswizzled,
/// Concurrent swizzle/unswizzle operation modified the pointer
#[error("Concurrent modification detected (race condition)")]
RaceCondition,
/// Null pointer encountered (cannot swizzle null)
#[error("Cannot swizzle null pointer")]
NullPointer,
/// Block ID exceeds maximum allowed value
#[error("Block ID overflow: {block_id} exceeds maximum")]
BlockIdOverflow {
/// The block ID that was too large
block_id: u32,
},
/// Offset exceeds maximum allowed value
#[error("Offset overflow: {offset} exceeds maximum")]
OffsetOverflow {
/// The offset that was too large
offset: u32,
},
/// Invalid node type encoding
#[error("Invalid node type: {value}")]
InvalidNodeType {
/// Raw node type value
value: u8,
},
}
impl PersistentARTrieError {
/// Create an I/O error with context
pub fn io_error(
operation: impl Into<String>,
path: impl Into<String>,
source: io::Error,
) -> Self {
Self::IoError {
operation: operation.into(),
path: path.into(),
source,
}
}
/// Create a corruption error
pub fn corrupted(reason: impl Into<String>) -> Self {
Self::CorruptedFile {
reason: reason.into(),
}
}
/// Create an internal error
pub fn internal(message: impl Into<String>) -> Self {
Self::InternalError {
message: message.into(),
}
}
/// Check if this error is recoverable (can retry operation)
pub fn is_recoverable(&self) -> bool {
matches!(
self,
Self::ConcurrentModification { .. } | Self::SwizzleError(SwizzleError::RaceCondition)
)
}
/// Check if this error indicates corruption (file should be repaired/rebuilt)
pub fn is_corruption(&self) -> bool {
matches!(
self,
Self::CorruptedFile { .. }
| Self::ChecksumMismatch { .. }
| Self::InvalidMagic { .. }
| Self::ArenaChecksumMismatch { .. }
| Self::TruncatedArena { .. }
| Self::CheckpointVerificationFailed { .. }
)
}
/// Check if this error is transient (e.g., out of buffer space)
pub fn is_transient(&self) -> bool {
matches!(self, Self::BufferPoolExhausted { .. })
}
}
impl From<super::wal::WalError> for PersistentARTrieError {
fn from(err: super::wal::WalError) -> Self {
use super::wal::WalError;
match err {
WalError::Io(e) => Self::IoError {
operation: "WAL operation".to_string(),
path: String::new(),
source: e,
},
WalError::InvalidRecordType(t) => Self::CorruptedFile {
reason: format!("Invalid WAL record type: {}", t),
},
WalError::CorruptedRecord(msg) => Self::CorruptedFile {
reason: format!("Corrupted WAL record: {}", msg),
},
WalError::UnexpectedEof => Self::CorruptedFile {
reason: "Unexpected end of WAL file".to_string(),
},
WalError::AlreadyExists => Self::InternalError {
message: "WAL file already exists".to_string(),
},
WalError::NotFound => Self::IoError {
operation: "WAL open".to_string(),
path: String::new(),
source: io::Error::new(io::ErrorKind::NotFound, "WAL file not found"),
},
WalError::ParentNotFound(path) => Self::IoError {
operation: "WAL create parent directory".to_string(),
path: path.display().to_string(),
source: io::Error::new(
io::ErrorKind::NotFound,
format!("Parent directory not found: {}", path.display()),
),
},
WalError::InvalidRegimeStamp(msg) => Self::InternalError {
message: format!("Invalid WAL regime stamp: {}", msg),
},
}
}
}
impl From<super::recovery::RecoveryError> for PersistentARTrieError {
fn from(err: super::recovery::RecoveryError) -> Self {
Self::InternalError {
message: format!("Recovery error: {}", err),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = PersistentARTrieError::InvalidMagic {
expected: 0x5041525400010000,
found: 0x0000000000000000,
};
let msg = format!("{}", err);
assert!(msg.contains("Invalid magic"));
assert!(msg.contains("5041525400010000"));
}
#[test]
fn test_swizzle_error_display() {
let err = SwizzleError::AlreadySwizzled;
assert_eq!(
format!("{}", err),
"Pointer is already swizzled (in memory)"
);
let err = SwizzleError::BlockIdOverflow { block_id: 42 };
assert!(format!("{}", err).contains("42"));
let err = SwizzleError::OffsetOverflow { offset: 1024 };
assert!(format!("{}", err).contains("1024"));
}
#[test]
fn test_is_recoverable() {
let recoverable = PersistentARTrieError::ConcurrentModification { block_id: 1 };
assert!(recoverable.is_recoverable());
let not_recoverable = PersistentARTrieError::CorruptedFile {
reason: "test".to_string(),
};
assert!(!not_recoverable.is_recoverable());
}
#[test]
fn test_is_corruption() {
let corruption = PersistentARTrieError::CorruptedFile {
reason: "test".to_string(),
};
assert!(corruption.is_corruption());
let checksum = PersistentARTrieError::ChecksumMismatch {
block_id: 0,
expected: 123,
found: 456,
};
assert!(checksum.is_corruption());
let not_corruption = PersistentARTrieError::OutOfSpace {
current_blocks: 100,
max_blocks: 100,
};
assert!(!not_corruption.is_corruption());
}
#[test]
fn test_swizzle_error_conversion() {
let swizzle_err = SwizzleError::NullPointer;
let artrie_err: PersistentARTrieError = swizzle_err.into();
match artrie_err {
PersistentARTrieError::SwizzleError(SwizzleError::NullPointer) => {}
_ => panic!("Expected SwizzleError variant"),
}
}
#[test]
fn test_io_error_helper() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let err = PersistentARTrieError::io_error("open", "/path/to/file", io_err);
match err {
PersistentARTrieError::IoError {
operation,
path,
source,
} => {
assert_eq!(operation, "open");
assert_eq!(path, "/path/to/file");
assert_eq!(source.kind(), io::ErrorKind::NotFound);
}
_ => panic!("Expected IoError variant"),
}
}
}