From 1dfff05bf409b32bd58436dac7fd321e590cc4b6 Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Fri, 6 Feb 2026 20:28:35 +0530 Subject: [PATCH 1/2] docs: document re-entrancy and integration constraints Add Integration Constraints section to limitations.md: - One first::test() per #[test] - Async tests not supported - Not thread-safe - No nested workspaces Add limitations summary to lib.rs crate docs. Closes #18 --- docs/limitations.md | 40 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 9 +++++++++ 2 files changed, 49 insertions(+) diff --git a/docs/limitations.md b/docs/limitations.md index 56c979e..4b2c1da 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -145,3 +145,43 @@ Crash points are serialized. Concurrent operations are not explored for race con ### Linux Only (v0.1) The current implementation uses Linux-specific APIs (`SIGKILL`, `/proc`). macOS and Windows support is not yet available. + +--- + +## Integration Constraints (v0.1) + +### One `first::test()` Per `#[test]` + +| Pattern | Status | +|---------|--------| +| One `first::test()` per `#[test]` | ✅ Supported | +| Multiple `first::test()` in one `#[test]` | ❌ Undefined | + +**Rationale**: The orchestrator manages process lifecycle. Nested or sequential calls conflict. + +### Async Tests Not Supported + +| Pattern | Status | +|---------|--------| +| `#[test]` (sync) | ✅ Supported | +| `#[tokio::test]` | ❌ Not supported | +| `#[async_std::test]` | ❌ Not supported | + +**Rationale**: FIRST uses `fork()` + `SIGKILL` — async runtimes don't survive this. + +### Not Thread-Safe + +| Pattern | Status | +|---------|--------| +| Single-threaded `.run()` closure | ✅ Supported | +| `crash_point()` from spawned threads | ❌ Undefined | + +**Rationale**: Crash point counter uses atomic state but determinism requires single-threaded execution. + +### No Nested Workspaces + +| Pattern | Status | +|---------|--------| +| User-managed dirs inside `env.path()` | ✅ Supported | +| Expecting FIRST to manage nested isolation | ❌ Not supported | + diff --git a/src/lib.rs b/src/lib.rs index f7df3b7..33c5198 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,15 @@ //! }); //! } //! ``` +//! +//! # Limitations (v0.1) +//! +//! - One `first::test()` per `#[test]` function +//! - Async tests (`#[tokio::test]`) not supported +//! - Not thread-safe (`crash_point()` from spawned threads is undefined) +//! - No nested workspaces +//! +//! See `docs/limitations.md` for full details. mod env; mod orchestrator; From dd2de13883e4fb8600348627c3cdcb21540304fa Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Sat, 7 Feb 2026 10:28:08 +0530 Subject: [PATCH 2/2] docs: refresh documentation and switch to Apache 2.0 - Rewrite README for conciseness and professional formatting - Switch license from MIT to Apache 2.0 - Update project logo - Rewrite all architecture docs (~60% reduction) - Rewrite limitations.md and proof/reference_wal.md - Remove empty invariants.md --- Cargo.toml | 2 +- LICENSE | 204 ++++++++++ README.md | 562 +++++--------------------- assets/image.jpg | Bin 0 -> 104061 bytes assets/logo.svg | 24 +- docs/architecture/001_structure.md | 123 ++---- docs/architecture/002_crash_point.md | 135 ++----- docs/architecture/004_orchestrator.md | 146 ++----- docs/invariants.md | 0 docs/limitations.md | 203 ++-------- docs/proof/reference_wal.md | 126 +----- 11 files changed, 473 insertions(+), 1052 deletions(-) create mode 100644 assets/image.jpg delete mode 100644 docs/invariants.md diff --git a/Cargo.toml b/Cargo.toml index 9b6aae0..c92ac30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "first" version = "0.1.0" edition = "2024" description = "Deterministic crash testing framework for storage engines" -license = "MIT" +license = "Apache-2.0" repository = "https://github.com/siphonite/first" keywords = ["crash-testing", "storage", "database", "testing", "fault-injection"] categories = ["development-tools::testing"] diff --git a/LICENSE b/LICENSE index e69de29..bc6f5a8 100644 --- a/LICENSE +++ b/LICENSE @@ -0,0 +1,204 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Copyright 2026 Aman Kumar diff --git a/README.md b/README.md index d5ea8db..8dcae97 100644 --- a/README.md +++ b/README.md @@ -1,501 +1,157 @@

- FIRST logo + FIRST logo

-# FIRST +

FIRST

-> FIRST is a deterministic crash and recovery testing framework for storage engines and WAL-based systems. - -## The Problem - -Modern databases and storage engines rely on strong crash-consistency guarantees. - -They write data using WALs, manifests, page files, and metadata updates, then recover state after crashes by replaying logs or rebuilding in-memory structures. These recovery paths are some of the most critical — and most fragile — code in the system. - -However, crash behavior is rarely tested systematically. - -Across the ecosystem today: - -- Unit tests validate logic assuming uninterrupted execution -- Integration tests exercise happy paths -- Benchmarks measure performance, not correctness -- Fault injection exists, but is often coarse or non-deterministic -- Crash consistency and power-loss behavior are tested ad-hoc, per project -- Storage correctness is largely enforced by tribal knowledge and past incidents - -Real failures do not happen at function boundaries. - -They happen: -- Between writes -- During system calls -- After data reaches the page cache but before persistence -- After a WAL append but before a commit marker -- After a rename but before directory fsync - -These failure modes are difficult to reason about and easy to miss. - -As a result, many storage systems ship with latent crash-consistency bugs that: -- Pass unit tests -- Pass integration tests -- Pass benchmarks -- Surface only under real crashes, often years later - -Every major database eventually builds internal crash-testing and chaos infrastructure to address this. - -But these solutions are: -- Reinvented independently -- Tightly coupled to internal codebases -- Hard to maintain -- Rarely exhaustive -- Not reusable across projects - -There is no standard framework, shared language, or common methodology for testing crash correctness in storage systems. - -**FIRST exists to fill this gap.** - ---- - -## Why FIRST - -Crash consistency failures are difficult to test because crashes are not normal execution paths. - -Most testing frameworks treat crashes as rare, external events. When failures are injected, they are often: -- Random -- Non-deterministic -- Coarse-grained -- Difficult to reproduce - -This makes failures hard to debug and discourages exhaustive testing. - -**FIRST takes a different approach.** - -FIRST treats crashes as a **first-class part of execution**. - -Instead of asking: -> "Does this code work?" - -FIRST asks: -> "At every possible crash point, does recovery preserve the system's invariants?" - -To answer this, FIRST provides: - -- **Deterministic crash injection** - Crashes are injected at precise, replayable points, making failures reproducible. - -- **Systematic exploration** - Crash points are enumerated deliberately, not discovered by chance. - -- **Recovery-driven testing** - Every crash is followed by a restart and execution of recovery logic. - -- **Invariant-based validation** - Tests assert properties that must always hold, rather than expected outputs. - -By making crashes deterministic, observable, and repeatable, FIRST turns crash consistency from an ad-hoc practice into a testable property. +

+ Deterministic crash testing for storage engines
+ Filesystem Injection and Reliability Stress Test +

-This allows storage systems to reason about correctness under failure with the same rigor applied to normal execution. +

+ Design • + Limitations • + Examples +

--- -## How It Works - -FIRST operates at the **persistence boundary** between your application and the filesystem. +## What is FIRST? -Here's the execution flow: +FIRST tests what happens when your storage system crashes. It injects crashes at precise points in your code, restarts, runs recovery, and validates invariants — deterministically and reproducibly. -1. **Crash Injection** - Mark crash points explicitly with `first::crash_point("label")` or let FIRST automatically intercept persistence syscalls (write, fsync, rename). - -2. **Workload Execution** - Your test workload runs normally until a scheduled crash point is reached, then the process terminates immediately (SIGKILL). - -3. **State Preservation** - Filesystem state is captured exactly as it existed at crash time using copy-on-write snapshots. - -4. **Restart & Recovery** - The process restarts, your storage system's recovery logic runs (WAL replay, index rebuild, etc.), and invariants are validated. - -5. **Systematic Iteration** - The test repeats for each crash point discovered in the workload, exploring all meaningful failure scenarios. +```rust +first::test() + .run(|env| { + let mut wal = Wal::open(&env.path("wal")).unwrap(); + let tx = wal.begin(); + wal.put(tx, "key", "value"); + first::crash_point("before_commit"); + wal.commit(tx); + }) + .verify(|env, _crash| { + let wal = Wal::open(&env.path("wal")).unwrap(); + // Invariant: uncommitted data must not be visible + assert!(wal.get("key").is_none() || wal.get("key") == Some("value")); + }) + .execute(); +``` -**Performance:** Each crash point takes approximately 50ms to test. A typical workload with 100 operations completes in seconds to minutes, making FIRST practical for continuous integration. +## Why FIRST? -**Determinism:** Given the same seed, FIRST produces identical crash sequences. Every failure is reproducible. +Real crashes don't happen at function boundaries. They happen: +- After `write()` but before `fsync()` +- After appending to WAL but before the commit marker +- After `rename()` but before directory sync -See [`DESIGN.md`](DESIGN.md) for detailed architecture and implementation approach. +Traditional tests don't catch these. FIRST does. ---- +| Traditional Testing | FIRST | +|---------------------|-------| +| Tests happy paths | Tests crash paths | +| Random fault injection | Deterministic crash points | +| Failures are flaky | Every failure is reproducible | +| Recovery rarely exercised | Recovery runs after every crash | -## Example +## Quick Start -Here's how you test an append-only log for crash consistency: +```toml +# Cargo.toml +[dev-dependencies] +first = "0.1" +``` ```rust -use first::test; - -/// Test that log entries are atomic - either fully written or not visible -fn test_log_atomicity() { +// tests/my_crash_test.rs +#[test] +fn test_atomicity() { first::test() - // Define the workload .run(|env| { - let log = AppendLog::create(env.path("test.log")); - - log.append("entry1").expect("append failed"); - // Implicit crash point: after write() syscall - - first::crash_point("after_first_append"); - // Explicit crash point: test critical boundary - - log.sync().expect("sync failed"); - // Implicit crash point: after fsync() syscall - - log.append("entry2").expect("append failed"); - // Implicit crash point: after write() syscall - - log.sync().expect("sync failed"); - // Implicit crash point: after fsync() syscall + // Your workload with crash_point() calls }) - - // Define recovery and validation - .verify(|env, crash_info| { - println!("Recovering from crash at: {:?}", crash_info); - - // Recovery: reopen the log (triggers internal recovery logic) - let log = AppendLog::open(env.path("test.log")) - .expect("log should always be recoverable"); - - let entries = log.read_all(); - - // Invariant: Atomicity - entries are all-or-nothing - match entries.len() { - 0 => { - // Crashed before first sync - no data persisted - assert!(entries.is_empty()); - }, - 1 => { - // Crashed after first sync, before second - assert_eq!(entries, vec!["entry1"]); - }, - 2 => { - // Crashed after both syncs - all data persisted - assert_eq!(entries, vec!["entry1", "entry2"]); - }, - _ => { - // Partial write visible - atomicity violated! - panic!("Invalid state after recovery: {:?}", entries); - } - } - - // Additional invariants - assert!(log.verify_checksums(), "Checksums must be valid"); - assert!(log.is_well_formed(), "Log structure must be consistent"); - }); + .verify(|env, crash| { + // Recovery + invariant checks + }) + .execute(); } ``` -**What this test does:** - -- Executes the workload with 5 crash points (2 explicit + 3 implicit from syscalls) -- For each crash point, terminates the process and preserves filesystem state -- Restarts and runs recovery logic -- Validates that invariants hold after recovery -- Reports violations with exact crash point for reproduction +```bash +cargo test +``` -**Example failure report:** +## How It Works ``` -FIRST Test Failure -═══════════════════════════════════════════════ -Test: test_log_atomicity -Crash Point: 3 ("after_first_append") -Seed: 42 - -Invariant Violation: - Partial write detected - entry1 has incomplete data - Expected: valid checksum - Actual: checksum mismatch (0x00000000) - -Filesystem state preserved at: /tmp/first/crash_3/ - -Reproduce: - $ FIRST_SEED=42 FIRST_CRASH_POINT=3 cargo test test_log_atomicity -═══════════════════════════════════════════════ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Orchestrator│────▶│ Execution │────▶│ Verify │ +│ (Parent) │ │ (Child) │ │ (Child) │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + │ SIGKILL at Recovery + + │ crash_point() Invariants + │ │ │ + └───────────────────┴────────────────────┘ + Repeat for each crash point ``` -This reproducibility transforms intermittent production bugs into debuggable test failures. - ---- - -## Performance - -FIRST is designed for continuous integration and fast feedback: - -| Workload | Crash Points | Test Time (Sequential) | Test Time (8 cores) | -|----------|--------------|------------------------|---------------------| -| Simple log (10 operations) | ~30 | 1.5 seconds | 0.3 seconds | -| Medium DB (100 operations) | ~300 | 15 seconds | 2 seconds | -| Complex LSM (1000 operations) | ~500 | 25 seconds | 4 seconds | +1. **Execution**: Your workload runs until hitting the target crash point → `SIGKILL` +2. **Verify**: Fresh process opens the preserved filesystem state, runs recovery, checks invariants +3. **Iterate**: Repeat for each crash point discovered -**Key characteristics:** +## Example Output -- **Linear scaling**: O(N) where N = number of crash points -- **Per-point overhead**: ~50ms on tmpfs (in-memory filesystem) -- **Parallel execution**: Near-linear speedup on multi-core systems -- **Practical for CI**: Most tests complete in seconds to minutes - -State space exploration is **bounded and systematic**, not exhaustive. FIRST focuses on high-value crash boundaries (fsync, rename, multi-step operations) rather than attempting to test every possible execution interleaving. - -See [`DESIGN.md § State Space and Performance`](DESIGN.md#7-state-space-and-performance) for detailed analysis. - ---- - -## How FIRST Differs from Existing Tools - -FIRST fills a specific gap in the crash testing ecosystem: - -| Tool | **FIRST** | CrashMonkey | Jepsen | dm-flakey | FoundationDB Sim | -|------|-----------|-------------|--------|-----------|------------------| -| **Target** | Storage Engines | Filesystems | Distributed Systems | Block Devices | Full System | -| **Operates At** | Application persistence boundary | Block I/O layer | Network layer | Kernel block device | Internal runtime | -| **Knowledge Level** | App-aware (WAL, manifests) | FS-aware (bio) | Black box | Block-level only | Full system | -| **Deterministic** | [YES] | [MOSTLY] | [NO] | [YES] (scriptable) | [YES] | -| **Setup Required** | `cargo test` | VM + kernel module | Cluster setup | Root + kernel module | Not extractable | -| **Recovery Testing** | [YES] Built-in | [YES] | [NO] | [LIMITED] | [YES] | -| **Open Source** | [YES] (planned) | [YES] | [YES] | [YES] | [NO] Proprietary | - -**Why not use existing tools?** - -- **CrashMonkey** tests filesystem implementations, not storage engines. It operates at the block I/O layer and doesn't understand application-level persistence semantics (e.g., "after WAL commit but before manifest update"). - -- **Jepsen** tests distributed systems by injecting network faults. FIRST tests local persistence and crash recovery—different problem domains. - -- **dm-flakey** injects faults at the block device level, requiring kernel modules and root access. FIRST operates at syscall boundaries and runs as a regular user process. - -- **FoundationDB's simulator** is proprietary and deeply integrated into their codebase. FIRST is designed as a reusable, open-source framework. - -**FIRST's unique position:** - -Unlike filesystem or distributed testing tools, FIRST understands **application-level persistence semantics** and provides **deterministic, reproducible** crash scenarios specifically for storage engine developers. - -It operates where your code meets the filesystem—at the exact boundary where crash bugs happen. - ---- - -## What FIRST Guarantees - -FIRST provides guarantees about **testing behavior**, not about application correctness by itself. - -Specifically, FIRST guarantees that: - -- **Crashes are deterministic and replayable** - Given the same test, crash schedule, and seed, failures can be reproduced exactly. - -- **Crash points are explicit and observable** - Tests run with well-defined crash boundaries rather than implicit assumptions about execution. - -- **Recovery logic is always exercised** - Every injected crash is followed by a restart and execution of user-provided recovery code. - -- **Persistent state after crashes is inspectable** - Filesystem state can be observed, compared, and validated after each crash. - -- **Invariant violations are surfaced early** - Violations appear as minimal, reproducible test failures rather than intermittent production bugs. - -FIRST does **not** guarantee that a system is correct. - -Instead, it guarantees that: -- Crash behavior is systematically explored -- Recovery paths are continuously tested -- Violations are reproducible and debuggable - -**Correctness remains the responsibility of the storage system and the invariants it defines.** - -FIRST provides the testing infrastructure to verify those invariants hold under crash conditions. - ---- - -## Non-Goals - -FIRST is intentionally focused and does not attempt to solve every testing problem. - -Specifically, FIRST is **not**: - -- **A fuzzer or property-based testing framework** - FIRST prioritizes determinism and systematic exploration over randomness. - -- **A performance or benchmarking tool** - FIRST focuses exclusively on correctness under crashes, not throughput or latency. - -- **A database or storage engine** - FIRST provides testing infrastructure, not storage primitives or recovery logic. +``` +[first] crash point 1: OK +[first] crash point 2: OK +[first] crash point 3: FAILED (see /tmp/first/run_3) +[first] crash label: "after_commit_write" +[first] to reproduce: + FIRST_PHASE=VERIFY FIRST_CRASH_TARGET=3 FIRST_WORK_DIR=/tmp/first/run_3 \ + cargo test my_test -- --exact +``` -- **A replacement for unit or integration tests** - FIRST complements existing testing by targeting crash and recovery paths. +## Key Features -- **A distributed systems testing framework** - FIRST targets local persistence and crash recovery, not network partitions or consensus failures. +| Feature | Description | +|---------|-------------| +| **Deterministic** | Same seed/target = same crash = same failure | +| **Reproducible** | Every failure includes exact reproduction command | +| **Zero Setup** | Works with `cargo test`, no kernel modules or VMs | +| **Invariant-Based** | Assert properties, not expected outputs | +| **Artifact Preservation** | Filesystem state preserved on failure for debugging | -These non-goals are deliberate. +## Limitations (v0.1) -By remaining narrowly scoped, FIRST aims to be reliable, predictable, and easy to integrate into existing storage systems. +- Linux only +- Single-threaded execution +- Explicit crash points (no syscall interception yet) +- One `first::test()` per `#[test]` function +- No async test support ---- +See [docs/limitations.md](docs/limitations.md) for details. ## Project Status -FIRST is in early development, with design solidified and implementation in progress. - -### Current State - -**Available Now:** -- ✓ Comprehensive design document ([`DESIGN.md`](DESIGN.md)) -- ✓ Core architecture defined -- ✓ API design finalized -- ✓ Implementation approach validated - -**In Progress:** -- → Rust implementation (core framework) -- → Example storage systems (append-only log, simple LSM) -- → Test suite and validation - -**Not Yet Available:** -- Public repository (coming soon) -- Stable release -- Language bindings (C++, Go planned for later phases) - -### Development Focus - -The current focus is on: -- Defining a clear and minimal core model -- Establishing deterministic crash and recovery semantics -- Validating the design through small, targeted examples - -The API is intentionally unstable and subject to change during this phase. - -This early stage prioritizes: -- **Correctness** over completeness -- **Clarity** over feature count -- **Design rigor** over rapid expansion - -### Feedback Welcome - -We're sharing the design early to gather feedback from storage and database engineers before the first public release. - -Feedback is especially valuable on: -- Invariant modeling patterns -- Crash boundary selection -- Recovery semantics -- API ergonomics -- Integration with existing storage systems - ---- - -## Roadmap - -FIRST is being developed in deliberate phases. - -### Phase 1 — Core Crash Testing (Current Focus) - -- [x] Design document and architecture -- [ ] Deterministic crash scheduler -- [ ] Explicit crash point annotations -- [ ] Process termination and restart (SIGKILL) -- [ ] Filesystem state preservation (CoW snapshots) -- [ ] Persistent state inspection -- [ ] Minimal example storage systems -- [ ] Basic invariant validation framework - -**Target:** Q2 2026 - -### Phase 2 — Reusable Test Harness - -- [ ] Ergonomic test API refinements -- [ ] Syscall interception mode (LD_PRELOAD) -- [ ] Crash point pruning and optimization -- [ ] Parallel crash point exploration -- [ ] Improved diagnostics and failure reproduction -- [ ] Integration with `cargo test` -- [ ] Documentation and examples - -**Target:** Q3 2026 - -### Phase 3 — Language Bindings and Ecosystem - -- [ ] C/C++ bindings -- [ ] Go bindings -- [ ] Java bindings (exploratory) -- [ ] Support for Windows and macOS -- [ ] Integration with existing test frameworks -- [ ] Community examples and case studies - -**Target:** Q4 2026 and beyond - -The roadmap is intentionally conservative. - -Features are added only when they preserve determinism, reproducibility, and clarity. Each phase builds on proven foundations from the previous phase. - ---- - -## Contributing - -FIRST is an early-stage project focused on correctness, determinism, and clear design. +**v0.1** — Core crash testing functionality complete. -Contributions are welcome, especially in the form of: -- Design feedback and architectural suggestions -- Invariant modeling ideas and patterns -- Small, focused examples of storage systems -- Documentation improvements -- Bug reports and edge case identification +- ✅ Deterministic crash injection +- ✅ Orchestrator lifecycle +- ✅ Invariant validation +- ✅ Reference WAL example +- ⬜ Syscall interception (planned v0.2) +- ⬜ Parallel execution (planned v0.2) -### Before Contributing Code +## Documentation -Please open an issue to discuss your approach before submitting code. - -This helps: -- Keep the core design coherent -- Avoid premature complexity -- Ensure contributions align with project goals -- Prevent duplicate or conflicting work - -### Development Principles - -All contributions should maintain FIRST's core principles: - -1. **Determinism first** — Every failure must be reproducible -2. **Simplicity** — Prefer clarity over cleverness -3. **Focused scope** — Stay within defined boundaries -4. **Test what you build** — Demonstrate correctness -5. **Document your decisions** — Explain the "why" - -See `CONTRIBUTING.md` for detailed guidelines (coming soon). - ---- - -## Learn More - -- **[Design Document](DESIGN.md)** — Comprehensive technical design, implementation details, and architectural decisions -- **[Examples](examples/)** — Reference implementations and test patterns (coming soon) -- **[API Documentation](docs/api.md)** — Detailed API reference (coming soon) -- **[FAQ](docs/faq.md)** — Common questions and answers (coming soon) - ---- +- **[Design Document](docs/design.md)** — Architecture and implementation details +- **[Limitations](docs/limitations.md)** — Known constraints and crash model +- **[Reference WAL](examples/reference_wal/)** — Complete example with crash-consistency proof ## License -[To be announced] - ---- - -## Acknowledgments - -FIRST builds on research in crash consistency testing, including: - -- **ALICE** (OSDI '14) — Logical crash consistency checking -- **CrashMonkey** (OSDI '18) — Automated filesystem crash testing -- **AGAMOTTO** (SOSP '21) — Filesystem testing via record/replay - -We're grateful to the storage systems research community for establishing the foundations that make FIRST possible. +Licensed under the [Apache License, Version 2.0](LICENSE). --- diff --git a/assets/image.jpg b/assets/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7b753a1e216bc0bfe487fec7ebfb969b34bb0139 GIT binary patch literal 104061 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

2M zMBBUYI8ioR8)}@U@$#{_UVvw&yK;?=nX8j8>mS>$Ks$);KGoq37JY39@c#$?)RvhVg(Z;`5S?3LXGnD-a zKv?CQ8QPps&gG1vkoZv+#oTvJ8nb3>f|OV_52O)p)C8f3}5E^7Rb6+kW;74M!@eh_DSpEiccL)*l*qPfd-d5cwf> zSxVpSx$5pS$1oN=>Ckpx{wf5GIS`Kr=a0w?40!Wd2f$P@l#+r{$nsa<=LlANs#t&RB3oV zoE)EUa%P`qAIsQSQksx#(UGR9GoEX@Zr@-`Djf4Q?X`?P#_!6F`d<}00WJR_ zY<4hRvL>gh3kn_znq0HFe*jWcLi*k()74>N8pg+vq~c;HhT?dVVNbu;e^wbNtq8&A ztZ6(T4E6Ue?krO+tAjfhS)QvLHg9`e0LzvpU2iv$lSDUTCaDL z%YseysSg8+DYX5vmgD8lbP*H+C^>7Nlz?n*N71@g$YBW}z|`lnNBJ)EmM12<`xqBt zUqN+RSi?qKh#}{5mAU2*$Zn9qJH$lei5T$p8du69l!ZJ~GXHNtP7W@5=4vFIH0=2v9*;`te_j|JP*v2HyMMfyrN&;9XrQ{HC5tX$Xt?VLa(#+YfHa9c0`V6?aP( za)bzeZ5L>gBkIN#ZW3V6V5?9x2^6emcQG_ar`JWuEOFMf-SLM?zO$$T=u~xy5^bw0riO(qgS~?|0Rrn=Yjqg z?*D)K-J{<)=Rh#^!4l@otig<=5}rU~T02m29J~++63kvF%_*gJ zLUP1nIY%;B&}bwSfc!v5DJjeQKt!cX(@cG&&jYT$LP=7`PjMt(4?~bhG19Lvm&Un6 zCL--PAaaWNrz+NQVD3w-`##b}eQ_n*C=IFQfK^K+^z=vMKUNd4deThpv)kdHq{n_fbTt-X)%fnvKM4+~d^O=>a>{)s ziM0ejKI+=?wC2-Aq7I8M*wup9mQs%4^Ef*-!8oJ>&Q4x_ak_8{lDz`yQt2y z3x5uh0dsXPE(1~%5GEFTLcN2l&lc&NbSYZe9LV(xhT~D+J zVx&-8+4;Er&aTyS4k|_%{ay=ROHam5^6x3;1Qy=2q`yV$Qhy%A-k5Rl$-XUHJ&x+> zapa&HO0|5BJ^)>hDr0{(`Oel^F-i-o=9hdv?XjDLJ#l-(DFy0WqNOTQ{>b}F^IQZ3 zi8DcNu5lBusXa&OBo0&X!T17nlGgaT{jT@)0;P+Ey z5UHx7rNW!jhG5H7bLpvKg54UsX#%hIzk{i)809A}Rk(0k(Pc|nq6oSX*Anr68b-)b* z6a^5Xjjqes%HaJxeWsitA z@YIk=Z`VELY8K|qAme4yzLZG5gd4!`V5ePPWnd^*G0`74cc$6*d<%iwNxr~6!3OGL z6^Ff3!kT}KpYR%%q8Ch-g>+KvOv=Rt7vdlC5+!K1$Op8USIXU7amvF?-Pyy^d;Op& zwdJw96Bgg4XaY4kN)N$}KuyfH1tiY)3`?u0@$fL$7t58BJa1QT>PB(tnW<*+243F* z8vCzYoYOKb69%2m1Jm)xuFpumE=qCIJ~fR<;PZs8*_DE%edK|-veF6U&sYBk;$BZi literal 0 HcmV?d00001 diff --git a/assets/logo.svg b/assets/logo.svg index c9790a3..5968cfe 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,16 +1,8 @@ - - - - - - - - - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/docs/architecture/001_structure.md b/docs/architecture/001_structure.md index ee1a06b..8cea9ba 100644 --- a/docs/architecture/001_structure.md +++ b/docs/architecture/001_structure.md @@ -1,115 +1,54 @@ -# Architecture Decision: Crate Structure and Execution Model +# ADR-001: Crate Structure -**Status:** Proposed -**Issue:** #1 +**Status:** Accepted -## 1. High-Level Structure +## Decision -**Decision:** `first` will be a **Library Crate**. - -Users will add `first` to their `dev-dependencies`. -Tests will be written as standard Rust integration tests (in `tests/*.rs`) or unit tests. +FIRST is a **library crate** with no CLI. Users add it to `dev-dependencies` and run tests via `cargo test`. ```toml [dev-dependencies] first = "0.1" ``` -There is no separate `first-cli` binary required to run tests. Standard `cargo test` is the interface. - -## 2. Execution Model: The "Self-Spawning Supervisor" - -To achieve process isolation and deterministic crashes, `first` uses a multiprocess architecture where the test binary spawns *itself*. - -### Three Execution Contexts +## Execution Model -1. **Orchestrator (The Supervisor)** - * **Role:** Manages the test lifecycle, FS snapshots, and scheduling. - * **Trigger:** Default state when `first::test()` is called with no special env vars. - * **Behavior:** - * Does *not* run the user's workload directly. - * Loops through the **Crash Schedule**. - * For each crash point: - 1. Prepares a fresh workspace (`/tmp/first/run_N`). - 2. Spawns **Child: Crash Phase**. - 3. Waits for child to exit (expected signal: SIGKILL). - 4. Snapshots the filesystem (CoW). - 5. Spawns **Child: Verify Phase** against the snapshot. - 6. Reports pass/fail. +FIRST uses a **self-spawning supervisor** pattern with three execution contexts: -2. **Execution Phase (The Victim)** - * **Role:** Runs the actual workload until it crashes. - * **Trigger:** `FIRST_PHASE=EXECUTION` - * **Behavior:** - * `first::test()` sees the env var and immediately executes the `.run()` closure. - * Ignores `.verify()` closure. - * `first::crash_point()` checks the global counter. - * If target reached -> `kill(SIGKILL, self)`. - -3. **Verification Phase (The Inspector)** - * **Role:** Recovers the system and checks invariants. - * **Trigger:** `FIRST_PHASE=VERIFY` - * **Behavior:** - * `first::test()` sees the env var. - * Skips `.run()` closure. - * Executes `.verify()` closure. - * Exits 0 on success, panic on failure. - -### Diagram +| Context | Trigger | Behavior | +|---------|---------|----------| +| **Orchestrator** | Default (no env vars) | Manages lifecycle, spawns children | +| **Execution** | `FIRST_PHASE=EXECUTION` | Runs `.run()`, crashes at target | +| **Verify** | `FIRST_PHASE=VERIFY` | Runs `.verify()`, checks invariants | ```mermaid sequenceDiagram - participant User as cargo test - participant Orch as Orchestrator (Pid 100) - participant Crash as Child: Execution (Pid 101) - participant Verify as Child: Verify (Pid 102) - - User->>Orch: Spawn test_foo - Orch->>Orch: Initialize Env, Plan Schedule - - loop For each Crash Point - Orch->>Crash: Spawn(FIRST_PHASE=EXECUTION) - Crash->>Crash: Run Workload... - Crash->>Crash: Crash Point Reached! (SIGKILL) - Crash--xOrch: Exit(SIGKILL) - - Orch->>Orch: Snapshot Filesystem - + participant Orch as Orchestrator + participant Exec as Execution (Child) + participant Verify as Verify (Child) + + loop For each crash point + Orch->>Exec: Spawn(FIRST_PHASE=EXECUTION) + Exec->>Exec: Run workload → SIGKILL + Exec--xOrch: Exit(137) Orch->>Verify: Spawn(FIRST_PHASE=VERIFY) - Verify->>Verify: Run Recovery & Invariants + Verify->>Verify: Recovery + invariants Verify-->>Orch: Exit(0) end - - Orch-->>User: Test Pass/Fail ``` -## 3. Interface Design - -### Mode Selection -We will use Environment Variables, handled internally by `first`. - -* `FIRST_PHASE`: `EXECUTION` | `VERIFY` (Default: `ORCHESTRATOR`) -* `FIRST_CRASH_TARGET`: Integer ID of the crash point to trigger. -* `FIRST_WORK_DIR`: Absolute path to the isolated directory for this run. - -### Global State -Since crash points can be deep in the call stack, we need global state access. - -* **Location:** `first::rt` (Runtime module) -* **Storage:** `static AtomicUsize` specific to the current process. - * `GLOBAL_CRASH_COUNTER`: Incremented by `crash_point()`. - * `TARGET_CRASH_POINT`: Loaded from env var at startup. - -## 4. Addressing Constraints +## Environment Variables -* **How does `cargo test` work with this?** - * `cargo test` runs the Orchestrator. - * The Orchestrator uses `std::env::current_exe()` to find the test binary. - * It re-runs the binary, filtering for the *specific test name* to avoid recursive bombs. +| Variable | Description | +|----------|-------------| +| `FIRST_PHASE` | `EXECUTION` / `VERIFY` (default: Orchestrator) | +| `FIRST_CRASH_TARGET` | Target crash point ID (1-indexed) | +| `FIRST_WORK_DIR` | Isolated directory for this run | -* **Cleanup?** - * The Orchestrator owns the temp dirs. It cleans them up unless `FIRST_KEEP_ARTIFACTS=1` is set. +## Global State -## 5. Recommendation +Crash points require global state access: -This structure keeps the API simple (`cargo test` just works) while providing the necessary isolation for crash testing. +- **Location:** `first::rt` +- **Storage:** `static AtomicUsize` for crash counter +- **Initialization:** Env vars read once at startup via `OnceLock` diff --git a/docs/architecture/002_crash_point.md b/docs/architecture/002_crash_point.md index c59c21a..0c8bc29 100644 --- a/docs/architecture/002_crash_point.md +++ b/docs/architecture/002_crash_point.md @@ -1,128 +1,51 @@ -# Architecture Decision: `crash_point()` Semantics +# ADR-002: `crash_point()` Semantics -## 1. Overview +**Status:** Accepted -`first::crash_point()` is the core primitive for explicit crash injection. This document defines its exact behavior for v0.1. - ---- - -## 2. Function Signature +## Function Signature ```rust -/// A potential crash location in the execution. -/// -/// When FIRST is active during the "Execution" phase, this function: -/// 1. Increments the global crash counter. -/// 2. If `counter == target`, terminates the process immediately. -/// 3. Otherwise, returns normally. -/// -/// When FIRST is inactive (Orchestrator or Verify phase), this is a no-op. pub fn crash_point(label: &str); ``` -**Labels are required.** This ensures: -- Every crash point is identifiable in logs/reports. -- Developers think intentionally about where crashes matter. -- Future features (e.g., filtering by label) are possible. +**Labels are required** — ensures every crash point is identifiable in logs and failure reports. ---- +## Behavior -## 3. Counter Increment Timing +| Phase | Behavior | +|-------|----------| +| Execution | Increments counter, may terminate process | +| Orchestrator | No-op | +| Verify | No-op | -The counter increments **at the start of `crash_point()`**, before any check. +## Counter Semantics + +Counter increments **before** the target check (1-indexed): ``` -crash_point("A") → Counter: 0 → 1. Check: 1 == target? -crash_point("B") → Counter: 1 → 2. Check: 2 == target? -crash_point("C") → Counter: 2 → 3. Check: 3 == target? +crash_point("A") → Counter: 0 → 1. Check: 1 == target? +crash_point("B") → Counter: 1 → 2. Check: 2 == target? ``` -**Rationale:** Incrementing first means "crash point N" refers to "crash *before* the Nth point completes." This models "power loss during operation N." - ---- +## Crash Metadata -## 4. Crash Metadata +When triggered, emits JSON to stderr before `SIGKILL`: -When a crash is triggered, the following is recorded (written to a file or stderr before `SIGKILL`): - -| Field | Description | Example | -|-------|-------------|---------| -| `point_id` | The numeric crash counter value | `42` | -| `label` | The string passed to `crash_point()` | `"after_wal_write"` | -| `seed` | The test's random seed (if any) | `12345` | -| `work_dir` | Path to the test's isolated directory | `/tmp/first/run_42` | - -**Format (JSON to stderr before kill):** ```json -{"event":"crash","point_id":42,"label":"after_wal_write","seed":12345,"work_dir":"/tmp/first/run_42"} +{"event":"crash","point_id":5,"label":"after_commit","seed":null,"work_dir":"/tmp/first/run_5"} ``` -The Orchestrator parses this from the child's stderr after `SIGKILL`. - ---- - -## 5. Target Not Reached - -If the "Execution" phase completes *without* the target crash point being reached: - -1. The child process exits normally (exit code 0). -2. The Orchestrator detects this. -3. **Interpretation:** The schedule is exhausted. All crash points have been explored. -4. The test passes (assuming all previous iterations passed). - -**Edge Case:** If `target == 0`, we crash immediately at the first `crash_point()` call. - ---- - -## 6. Determinism Guarantees (v0.1) - -For v0.1, we guarantee: - -| Property | Guarantee | -|----------|-----------| -| **Same crash point** | Given the same `target`, the crash occurs at the same logical location. | -| **Same label** | The label is always the one passed to the matching `crash_point()` call. | -| **Same filesystem state** | If the workload is deterministic, the FS state at crash is identical across runs. | - -**NOT Guaranteed (v0.1):** -- Thread interleaving (single-threaded model assumed). -- Timing of background OS operations (page cache flush). - ---- - -## 7. No-Op Behavior - -When `FIRST_PHASE` is not `EXECUTION`, `crash_point()` does nothing: - -```rust -pub fn crash_point(label: &str) { - if !is_execution_phase() { - return; // No-op: Orchestrator or Verify phase - } - // ... increment and check ... -} -``` - -This allows the same test code to run in all phases without conditional compilation. - ---- - -## 8. Summary - -| Question | Answer | -|----------|--------| -| When does counter increment? | At the *start* of `crash_point()`, before the check. | -| Are labels required? | **Yes**, always required. | -| What metadata is recorded? | `point_id`, `label`, `seed`, `work_dir` (JSON to stderr). | -| What if target not reached? | Child exits 0; Orchestrator treats it as "schedule exhausted." | -| Determinism for v0.1? | Same target → same crash location & FS state (single-threaded). | - ---- +## Exit Behavior -## 9. Open Questions (for later) +| Condition | Exit Code | Orchestrator Interpretation | +|-----------|-----------|----------------------------| +| Target reached | 137 (SIGKILL) | Expected crash → run VERIFY | +| Target not reached | 0 | Schedule exhausted → success | -- Should we support `crash_point_if(condition, label)` for conditional crashes? -- Should labels be globally unique, or can they repeat (e.g., in a loop)? -- Should we log *all* crash points encountered, not just the triggered one? +## Determinism Guarantees -These can be deferred to v0.2+. +| Guaranteed | Not Guaranteed | +|------------|----------------| +| Same target → same crash location | Thread interleaving | +| Same label | OS page cache timing | +| Same FS state (if workload is deterministic) | | diff --git a/docs/architecture/004_orchestrator.md b/docs/architecture/004_orchestrator.md index 69547cf..7e261df 100644 --- a/docs/architecture/004_orchestrator.md +++ b/docs/architecture/004_orchestrator.md @@ -1,136 +1,64 @@ -# Architecture Decision: Test Runner and Orchestration Loop +# ADR-004: Orchestrator -## 1. Overview +**Status:** Accepted -This document defines the orchestration engine for FIRST v0.1 — the supervisor that turns one test function into N crash-verify cycles. - ---- - -## 2. Core Loop +## Core Loop ``` target = 1 loop { - 1. Create work_dir: /tmp/first/run_{target} - 2. Spawn EXECUTION child with target + 1. Create /tmp/first/run_{target} + 2. Spawn EXECUTION child 3. Wait for exit: - - SIGKILL (137) → crash occurred, run VERIFY - - Exit 0 → schedule exhausted, DONE - - Other → test failure, STOP - 4. Spawn VERIFY child against same work_dir - 5. If VERIFY fails → STOP with failure + - 137 (SIGKILL) → run VERIFY + - 0 → done (schedule exhausted) + - other → failure + 4. Spawn VERIFY child + 5. If VERIFY fails → stop 6. target += 1 } ``` ---- +## Decisions -## 3. Locked Decisions (v0.1) - -| Decision | Choice | -|----------|--------| +| Aspect | Choice | +|--------|--------| | Discovery | Iterative (no pre-counting) | | Filesystem | Fresh directory per target | -| Phase selection | Environment variables | | Self-spawning | `std::env::current_exe()` | -| Crash detection | Exit code 137 (SIGKILL) | -| Cleanup on success | Delete (unless `FIRST_KEEP_ARTIFACTS=1`) | -| Cleanup on failure | **Always keep** | - ---- - -## 4. Test Name Filtering - -**Constraint:** FIRST v0.1 assumes one FIRST test per Rust test function. - -**Mechanism:** -1. Orchestrator extracts test name from `std::env::args()` or env var -2. Children are spawned with: `cargo test -- --exact` - -**Environment variable:** -``` -FIRST_TEST_NAME= -``` - ---- - -## 5. Environment Variables - -| Variable | Phase | Description | -|----------|-------|-------------| -| `FIRST_PHASE` | All | `EXECUTION` / `VERIFY` (default: Orchestrator) | -| `FIRST_CRASH_TARGET` | Execution | Target crash point ID (1-indexed) | -| `FIRST_WORK_DIR` | All | Isolated directory for this run | -| `FIRST_SEED` | All | Random seed for reproducibility | -| `FIRST_TEST_NAME` | Children | Exact test name to run | -| `FIRST_KEEP_ARTIFACTS` | Orchestrator | Set to `1` to keep dirs on success | +| Crash detection | Exit code 137 | +| Cleanup | Delete on success (keep on failure) | ---- +## Environment Variables -## 6. Exit Code Interpretation +| Variable | Description | +|----------|-------------| +| `FIRST_PHASE` | `EXECUTION` / `VERIFY` | +| `FIRST_CRASH_TARGET` | Target crash point (1-indexed) | +| `FIRST_WORK_DIR` | Isolated directory | +| `FIRST_SEED` | Random seed | +| `FIRST_KEEP_ARTIFACTS` | Set to `1` to preserve dirs | -| Exit Code | Meaning | Orchestrator Action | -|-----------|---------|---------------------| -| 137 | SIGKILL (crash occurred) | Run VERIFY phase | -| 0 | Normal completion | Schedule exhausted (success) | -| 101 | Rust panic | Test failure | -| Other | Unexpected | Test failure | +## Exit Codes ---- +| Code | Meaning | Action | +|------|---------|--------| +| 137 | SIGKILL | Run VERIFY | +| 0 | Normal | Schedule exhausted | +| 101 | Panic | Test failure | -## 7. Progress Output - -Minimal, one line per crash point: - -``` -[first] crash point 1: OK -[first] crash point 2: OK -[first] crash point 3: FAILED (see /tmp/first/run_3) -``` - -No spinners, percentages, or verbosity flags. - ---- - -## 8. API Shape +## API ```rust first::test() - .run(|env| { - // Workload: executed in EXECUTION phase only - }) - .verify(|env, crash_info| { - // Recovery + invariants: executed in VERIFY phase only - }); + .run(|env| { /* workload */ }) + .verify(|env, crash| { /* invariants */ }) + .execute(); ``` -**Behavior by phase:** - -| Phase | `.run()` | `.verify()` | -|-------|----------|-------------| -| Orchestrator | Stored, not called | Stored, not called | -| Execution | **Called** | Ignored | -| Verify | Ignored | **Called** | - ---- - -## 9. Components to Implement - -| Component | Purpose | File | -|-----------|---------|------| -| `TestBuilder` | Stores closures, detects mode | `src/test.rs` | -| `Env` | Provides `path()` to closures | `src/env.rs` | -| `CrashInfo` | Parsed crash metadata | `src/env.rs` | -| `Orchestrator` | The supervisor loop | `src/orchestrator.rs` | -| `spawn_child()` | Re-invokes with env vars | `src/orchestrator.rs` | - ---- - -## 10. Explicitly Deferred (NOT v0.1) +## Deferred (v0.2+) -- Snapshots / CoW filesystem -- Parallel crash runs +- CoW snapshots +- Parallel execution - Crash pruning -- Multiple FIRST tests per function -- Structured output formats (JSON) -- Windows/macOS support +- Windows/macOS diff --git a/docs/invariants.md b/docs/invariants.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/limitations.md b/docs/limitations.md index 4b2c1da..9332548 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -1,187 +1,60 @@ # FIRST Limitations -This document describes known limitations of the FIRST framework. - ---- - ## Crash Model (v0.1) -FIRST v0.1 models process crashes using `SIGKILL`. - -### What v0.1 Accurately Simulates - -- Sudden process termination -- Loss of in-memory state -- Incomplete syscalls (operations interrupted mid-execution) -- Recovery from filesystem state left by prior operations -- Page cache flush ordering non-determinism - -### What v0.1 Does NOT Simulate - -- **Power loss** — Actual power failure affects the block layer and hardware -- **Block-layer rollback** — Storage devices may lose data in their write caches -- **Torn writes** — Partial sector writes that can occur during power loss -- **Filesystem metadata rollback** — Directory entries applied but not journaled - -### Detectable Bug Classes - -| Bug Type | Detectable? | Notes | -|----------|-------------|-------| -| Recovery logic errors | ✅ Yes | SIGKILL accurately tests recovery paths | -| Transaction atomicity bugs | ✅ Yes | Commit-before-durable patterns exposed | -| Missing fsync before commit | ✅ Yes | Page cache flush ordering is undefined | -| Missing parent-directory fsync | ❌ No | Requires filesystem interposition | -| Partial sector writes | ❌ No | Requires block-layer simulation | -| Torn writes | ❌ No | Requires hardware-level simulation | - ---- - -## Reference WAL Bug (Intentional) - -The reference WAL contained one intentional recovery-semantic bug, which was introduced and fixed as part of the Milestone 3 proof. This demonstrates that FIRST can expose real crash-consistency violations. - -### Bug Description - -The WAL writes the transaction `COMMIT` marker before ensuring all transaction records are durable: - -``` -write(record_1) -write(record_2) -write(record_3) -write(COMMIT) -fsync() ← records and commit flushed together, order undefined -``` - -The correct implementation would be: - -``` -write(record_1) -write(record_2) -write(record_3) -fsync() ← ensure records are durable -write(COMMIT) -fsync() ← then write commit -``` - -### Failure Mode - -When a crash occurs at `after_commit_write` (after COMMIT is written but before fsync): - -1. Both records and COMMIT are in the kernel page cache -2. The process is killed with SIGKILL -3. The kernel flushes the page cache in undefined order -4. COMMIT may reach disk before all records -5. Recovery sees a committed transaction with missing records -6. **Atomicity invariant violated** - -### Why This Bug Is Valid Under SIGKILL - -This bug does not require power loss or block-layer simulation. It relies only on: - -- **Page cache behavior**: Writes go to page cache, not directly to disk -- **Flush ordering**: Kernel may flush pages in any order -- **Missing durability barrier**: No fsync between records and COMMIT - -When SIGKILL terminates the process, the kernel continues to manage the page cache. The order in which dirty pages are written to disk is not guaranteed to match the order they were written by the application. +FIRST uses `SIGKILL` to simulate crashes. ---- +### What It Simulates -## Proof of FIRST Effectiveness (v0.1) +✅ Sudden process termination +✅ Loss of in-memory state +✅ Recovery from filesystem state +✅ Page cache flush ordering non-determinism -The following proof chain demonstrates that FIRST v0.1 can expose real crash-consistency bugs: +### What It Does NOT Simulate -1. **FIRST injects a crash** at the `after_commit_write` crash point -2. **The process is killed** with SIGKILL -3. **Filesystem state is preserved** exactly as it existed at crash time -4. **Recovery runs** and opens the WAL -5. **Recovery observes** a `COMMIT` marker for transaction 1 -6. **Recovery attempts** to replay the transaction -7. **Some records are missing** (not yet flushed to disk) -8. **Atomicity invariant fails**: committed transaction has partial visibility -9. **Failure is deterministic** and reproducible with the same crash point +❌ Power loss (block-layer effects) +❌ Torn writes (partial sector writes) +❌ Filesystem metadata rollback -This proof validates: +### Detectable Bugs -- Crash point injection works correctly -- SIGKILL-based process termination preserves filesystem state -- Recovery logic is exercised after each crash -- Invariant checking detects atomicity violations -- Failures are reproducible for debugging +| Bug Type | Detectable | +|----------|------------| +| Recovery logic errors | ✅ | +| Transaction atomicity bugs | ✅ | +| Missing fsync before commit | ✅ | +| Missing parent-directory fsync | ❌ | +| Torn writes | ❌ | --- -## Future Versions - -Future versions of FIRST may add enhanced crash simulation: - -### v0.2 — Filesystem Interposition - -- `LD_PRELOAD` to intercept syscalls -- Simulate incomplete `rename()` or `write()` operations -- Expose directory-fsync bugs - -### v0.3 — FUSE-based Simulation - -- Virtual filesystem with selective data loss -- Full control over what survives a crash - -### v0.4 — Block-layer Simulation +## Integration Constraints -- Device-level interception -- Simulate write cache behavior and torn writes +| Constraint | Status | +|------------|--------| +| One `first::test()` per `#[test]` | Required | +| Single-threaded `.run()` closure | Required | +| `#[tokio::test]` / async | ❌ Not supported | +| `crash_point()` from spawned threads | ❌ Undefined | +| Nested workspaces | ❌ Not supported | --- -## Other Known Limitations - -### Single-Process Only (v0.1) - -FIRST v0.1 runs tests in a single-process model. Multi-process or distributed scenarios are not supported. - -### No Concurrency Testing (v0.1) - -Crash points are serialized. Concurrent operations are not explored for race conditions related to persistence. - -### Linux Only (v0.1) +## Platform Support -The current implementation uses Linux-specific APIs (`SIGKILL`, `/proc`). macOS and Windows support is not yet available. +| Platform | Status | +|----------|--------| +| Linux | ✅ Supported | +| macOS | ❌ Planned | +| Windows | ❌ Planned | --- -## Integration Constraints (v0.1) - -### One `first::test()` Per `#[test]` - -| Pattern | Status | -|---------|--------| -| One `first::test()` per `#[test]` | ✅ Supported | -| Multiple `first::test()` in one `#[test]` | ❌ Undefined | - -**Rationale**: The orchestrator manages process lifecycle. Nested or sequential calls conflict. - -### Async Tests Not Supported - -| Pattern | Status | -|---------|--------| -| `#[test]` (sync) | ✅ Supported | -| `#[tokio::test]` | ❌ Not supported | -| `#[async_std::test]` | ❌ Not supported | - -**Rationale**: FIRST uses `fork()` + `SIGKILL` — async runtimes don't survive this. - -### Not Thread-Safe - -| Pattern | Status | -|---------|--------| -| Single-threaded `.run()` closure | ✅ Supported | -| `crash_point()` from spawned threads | ❌ Undefined | - -**Rationale**: Crash point counter uses atomic state but determinism requires single-threaded execution. - -### No Nested Workspaces - -| Pattern | Status | -|---------|--------| -| User-managed dirs inside `env.path()` | ✅ Supported | -| Expecting FIRST to manage nested isolation | ❌ Not supported | +## Roadmap +| Version | Feature | +|---------|---------| +| v0.2 | `LD_PRELOAD` syscall interception | +| v0.3 | FUSE-based simulation | +| v0.4 | Block-layer simulation | diff --git a/docs/proof/reference_wal.md b/docs/proof/reference_wal.md index 9bd3954..800af82 100644 --- a/docs/proof/reference_wal.md +++ b/docs/proof/reference_wal.md @@ -1,148 +1,54 @@ -# Reference WAL: FIRST Proof of Correctness +# Reference WAL: Proof of Correctness -## Summary - -FIRST deterministically exposed and verified the fix for an atomicity bug in the reference WAL where transactions could be partially visible after crash recovery. - ---- +FIRST deterministically exposed an atomicity bug in the reference WAL. ## The Bug -The WAL wrote the `COMMIT` marker before ensuring all transaction records were durable: - ```rust -// BUGGY: records may not be on disk when COMMIT is written +// BUGGY: COMMIT written before records are durable writeln!(self.file, "COMMIT {}", txid)?; crash_point("after_commit_write"); self.file.sync_all()?; // ← too late ``` -If a crash occurred after writing COMMIT but before `fsync()`, the kernel could flush COMMIT to disk before all PUT records, resulting in a committed transaction with missing records. - ---- +If crash occurs after COMMIT but before `fsync()`, records may not reach disk, but COMMIT does → **atomicity violation**. ## Why It's Subtle -- **Invisible in normal testing**: Without crashes, all writes complete successfully -- **Page cache hides the problem**: Writes appear atomic from the application's perspective -- **Correct-looking code**: A single `fsync()` after COMMIT *seems* sufficient -- **Non-deterministic in production**: Depends on kernel flush timing -- **Common pattern**: Many WAL implementations make this exact mistake - ---- +- Invisible without crashes (all writes succeed) +- Page cache hides the non-atomicity +- Single `fsync()` *looks* correct +- Common mistake in WAL implementations -## Crash Point That Exposed It +## FIRST Output ``` -after_commit_write -``` - -This crash point fires after the COMMIT record is written to the page cache but before any `fsync()` call. - ---- - -## FIRST Output (Failing Run) - -Before the fix, FIRST reported: - -``` -[first] crash point 1: OK -[first] crash point 2: OK -[first] crash point 3: OK -[first] crash point 4: OK [first] crash point 5: FAILED (see /tmp/first/run_5) [first] crash label: "after_commit_write" -[first] reason: verification failed with exit code 101 -[first] to reproduce: - FIRST_PHASE=VERIFY FIRST_CRASH_TARGET=5 FIRST_WORK_DIR=/tmp/first/run_5 \ - FIRST_CRASH_POINT_ID=5 FIRST_CRASH_LABEL="after_commit_write" \ - cargo test transaction_atomicity_under_crash -- --exact -``` -The verification phase panicked with: - -``` -Atomicity violation at crash point 'after_commit_write': -committed transaction has only 1/3 records visible. -key1=Some("value1"), key2=None, key3=None +Atomicity violation: committed transaction has only 1/3 records visible. ``` ---- - ## The Fix -Add `fsync()` before writing COMMIT to ensure records are durable first: - ```diff - pub fn commit(&mut self, txid: TxId) { -+ self.file.sync_all().expect("failed to fsync records"); - writeln!(self.file, "COMMIT {}", txid).expect("failed to write COMMIT"); - crash_point("after_commit_write"); - self.file.sync_all().expect("failed to fsync after COMMIT"); + pub fn commit(&mut self, txid: TxId) { ++ self.file.sync_all()?; // ← ensure records durable FIRST + writeln!(self.file, "COMMIT {}", txid)?; + self.file.sync_all()?; ``` -Now the ordering is: - -1. Write all PUT records -2. `fsync()` ← records durable -3. Write COMMIT -4. `fsync()` ← COMMIT durable - -A crash at any point now guarantees atomicity: -- Before step 2: no records, no COMMIT -- After step 2, before step 4: records durable, no COMMIT visible -- After step 4: records and COMMIT both durable - ---- - ## Verification -After the fix, FIRST reports all crash points passed: +After fix: ``` -[first] crash point 1: OK -[first] crash point 2: OK -[first] crash point 3: OK -[first] crash point 4: OK -[first] crash point 5: OK -[first] crash point 6: OK -[first] crash point 7: OK [first] all 7 crash points passed ``` ---- - -## How to Reproduce the Original Bug - -1. Check out the commit immediately before the fix (see `Fixes #9` in git history) -2. Run the crash consistency test: +## Reproduce ```bash cd examples/reference_wal cargo test transaction_atomicity_under_crash -- --nocapture ``` - -3. Observe the failure at `after_commit_write` - -To re-run just the failing verification: - -```bash -FIRST_PHASE=VERIFY \ -FIRST_CRASH_TARGET=5 \ -FIRST_WORK_DIR=/tmp/first/run_5 \ -FIRST_CRASH_POINT_ID=5 \ -FIRST_CRASH_LABEL="after_commit_write" \ -cargo test transaction_atomicity_under_crash -- --exact --nocapture -``` - ---- - -## Conclusion - -FIRST successfully: - -1. **Found** a real atomicity bug via systematic crash point exploration -2. **Reproduced** the bug deterministically with a single command -3. **Verified** the fix by confirming all crash points pass - -This demonstrates FIRST's effectiveness for crash-consistency testing of storage engines.