Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ jobs:
run: rustup toolchain install stable --profile minimal --component llvm-tools-preview
- run: rustup override set stable
- run: cargo install cargo-llvm-cov --locked
- run: cargo llvm-cov --all-features --workspace --fail-under-lines 95
- run: cargo llvm-cov --all-features --workspace --json --output-path target/coverage.json
- run: node scripts/check-source-coverage.mjs target/coverage.json

package:
name: Package
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/robustness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Robustness

on:
pull_request:
workflow_dispatch:
schedule:
- cron: "30 2 * * *"
- cron: "0 3 * * 0"

permissions:
contents: read

jobs:
fuzz-pr:
name: Deterministic fuzz smoke
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Install pinned nightly
run: rustup toolchain install nightly-2026-02-17 --profile minimal
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --version 0.13.2 --locked
- name: Run 10,000 state-machine cases
run: >-
cargo +nightly-2026-02-17 fuzz run range_cache_state_machine
fuzz/corpus/range_cache_state_machine --
-runs=10000 -seed=0 -timeout=5 -max_len=4096
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: range-cache-fuzz-pr-artifacts
path: fuzz/artifacts
if-no-files-found: ignore

fuzz-nightly:
name: 15-minute nightly fuzz
if: github.event_name == 'schedule' && github.event.schedule == '30 2 * * *'
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v7
- name: Install pinned nightly
run: rustup toolchain install nightly-2026-02-17 --profile minimal
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --version 0.13.2 --locked
- name: Fuzz for 15 minutes
run: >-
cargo +nightly-2026-02-17 fuzz run range_cache_state_machine
fuzz/corpus/range_cache_state_machine --
-max_total_time=900 -seed=0 -timeout=5 -max_len=4096
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: range-cache-fuzz-nightly-artifacts
path: fuzz/artifacts
if-no-files-found: ignore

miri-weekly:
name: Weekly core Miri
if: github.event_name == 'schedule' && github.event.schedule == '0 3 * * 0'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- name: Install pinned nightly and Miri
run: >-
rustup toolchain install nightly-2026-02-17
--profile minimal --component miri,rust-src
- name: Run core and private-invariant tests
run: >-
cargo +nightly-2026-02-17 miri test
--no-default-features --lib --test core
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ project follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.1.1] - 2026-07-29

### Added

- Expanded Criterion microbenchmarks for cache operations, contention, and
async read-through behavior.
- Complete core and async README examples plus documented benchmark results.
- Deterministic failure, cancellation, concurrency, and private-invariant tests.
- A public-API state-machine fuzz target with checked-in edge-case corpora.
- Source-normalized 100% coverage enforcement, nightly fuzzing, and weekly Miri.

### Changed

- Raised the minimum supported Rust version from 1.85 to 1.86.
- Updated canonical repository links after the project ownership transfer.
- Removed recency bookkeeping from unbounded caches and made bounded touches
reuse stable access-keyed LRU entries.
- Bounded sparse insertion and read planning to relevant ordered ranges.
- Added direct one-gap async reads and consolidated in-flight response locking.

## [0.1.0] - 2026-07-21

Expand All @@ -30,5 +39,6 @@ project follows [Semantic Versioning](https://semver.org/).
- Reference-model property tests, Criterion benchmarks, cross-platform CI, and
coverage enforcement.

[Unreleased]: https://github.com/xav-db/range-cache/compare/v0.1.0...HEAD
[Unreleased]: https://github.com/xav-db/range-cache/compare/v0.1.1...HEAD
[0.1.1]: https://github.com/xav-db/range-cache/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/xav-db/range-cache/releases/tag/v0.1.0
45 changes: 43 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,57 @@ cargo fmt --all -- --check
cargo test --lib --tests --no-default-features
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings
rustfmt --edition 2024 --check $(rg --files -g '*.rs' -g '!target/**' -g '!fuzz/target/**')
RUSTDOCFLAGS="-D warnings" cargo test --doc --no-default-features
RUSTDOCFLAGS="-D warnings" cargo test --doc --all-features
cargo bench --all-features --bench range_cache -- --test
cargo publish --dry-run
```

For coverage, install `cargo-llvm-cov` and run:
## Coverage

Install `cargo-llvm-cov`, then generate and check the source-normalized report:

```bash
cargo llvm-cov --all-features --workspace --json --output-path target/coverage.json
node scripts/check-source-coverage.mjs target/coverage.json
```

The checker requires 100% source function, line, and region coverage. LLVM emits
generic Rust functions once per test binary and raw summaries can count a source
region as missed in one monomorphization even when another executes it. The
checker collapses only identical source coordinates, using their maximum count;
it does not exclude files, functions, or regions.

## Fuzzing

The state-machine target uses only the public core API and checks every
operation against an independent dense reference model. Install the pinned tools
and reproduce the PR run with:

```bash
rustup toolchain install nightly-2026-02-17 --profile minimal
cargo install cargo-fuzz --version 0.13.2 --locked
cargo +nightly-2026-02-17 fuzz run range_cache_state_machine fuzz/corpus/range_cache_state_machine -- -runs=10000 -seed=0 -timeout=5 -max_len=4096
```

Reproduce and minimize a crash with:

```bash
cargo +nightly-2026-02-17 fuzz run range_cache_state_machine fuzz/artifacts/range_cache_state_machine/crash-...
cargo +nightly-2026-02-17 fuzz tmin range_cache_state_machine fuzz/artifacts/range_cache_state_machine/crash-...
```

Promote the minimized input into `fuzz/corpus/range_cache_state_machine/` and
add a readable deterministic regression test that captures the same failure.

## Miri

Run the core and private-invariant tests under the pinned nightly:

```bash
cargo llvm-cov --all-features --workspace --fail-under-lines 95
rustup toolchain install nightly-2026-02-17 --profile minimal --component miri,rust-src
cargo +nightly-2026-02-17 miri test --no-default-features --lib --test core
```

Use a current stable toolchain for development. Changes must remain compatible
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "range-cache"
version = "0.1.0"
version = "0.1.1"
edition = "2024"
rust-version = "1.86"
authors = ["HelixDB, Inc. <hello@helix-db.com>"]
Expand All @@ -12,7 +12,7 @@ documentation = "https://docs.rs/range-cache"
readme = "README.md"
keywords = ["cache", "range", "bytes", "async", "storage"]
categories = ["caching", "data-structures", "asynchronous"]
exclude = [".github/"]
exclude = [".github/", "fuzz/"]

[features]
default = []
Expand Down
62 changes: 23 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,62 +92,42 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
convert::Infallible,
num::NonZeroUsize,
ops::Range,
sync::{Arc, Mutex},
sync::Arc,
};

use bytes::Bytes;
use range_cache::{CacheCapacity, CachedReader, RangeCache, RangeReader, ReaderConfig};

struct ObjectStore {
data: Bytes,
fetched: Mutex<Vec<Range<usize>>>,
}
struct MemorySource(Bytes);

#[async_trait::async_trait]
impl RangeReader<String> for ObjectStore {
impl RangeReader<String> for MemorySource {
type Error = Infallible;

async fn read_range(
&self,
_key: &String,
range: Range<usize>,
) -> Result<Bytes, Self::Error> {
self.fetched
.lock()
.expect("fetch log lock is not poisoned")
.push(range.clone());
Ok(self.data.slice(range))
Ok(self.0.slice(range))
}
}

let source = Arc::new(ObjectStore {
data: Bytes::from_static(b"abcdefghijklmnop"),
fetched: Mutex::new(Vec::new()),
});
let cache = RangeCache::new(CacheCapacity::Bounded(
NonZeroUsize::new(1024).expect("capacity is non-zero"),
));
let source = Arc::new(MemorySource(Bytes::from_static(b"abcdefghijklmnop")));
let reader = CachedReader::new(
Arc::clone(&source),
RangeCache::new(CacheCapacity::Bounded(
NonZeroUsize::new(1024).expect("capacity is non-zero"),
)),
source,
cache,
ReaderConfig::new(NonZeroUsize::new(4).expect("concurrency is non-zero")),
);
let key = String::from("s3://bucket/index");

assert_eq!(
reader.read(&key, 0..8).await?,
Bytes::from_static(b"abcdefgh"),
);
assert_eq!(
reader.read(&key, 4..12).await?,
Bytes::from_static(b"efghijkl"),
);
assert_eq!(
*source
.fetched
.lock()
.expect("fetch log lock is not poisoned"),
vec![0..8, 8..12],
);
Ok(())
}

Expand All @@ -164,20 +144,24 @@ latency is reported instead of apparent byte throughput because the returned

| Operation | Workload | Median estimate |
| --- | --- | ---: |
| Full hit | 32 KiB requested from a 64 KiB cached range | 19.89 ns |
| Gap calculation | 64 resident ranges | 498.41 ns |
| Overlapping insertion | Merge across 64 resident ranges | 3.16 µs |
| Eviction | Insert with 64 resident ranges at capacity | 288.34 ns |
| Concurrent hit | 8 workers sharing one key | 73.21 ns/read |
| Warm read-through | 4 KiB cached read | 85.80 ns |
| Fragmented reconstruction | 64 alternating cached/missing segments | 18.00 µs |
| Coalesced read-through | 32 identical concurrent readers | 11.17 µs |
| Unbounded full hit | 32 KiB requested from a 64 KiB cached range | 10.04 ns |
| Bounded full hit | 32 KiB requested from a 64 KiB cached range | 18.15 ns |
| Gap calculation | 64 resident ranges | 237.17 ns |
| Overlapping insertion | Merge across 64 resident ranges | 2.27 µs |
| Sparse end insertion | 512 resident ranges | 90.25 ns |
| Eviction | Insert with 64 resident ranges at capacity | 180.85 ns |
| Concurrent hit | 8 workers sharing one unbounded key | 47.87 ns/read |
| Cold read-through | One 4 KiB missing range | 282.98 ns |
| Partial read-through | One 2 KiB gap in a 4 KiB read | 840.30 ns |
| Warm read-through | 4 KiB cached read | 76.89 ns |
| Fragmented reconstruction | 64 alternating cached/missing segments | 13.80 µs |
| Coalesced read-through | 32 identical concurrent readers | 4.77 µs |

The coalesced 32-reader case performs one 4 KiB source read; issuing those reads
directly would perform 32 calls and fetch 128 KiB.

Measured with `cargo bench --all-features --bench range_cache -- --noplot` on
commit `4e9e7baa61ede11bf70b0d246954f3187c1328d8` using `rustc 1.97.1` on macOS
commit `115e1f98d9b0c258f5fcd4efeeaab2c165de00e4` using `rustc 1.97.1` on macOS
26.5, an Apple M4 Pro (14 cores), and 24 GiB of memory. These numbers describe
that machine and revision; they are not cross-platform performance guarantees.

Expand Down
Loading