Guide: Error Handling
Relevant source files
The following files were used as context for generating this wiki page:The error handling strategy in Cyberpath Sentinel is a cornerstone of its design, directly supporting the core principles of trust, transparency, and compliance. Since Sentinel uses the filesystem as its database, it must robustly handle issues not typically managed by traditional database systems, such as file-level race conditions, partial writes during power failures, and data corruption. The approach combines architectural patterns for durability, strict compile-time checks for code quality, and a runtime strategy that favors program termination over potentially inconsistent states.
This guide provides an overview of the key architectural components, static analysis rules, and runtime behaviors that constitute Sentinel's error handling and recovery mechanisms.
Sources: README.md:10-11, IMPLEMENTATION_PLAN.md:11-15
Architectural Strategies for Resilience
Sentinel's architecture is designed to prevent and mitigate errors at multiple layers, focusing on durability, atomicity, and concurrency control. These strategies address the inherent "Real Trade-offs" of a filesystem-based database.
Sources: README.md:54-66, IMPLEMENTATION_PLAN.md:300-303
Concurrency Control and Locking
To handle multi-writer scenarios and prevent data corruption from race conditions, Sentinel plans to implement a FileLockManager. This component will manage access to document files, ensuring that write operations are serialized and safe.
The planned locking mechanism involves acquiring a lock before a file operation and releasing it upon completion. This prevents simultaneous writes to the same file.
Sources: README.md:57, IMPLEMENTATION_PLAN.md:120-137
The following diagram illustrates the planned flow for a locked file write operation.
This diagram shows the process of acquiring a lock to ensure an atomic and safe write operation, a key strategy for handling concurrency.
Sources: IMPLEMENTATION_PLAN.md:120-137, IMPLEMENTATION_PLAN.md:300-313
Durability via Write-Ahead Logging (WAL)
To ensure durability and enable crash recovery from partial writes, a Write-Ahead Log (WAL) is a central feature. Before any changes are written to the actual document files, they are first recorded as entries in a transaction log. In the event of a system crash, this log can be replayed to restore the database to a consistent state.
The TransactionLog is responsible for managing log entries for operations like Insert, Update, and Delete.
Sources: README.md:57, IMPLEMENTATION_PLAN.md:99-118, IMPLEMENTATION_PLAN.md:203-211
The sequence diagram below shows how a transaction uses the WAL to ensure an atomic update.
This diagram shows a successful update operation where changes are first logged to the WAL before being applied to the filesystem, ensuring atomicity.
Sources: IMPLEMENTATION_PLAN.md:99-118
Data Integrity Verification
To protect against silent data corruption and tampering, Sentinel's design includes multiple layers of integrity checks. Each document's metadata is planned to contain a hash and an optional digital signature.
| Integrity Check | Purpose | Implementation Phase |
|---|---|---|
| Checksums | Detects accidental corruption (e.g., partial writes). A SHA-256 hash of the document is stored and verified on read. | Phase 1 / Phase 4 |
| Digital Signatures | Detects tampering and provides non-repudiation. An RSA-2048 signature is created on write and verified on read. | Phase 4 |
| Merkle Trees | Verifies the integrity of an entire collection efficiently. | Phase 7 |
Sources: README.md:109-111, IMPLEMENTATION_PLAN.md:52-54, IMPLEMENTATION_PLAN.md:330-345
The following diagram shows the process of reading and verifying a document.
This diagram illustrates the multi-step verification process upon reading a document to ensure its integrity.
Sources: IMPLEMENTATION_PLAN.md:52-54, IMPLEMENTATION_PLAN.md:244-250, IMPLEMENTATION_PLAN.md:330-336
Compile-Time Error Prevention
Sentinel adopts a strict "fail-fast" and "correct-by-construction" philosophy, which is heavily enforced through static analysis and linting rules defined in the workspace Cargo.toml. This approach aims to catch a large class of potential runtime errors during compilation.
Sources: Cargo.toml:14-434
Release Profile: Abort on Panic
For release builds, Sentinel is configured to abort the process on a panic rather than unwinding the stack. This results in a smaller, faster binary and prevents the program from continuing in an undefined state after an unrecoverable error.
# Cargo.toml:12-13
[profile.release]
panic = "abort" # Abort on panic, no unwinding, smaller binary and better security
Sources: Cargo.toml:12-13
Strict Clippy Lints
The project enforces a comprehensive set of Clippy lints to disallow code patterns that are common sources of errors or panics. This forces developers to write more robust code that explicitly handles failure cases.
Key forbidden patterns include:
- Unchecked Unwraps: Using
.unwrap()onOptionorResulttypes is a common cause of panics. - Panicking Macros: Direct calls to
panic!,todo!,unimplemented!, andunreachable!are disallowed. - Risky Operations: Lints deny integer arithmetic that can have side effects, indexing that can panic, and floating-point comparisons.
The table below highlights some of the most critical error-related lints that are set to deny.
| Lint Name | Level | Rationale |
|---|---|---|
panic | deny | Prevents explicit panics, forcing use of Result. |
unwrap_used | allow | While allowed, it is generally discouraged in favor of explicit handling. The plan shows err_expect and ok_expect are denied. |
err_expect | deny | Disallows .expect() on Result, which can panic. |
ok_expect | deny | Disallows .expect() on Option, which can panic. |
indexing_slicing | deny | Prevents out-of-bounds access that would cause a panic. |
integer_division | allow | Allowed, but developers must be mindful of division by zero. |
unreachable | deny | Prevents use of the unreachable! macro. |
todo | deny | Prevents placeholder code from being committed. |
unimplemented | deny | Prevents placeholder macros from being committed. |
Sources: Cargo.toml:14-434
API-Level Error Handling
At the function level, the implementation plan specifies that all I/O-bound or fallible operations within the Store and Collection components will return an io::Result<T>. This design choice mandates that callers explicitly handle potential errors, such as file-not-found, permission denied, or I/O failures.
// From IMPLEMENTATION_PLAN.md
impl Store {
pub fn new(root_path: impl AsRef<Path>) -> io::Result<Self>
pub fn collection(&self, name: &str) -> io::Result<Arc<Collection>>
// ...
}
impl Collection {
pub async fn insert(&self, id: &str, data: Value) -> io::Result<()>
pub async fn get(&self, id: &str) -> io::Result<Option<Document>>
// ...
}
Sources: IMPLEMENTATION_PLAN.md:70-75, IMPLEMENTATION_PLAN.md:92-98
The basic usage example in the README.md demonstrates using .expect() for brevity, but the project's linting rules strongly discourage this in production code, favoring match statements or the ? operator for error propagation.
Sources: README.md:76-92, Cargo.toml:136, Cargo.toml:279