All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Hashderive onFlushPolicyandDurabilityPolicy. Both enums now implementHash, enabling them asHashMap/HashSetkeys for callers that bucket or deduplicate by policy.- Nightly benchmark CI workflow (
.github/workflows/bench-nightly.yml). Scheduled GitHub Actions job that runscargo bench --features encryptionon master with criterion baseline caching between runs, enabling performance regression detection between releases. - jscpd code-duplication gate. A
duplicationjob in CI (.github/workflows/ci.yml) that fails when library-code duplication exceeds 2%. Configured via.jscpd.json; wired intoscripts/verify-gate.shas thejscpdgate.
fuzz_flush_policyno longer fails to compile on current nightly. The&u8 as usizecasts in the fuzz target's parameter decoding were removed by a recent nightly toolchain (E0606). Replaced withusize::from(*b). The nightly Fuzz CI job was red because of this.
scripts/verify-gate.shnow includes ajscpdgate (19 gates total).
Default durability policy flipped to Throughput. Since v0.5.0 the
default was Segment with a documented plan to flip after one release. Seven
releases later, this is the flip. The crate's target use case is the local
throughput buffer in front of cloud sync — the cloud is the durable layer,
the local disk is a throughput buffer, and skipping fsync entirely is the
correct default for that workload.
This is a behavioral change (hence the minor bump): upgrading users who
relied on the Segment default (fsync file data on every flush) will see a
wider crash-loss window (~30s OS dirty window instead of the ~5-30s rename
window). Existing on-disk segment files are unaffected — the policy is
encode-time only, all existing files decode correctly.
If you relied on the old default and need file-level fsync, set the policy explicitly:
let config = SegmentConfig::builder()
.durability(DurabilityPolicy::Segment)
.build();For standalone-queue deployments where this buffer is the last durable copy,
Maximal (fsync file + directory after rename) is recommended over Segment.
- Default
DurabilityPolicyflipped fromSegmenttoThroughput(src/lib.rs): the#[default]attribute moves fromSegmenttoThroughput.SegmentConfig::default()now produces a config that skips fsync on every flush.MaximalandSegmentremain fully selectable. - All documentation updated: rustdoc, README crash-behavior table,
docs/DOMAIN_LANGUAGE.md,docs/LIMITATIONS.md,docs/PERFORMANCE.md,FEATURES.md,AGENTS.md.
cargo publish --dry-run --features encryptionadded to the release runbook (AGENTS.md) to catch Cargo.lock version-sync failures before the tag push.
0.5.7 - 2026-08-10
Performance tuning and documentation. The default zstd compression level changes from 3 to 1 based on a full compression-level sweep — level 1 is ~2x faster to encode with negligible ratio loss (3.2x vs 3.1x on text payloads). For a throughput buffer where segments are short-lived, encode speed matters more than ratio. New benchmarks, a LIMITATIONS page visible on docs.rs, and CI/gate hardening round out the release.
- Default
compression_levelchanged from 3 to 1 (src/lib.rs): the compression-level sweep (docs/perf/2026-08-10_compression-level-sweep.tsv) proved level 1 is ~2x faster than level 3 across all payload kinds (uniform, text, json, random) with negligible ratio loss. This is a behavioral change: existing segments at any level still decode correctly (the level is encode-only), but new segments will use level 1 unless explicitly overridden via.compression_level(N). Users who need the previous default should set.compression_level(3)explicitly.
docs/LIMITATIONS.md— comprehensive documentation of all design limitations (no WAL, no cursor persistence, no backpressure policy, etc.) with rationale. Linked from the crate-level rustdoc# Limitationssection (renders on docs.rs) and from the README.benches/bench_concurrent_append.rs— MPMC throughput benchmark covering 1/2/4/8 threads with bothappendandappend_all. Key finding:append_allis 3.6x faster thanappendat 8 threads because it acquires the mutex once per batch.- Latency percentiles (p50/p95/p99) in
examples/scaling.rs— surfaces tail latency from inline flush for both load and drain phases. --dir,--encrypted, and--keepflags inexamples/scaling.rs— enables real-disk (non-tmpfs) testing, XChaCha20-Poly1305 encryption at scale, and keeping the buffer directory for inspection after runs.scripts/compression-sweep.sh— sweeps all 22 zstd levels across 4 payload kinds, outputs TSV for analysis.- Expanded benchmark ranges:
bench_append_allbatch sizes expanded to 10/50/100/500/1k/5k/10k;bench_read_fromscan cache segment counts expanded to 10/100/1k/10k. - Benchmark baseline snapshot in
docs/PERFORMANCE.mdwith all 10 criterion targets' medians. - CI hardening: clippy(fuzz) added to test job,
RUSTDOCFLAGS=-D warningson doc build, clippy components on MSRV job. scripts/verify-gate.shrewritten with--listand--only=flags, 17 gates (was 15), slug validation.scripts/check-changelog-links.sh— GITHUB_TOKEN support, HTTP 403 graceful degradation.
0.5.6 - 2026-08-10
API polish and test-coverage expansion. The SegmentStore trait is now sealed
(feature-gated under loom; no impact on default-feature consumers),
SegmentConfig gains PartialEq/Eq, FlushPolicy gains a reusable
validate() method, and BufferStats/DurabilityPolicy/SegmentConfig gain
human-readable Display impls. Test coverage expands with 2 loom tests, 2
property tests, 1 stress test, 1 fuzz target, and 2 criterion benchmarks. No
on-disk format change, no new dependency.
PartialEq+Eqimpls forSegmentConfig(src/lib.rs): enables directassert_eq!on configs instead of field-by-field comparison. The cipher field is compared byArc::ptr_eq(pointer identity) because theSegmentCiphertrait does not requirePartialEq— comparing key material is a security concern. Two configs sharing the same cipherArcare equal; two configs with distinct-but-equivalent cipher instances are not.FlushPolicy::validate()method (src/lib.rs): centralises thedebug_assert!s forBatchOrIntervalMinconstraints (min_batch <= batch_size,interval <= max_interval) into a reusable public method, callable from both the builder andopen(). The builder'sflush_at_batch_or_interval_minandSegmentConfigBuilder::buildboth call it, andopen_internalvalidates the final policy before use.- Sealed
SegmentStoretrait (src/store.rs): the trait now requires a privateSealedsupertrait marker, preventing downstream implementations. OnlyRealStoreand in-tree test mocks can implement it. The seal marker (SegmentStoreSealed) is re-exported under theloomfeature so loom mock stores can opt in. Displayimpls forDurabilityPolicy,BufferStats, andSegmentConfig(src/lib.rs): all three types wereDebug-only. Now they produce stable, human-readable single-line summaries suitable for logging.SegmentConfig'sDisplaymasks the cipher ([set]/[none]) to prevent key leakage into logs.BufferStatsformats byte values in human-readable binary units (4.0KB,1.0MB). Format follows the existingFlushPolicy::Displaypattern.#[must_use]onBufferStats(src/lib.rs): the stats snapshot is meaningless if discarded — the lint now catches that at compile time.#[doc(alias = "backlog")]onpending_count()(src/lib.rs): discoverability — users searching for "backlog" in rustdoc now find the right method.- Property tests for
compute_store_pressure(src/property_tests.rs): 4 new properties covering zero-max,[0,1]clamping, monotonicity in bytes, and saturation at1.0. - Property tests for
percentile_of_sortededge cases (src/property_tests.rs): 4 new properties covering empty slice, all-equal values, "result is an actual element" (no interpolation), and p0/p100 boundary correctness. segment_size_stats_works_with_xchacha20_encrypted_segmentstest (src/tests.rs): parity test for the XChaCha20-Poly1305 cipher, mirroring the existing AES-GCM test.- Loom tests for
for_each_fromanditer_from(tests/loom.rs): 2 new tests proving the v0.5.5 snapshot-then-release-lock pattern (Phase 2 offor_each_from) and the materialisingiter_frompath are safe under every interleaving with a concurrentappend. Loom suite: 12 → 14 tests. - Property test for
publish_disk_statscorrectness (src/property_tests.rs): verifies both atomic counters (approx_disk_bytesandsegment_count) match the directory truth aftersync_disk_bytesand afterrecover(re-open), across arbitrary append/flush/delete sequences. - Property test for
delete_ackedidempotency under concurrentappend(src/property_tests.rs): scales the loom proof beyond two threads — two concurrent deleters with overlapping ack ranges + an appender, proving the livesegment_countcounter self-heals to directory truth aftersync_disk_bytes,head_seq <= pending_startafter settling, and the stats snapshot is never torn. - Stress test for
segment_size_statsunder concurrentflush+delete_acked(src/tests.rs): proves theO(n_segments)scan is panic-free and structurally valid under arbitrary concurrent mutation, and exactly correct after mutation settles. - Fuzz target for
for_each_from(fuzz/fuzz_targets/): arbitrarystart_seq+limitwith interleaved flush/delete_acked/append operations. Asserts no panic, correctseq → itemmapping, and strict ascent. bench_segment_size_statsbenchmark (benches/): quantifies theO(n_segments)scan cost at 100 / 1k / 10k segments.bench_cipherbenchmark (benches/,--features encryption): measures AES-256-GCM and XChaCha20-Poly1305 overhead vs the no-cipher baseline in the fullflush()encode pipeline.
-
seq_to_indexhelper extraction (src/lib.rs): three duplicatedusize::try_from(seq.saturating_sub(base)).unwrap_or(usize::MAX)call sites inread_from/for_each_fromreplaced with a single privateseq_to_indexhelper. No behaviour change. -
Internal deduplication of library + test boilerplate (
src/lib.rs,src/property_tests.rs): no user-facing change. Five computations that were inlined at multiple call sites are now named, reused methods:BufferInner::pending_count(),BufferInner::latest_sequence(), andBufferInner::pending_start()(const fn where applicable) replace duplicated sequence arithmetic instats(),pending_count(),latest_sequence(),read_from,for_each_from, anddelete_acked.Buffer::compute_store_pressurereplaces the verbatim pressure formula instore_pressure()andstats().Buffer::publish_disk_statsreplaces the two-atomic-store pattern insync_disk_bytesandrecover. In the property-test module,prop_item,prop_config,prop_buffer,concurrent_test_config, andcount_segmentshelpers replace ~100 lines of repeated inline construction. Behaviour is unchanged; all 145 tests (default features) + 12 loom tests pass.
- Version-label corrections across living docs (FEATURES.md, README.md,
AGENTS.md, ROADMAP.md): the post-v0.5.5 docs-health pass corrected every
stale version reference —
_(unreleased)_labels →_(v0.5.5)_, "current release is v0.5.4" → v0.5.5, "All 8 versions" → "All 12 versions", gate count 14 → 15, and property-test count 25 → 28. No code change. - CONTRIBUTING.md lint-architecture correction: the
unchecked_time_subtractionlint was described as anursery-group lint "not listed explicitly"; it is actually apedantic-group lint and is explicitly listed inCargo.toml [lints.clippy]. - TODO_LIST rebuilt from 32-report harvest: 7 → 24 items across 7 categories. ROADMAP gained a "Tooling direction" section. All items verified against code.
- 32 archived status/planning reports annotated and moved to
docs/{status,planning}/archived/. 6 high-value files received inline~~strikethrough~~ done at <hash>markers on concrete action items. - Crate-level
# Guaranteessection (src/lib.rs): documents the panic-free public API contract (Clippy-enforced), single-processflockenforcement, and filename-based crash recovery — previously implicit in CI but invisible to API consumers. - Examples table in crate-level rustdoc (
src/lib.rs): added missingbatch_or_interval_minandsegment_tuningrows (2 of 14 examples were undocumented). - FEATURES.md examples inventory (
FEATURES.md): expanded from 3 to all 14 examples. - AGENTS.md archive convention (
AGENTS.md): documented that resolved reports move todocs/{status,planning}/archived/.
0.5.5 - 2026-08-04
Non-breaking release: panic-free public API, live segment_count,
segment_size_stats tuning primitive, scan-cache TOCTOU fix, strict Clippy
lint architecture, and expanded concurrency-property coverage. No API break,
no on-disk format change, no new dependency.
-
for_each_fromunder concurrentdelete_ackedproperty test (src/property_tests.rs): exercises the delete-ack race window through the lending-iterator callback path, proving it returns no wrong, out-of-order, or payload-mismatched items. -
iter_fromunder concurrent flush + delete property test (src/property_tests.rs): races the materialising iterator against a front deleter and an in-memory flusher simultaneously, proving the wrapper does not introduce new failure modes. -
High-concurrency
segment_countstress test (src/tests.rs):segment_count_stress_4_writers_2_deletersruns 4 writers and 2 deleters in parallel, asserting the live atomic counter never panics and converges to the actual directory count aftersync_disk_bytes. -
SegmentSizeStats+segment_size_stats()(src/lib.rs): a new on-demand size-distribution query returningcount/min/max/mean/p50/p90byte sizes of the on-disk segment files — the tuning primitive forFlushPolicy::Batch(N). Likesync_disk_bytes, it is anO(n_segments)directory scan performed outside the buffer mutex, and is a pure query (it does not mutate the cached counters). Percentiles use the nearest-rank method. The newSegmentSizeStatsstruct is#[non_exhaustive]; non-breaking. -
Deterministic Barrier-based regression test for the scan-cache TOCTOU (
src/tests.rs):scan_cache_toctou_mtime_guard_forces_rescan_after_mid_scan_renameforces the exactscan → rename → scan-returns-staleinterleaving via aHookedStorewrappingRealStorewith twostd::sync::Barriersync points. Nothread::sleep, no retry loop — deterministic. Verified by temporarily reverting the fix: the test fails (10 items instead of 11), confirming it catches the regression. -
Loom coverage for
scan_segments(tests/loom.rs): two new loom tests (read_from_concurrent_flush_scan_cache_no_corruptionandread_from_concurrent_delete_acked_scan_cache_no_corruption) — the first loom tests to exerciseread_from(the scan-cache populate path) under concurrent mutation. Loom suite is now 12 tests (was 9). Proves no deadlocks, no panics, data integrity, and eventual consistency across every interleaving of the scan-cache populate/invalidate surface. -
Live
segment_countinBufferStats(src/lib.rs): a newsegment_count: u64field tracked incrementally alongsideapprox_disk_bytes— incremented onflush, decremented ondelete_acked, recalibrated byrecoverandsync_disk_bytes. Gives callers a live on-disk segment-file count without a directory scan, unlike the one-timeRecoveryReport::segment_countsnapshot. Non-breaking:BufferStatsis#[non_exhaustive]. -
Displayimpl forFlushPolicy(src/lib.rs): human-readable output for all variants (batch(256),interval(5s),batch_or_interval_min(batch=256, min=10, interval=5s, max=60s),manual). Stable format for log-scraping. -
Edge-case tests for
BatchOrIntervalMin(src/tests.rs): three boundary conditions —min_batch == 0(always flushes at interval),max_interval == interval(min_batch irrelevant),min_batch == batch_size(interval arm reduces to batch arm). -
Cipher equivalence tests (
src/tests.rs): provenew(&[u8; 32])andfrom_slice(&[u8])constructors produce interchangeable ciphers for both AES-GCM and XChaCha20-Poly1305. -
Concurrency stress test with
BatchOrIntervalMin(src/tests.rs): 4 writers × 2 500 events with auto-flush atbatch_size=1000. Proves the new policy is safe under contention. -
Standalone example for
BatchOrIntervalMin(examples/): demonstrates the tiny-segment suppression pattern with a burst-then-drip scenario. -
Fuzz target for flush-policy parameters (
fuzz/fuzz_targets/):fuzz_flush_policyexercisesshould_flushover arbitrary parameter combinations (variant, sizes, durations). Never panics on any input. -
baconin the default Nix devShell (flake.nix): live clippy feedback during development. -
publish.ymlidempotency guard (.github/workflows/): queries crates.io API before publishing; skips if the version already exists. Prevents red CI when a tag workflow re-runs after a successful publish. -
CONTRIBUTING.md lint architecture section (
CONTRIBUTING.md): documents the two-tier Clippy strategy so contributors understand why library code deniesunwrap/expectwhile test code allows them. -
last_flushinitialization timing docs (src/lib.rs): documents that the interval clock starts atopen(), not at the firstappend(). -
BatchOrIntervalMinin tradeoffs matrix (docs/DOMAIN_LANGUAGE.md): completes the tradeoffs documentation with the tiny-segment suppression knob. -
CHANGELOG link-validation script (
scripts/check-changelog-links.sh): validates that every GitHub tag URL in CHANGELOG.md points to a real tag. Wired intoscripts/verify-gate.sh(with--no-changelog-linksskip flag for offline runs). -
curlin the Nix devShell (flake.nix):check-changelog-links.shuses curl for GitHub API tag validation. Added todevShells.defaultbuildInputsso the gate runs reproducibly undernix developwithout relying on a system curl. -
Cargo.lock drift check in CI (
.github/workflows/ci.yml):cargo fetch --lockedstep catches unintended transitive dep bumps. -
Release runbook (
AGENTS.md): step-by-step release procedure with gotchas (idempotent publish, GitHub release API, tag-before-push ordering). -
26 historical status reports archived (
docs/status/archived/): resolved reports from July 2026 moved out of the activedocs/status/directory to reduce noise. 6 current reports remain. -
4 older status reports annotated (
docs/status/archived/): the 2026-07-22/23 batch received## Resolutionappendices closing their open items. -
Consistency-model property tests (
src/property_tests.rs): five property tests (16 → 21 total) formalising the two documentedread_fromrace windows. Three deterministic tests verify data correctness for every generated state (surviving items afterdelete_acked, disk/memory split, gap-closes-after-flush); two concurrent tests exercise the actual race windows with generated parameters, including a completeness assertion that the transient gap closes once the flusher settles. -
changelog-linksCI job (.github/workflows/ci.yml): CI now runsscripts/check-changelog-links.sh, closing the split brain where the localverify-gate.shgate validated CHANGELOG tag-links but CI did not — a broken link could ship while CI stayed green. -
segment_countcoverage (src/tests.rs,src/property_tests.rs,tests/loom.rs): three new tests close gaps in the live segment-count atomic. A property test (segment_count_matches_disk_across_flush_delete_ops) asserts the incrementally-maintained counter equals the real on-disk file count after everyappend/flush/delete_ackedop; a loom test (segment_count_self_heals_after_concurrent_flush_and_delete) proves the counter never panics and is recalibrated bysync_disk_bytesafter a concurrent flush+delete (which can momentarily wrap it past zero); andappend_all_auto_flush_increments_segment_countasserts the counter after anappend_all-triggered auto-flush. -
for_each_from+ dual-mutation concurrency property tests (src/property_tests.rs): a property test exercises thefor_each_fromlending iterator under concurrentflush(the same Phase 1/Phase 2 gap asread_from, different code path), andread_from_invariant_under_concurrent_delete_acked_and_flushraces a reader against a deleter AND a flusher at once (previously only single-mutation races were property-tested). -
Standalone
segment_tuningexample (examples/): demonstrates the fullsegment_size_stats()tuning loop — measure a small-batch baseline, sweep candidate batch sizes against a target segment-size window, and pick the first whose p50 lands inside it. The crate's stated tuning use case now has a runnable demonstration. -
Loom absence justification for
segment_size_stats(tests/loom.rs): documents in the module-level "What this does NOT cover" section whysegment_size_statshas no loom test (pure query, reuses the already-coveredscan_segmentspath, acquires no lock the hot path does not already acquire).
-
Panic-free public API: re-entrancy deadlock eliminated at the root (
src/lib.rs):for_each_fromno longer holds the buffer mutex across the user callback — in-memory pending items are snapshotted under the lock, then the lock is released before the callback runs. This removes the only panic path in the library: theassert_not_reenteredguard, theiteration_in_progressflag, and theIterationGuardRAII type are deleted. Re-entrant calls from inside afor_each_fromcallback (append,stats,delete_acked, ...) are now safe instead of panicking. The public API is now provably panic-free with no qualifications. No on-disk format change; no API signature change. The Phase 2 clone is bounded bylimit, not the whole backlog. Three tests insrc/tests.rscover the new behaviour (re-entrant reads, re-entrant mutation, buffer-usable-after-panicking-callback). -
Strict Clippy lint architecture (
Cargo.toml,src/lib.rs): a declarative[lints.clippy]section inCargo.tomldeniespedantic+nursery+ restriction lints (as_conversions,arithmetic_side_effects,unwrap_used,expect_used,indexing_slicing,string_slice,panic_in_result_fn,panic,exit,todo,unimplemented,unchecked_time_subtraction,unreachable) across all targets. The crate-level#![deny(...)]insrc/lib.rsis retained as belt-and-braces documentation of the library-only panic-prevention surface. Test, bench, and example modules carry#![allow(...)]overrides so assertions stay direct. Library code is fully clippy-clean under the entire strict set — provably panic-free on every public API path. Inspired by namtao's "Strict Lints" philosophy. -
cargo-nextestin the default Nix devShell (flake.nix): faster and clearer local test execution with failure isolation. Not a CI dependency — CI continues to usecargo test. -
Property test for
unwrap_envelopeboundary (src/property_tests.rs): 16th property test, exercising theENVELOPE_LENbyte boundary where the pre-rewrite code could panic on untrusted input. -
unwrap_enveloperewritten with bounds-checked access (src/segment.rs): four direct indexing/slicing operations on untrusted bytes (raw[..],raw[range],raw[idx]) replaced with.get()pattern matching. Provably panic-free on any input — including truncated or corrupted segment files. -
Cipher
new()constructors made infallible (src/cipher.rs):AesGcmCipher::newandXChaCha20Poly1305Cipher::newnow useKeyInit::new(GenericArray::from(&key))instead ofnew_from_slice(&key).expect(...). The signature and behavior are identical; the# Panicsdoc sections were removed since the constructors can no longer panic. -
Cloud-sync example's
unreachable!()replaced (examples/cloud_sync.rs): the retry-exhaustion path now returns a gracefulErr(...)instead ofunreachable!(), so copied integration code stays defensive even if the retry control flow changes.
-
iter_fromsequence-number bug with gaps (src/lib.rs): the wrapper enumerated returned items fromstart_seq + i, which is wrong when a deleted segment leaves a gap (e.g.start_seq = 0but the first surviving segment starts at11). The implementation now reusesfor_each_from, which derives eachseqfrom the segment's actualstartor the pending-window base, so the returned(seq, item)pairs are always correct. No API signature change. -
Floating-point false-positive in percentile property test (
src/property_tests.rs):percentile_of_sorted_matches_nearest_rank_for_all_pctused(pct / 100.0 * n).ceil()as the reference, which can round the product across an integer boundary (pct=55, n=100producedexpected=56instead of55). The reference now uses integerdiv_ceil, eliminating the flake. -
Scan-cache TOCTOU under concurrent
flush(src/lib.rs):scan_segmentscaptured the directorymtimeafter itsreaddir. A segment rename landing mid-scan could then pair a post-renamemtimewith a pre-rename (stale) segment list in the cache, so themtimestaleness guard failed to detect it and "a retry sees them" did not hold until the next directory mutation. Themtimeis now captured before the scan, so any mid-scan rename leaves the cachedmtimestale and forces a re-scan on the next call. Surfaced by the newread_from_invariant_under_concurrent_flushproperty test. (Effective on filesystems wheremtimeadvances; themtime_supported == falsepath still relies solely on explicit invalidation.) -
MAPFILE→mapfilebug incheck-changelog-links.sh: the script used uppercaseMAPFILE, which is not recognized as a builtin on GNU bash 5.x. The script would have always failed withcommand not found— it was dead code that had never run successfully before being wired into the gate. -
HEADtag skip incheck-changelog-links.sh: the[Unreleased]compare link usesv0.5.4...HEADper Keep-a-Changelog convention.HEADresolves to the default-branch tip on GitHub but 404s on the tag-ref API. The script now skipsHEADrefs, preventing a false failure on every run with unreleased changes. -
verify-gate.shstop-on-first exited 0 on failure (scripts/verify-gate.sh):run()captured the command's status withlocal rc=$?after anif "$@"; then ...; fiwith noelse. By POSIX such anifreturns 0 on a false condition, sorcwas always 0 — the default (stop-on-first) mode printedFAIL (rc=0)and exited 0 even when a gate failed. Fixed by capturing the real status via"$@" || rc=$?. The orchestrator also now runs underset -euo pipefail(wasset -uonly);run()returns 0 in--allmode soset -edoes not abort after the first failure (the summary still exits non-zero). -
verify-gate.sh --helprange was hardcoded:sed -n '2,22p'drifted on every header edit. Replaced with anawkfilter that prints the comment block dynamically (skips the shebang, prints#lines, stops at the first non-comment line) — self-maintaining. -
Loom test sentinel polluted the assertion (
tests/loom.rs):read_from_concurrent_delete_acked_scan_cache_no_corruptionappendedid: 99to invalidate the scan cache but did not filter it from the final assertion. The sentinel is now a named constant (SENTINEL_ID) and is filtered out so the asserted set contains only the real items under test.
-
Updated test counts and descriptions (
FEATURES.md,AGENTS.md): unit tests now 116 (was 115), property tests now 28 (was 26), with descriptions expanded to cover the newfor_each_from+delete_acked,iter_from+ flush + delete, andsegment_countstress tests. Theiter_fromseq-number fix is also documented in theOwned-item iteratorcapability note. -
Verification-discipline refresh (
AGENTS.md): rule 4 now cites the canonicalscripts/verify-gate.shgate and documents theset -euo pipefailrun()rewrite that captures real exit status, replacing the four-command hand-rolled subset.
-
Clarified
pending_count()vsunflusheddistinction (src/lib.rs): the rustdoc now states explicitly that "pending" means not yet acknowledged (the total backlog of on-disk segments plus in-memory items), not "not yet flushed." It notes thatflush()leaves the count unchanged and that the on-disk / in-memory split is not exposed separately by the public API. No API change (option (c) from the 2026-08-02 report's Q3). -
segment_countunderflow / wrap contract (src/lib.rs): the field's rustdoc now documents the two benign wrap scenarios (external file removal; concurrent flush+delete wherefetch_sublands beforefetch_add) and thatsync_disk_bytes/recoverrecalibrate to the authoritative directory scan. -
recoveris open-time only (src/lib.rs): the privaterecovermethod now documents that it runs exactly once insideopen/open_with_store/open_with_reportbefore the buffer is shared, so it cannot raceread_from/flush/delete_acked— there is no scan-cache/recovery interleaving window to test. Resolves the open "loom test for scan_segments + recover" item: the race is impossible by construction. -
Pre-encoded
MockStoreinvestigation (tests/loom.rs): the loom module doc now records why a decode-skipping mock is not tractable without compromising fidelity — encode is already skipped (the store receives pre-encoded bytes), and decode is the read path loom exists to exercise; bypassing it would need a production test-hook this crate rejects. Resolves the open "investigate pre-encoded MockStore" item.
0.5.4 - 2026-08-02
Non-breaking batch: a new FlushPolicy variant for tiny-segment suppression,
a counting-allocator regression guard, expanded concurrency tests, and
broader documentation. No API break, no on-disk format change, no new
dependency.
-
FlushPolicy::BatchOrIntervalMin(src/lib.rs): a new flush policy variant that suppresses tiny segment files during low-throughput periods. Flushes immediately atbatch_size, atintervalonly if at leastmin_batchitems are pending, or unconditionally atmax_interval(safety valve for crash-recovery latency). AddedSegmentConfigBuilder::flush_at_batch_or_interval_minconvenience setter withdebug_assert!validation guarding against contradictory configs (min_batch > batch_size,interval > max_interval). Covered by pure decision tests (no wall-clock dependency), an integration test for the batch-size trigger path, and an exhaustive proptest assertingshould_flushmatches the documented formula across all parameter combinations. -
Allocation-count regression guard (
tests/alloc_guard.rs): a counting allocator asserting fixed heap-allocation budgets on the hot paths (warm append, read_from in-memory, stats, append+flush). Machine-independent — catches tail-latency regressions (extra clones, Vec growth,format!in hot loops) without the CI hardware variance that makes absolute-latency thresholds flaky. -
MPMC boundary stress tests (
src/tests.rs): two tests proving thatread_fromnever returns corrupt data under concurrent operation:concurrent_read_and_delete_never_corrupts(spuriousIo(NotFound)when a segment is deleted between scan and read) andconcurrent_read_and_flush_never_corrupts(transient gaps when items leaveunflushedfor a segment file the scan already missed). -
Domain Language expansions (
docs/DOMAIN_LANGUAGE.md):- Consistency model: canonical single-consumer guarantees (read-your-writes, monotonic reads, contiguous result, at-least-once) vs concurrent MPMC behavior (the two race windows above), with retry guidance.
- Tradeoffs matrix: four tradeable knobs (DurabilityPolicy, FlushPolicy::Batch, compression_level, read_from vs for_each_from) and four non-tradeable invariants, with worked examples for both extremes.
- Schema evolution of T: the two-versioning-layer split (SBF1 envelope vs CBOR payload), compatible-change patterns, and migration strategies.
-
Crate-level rustdoc (
src/lib.rs): "Delivery guarantees" and "Schema evolution of T" summary sections linking to the corresponding domain language entries. -
Percentile-latency baseline (
docs/perf/2026-07-23_percentile-latency-baseline.md): documents where criterion's p99/p99.9 data lives and why allocation-count budgets are the CI-stable regression signal. -
Book-insights mapping (
docs/book-insights-mapping.md) and action plan (docs/planning/2026-07-23_15-50_book-insights-action-plan.md): theory-to-practice analysis mapping seven distributed-systems books against the codebase, with a Pareto execution plan and execution log.
-
SegmentConfigexamples in docs now useDefault::default()+ field reassignment instead of struct-literal syntax, which fails for external consumers due to#[non_exhaustive]. -
FlushPolicy time-based tests rewritten as pure decision tests. The
should_flushmethod is a pure function — the tests now call it directly with syntheticDurationvalues instead ofthread::sleep+ file I/O. Eliminates all CI flakiness from wall-clock dependency and reduces test runtime from ~3s to ~0.02s.
docs/PERFORMANCE.mdtuning guide: addedBatchOrIntervalMincallout in the FlushPolicy section as the write-amplification alternative for low-throughput producers who cannot useManual + append_all.
0.5.3 - 2026-07-22
- chacha20poly1305 0.10 → 0.11: migrated
XNonce::from_slice→XNonce::from/try_intoin the XChaCha20-Poly1305 cipher and thebring_your_own_cipherexample, mirroring the same migration already done foraes-gcm0.11. The on-disk format is unchanged.
docs/CIPHERS.mdnonce snippet drifted from its runnable counterpart (examples/bring_your_own_cipher.rs) — the markdown still showed the deprecatedNonce::from_slicepattern after the 0.11 migration. Both encrypt and decrypt snippets now match the compiled code.
0.5.2 - 2026-07-22
Performance-focused batch: no API break, no on-disk format change, no new dependency. Three levers that make the cloud-sync deployment target faster by default and show producers how to remove the flush path from the append hot path.
- Performance tuning guide (
docs/PERFORMANCE.md§ "Tuning for your workload"). Surfaces the four config-only Tier 0 levers (DurabilityPolicy,FlushPolicy,compression_level,for_each_from) in impact order, each with a code snippet and a "when NOT to use" guardrail. The README crash-behavior table now cross-links to the section. examples/background_flush.rs— the recommended pattern for p99-sensitive producers:FlushPolicy::Manual+ a caller-owned timer thread with an atomic shutdown flag and a final synchronous flush before exit. Achieves the same append/flush decoupling a library-internal worker would, without adding a per-buffer thread, a channel, or delayed error propagation to the crate. Seedocs/planning/2026-07-21_08-26_flush-worker-and-tier-0-levers.md§ "Addendum" for the design rationale.
flush()now recycles theunflushedVec capacity across flushes. Previouslystd::mem::takeleft the field at zero capacity, forcing the next batch'sappend()calls through ~log2(N) reallocs. The new pathreserves the previous batch's capacity inside the same lock scope as the take — one upfront allocation replaces incremental growth. No public API change, no new lock acquisition, no change to the concurrency contract. Covered by a new unit testflush_preserves_unflushed_capacity_for_next_batch.
- README expansion covering primitives that shipped in v0.4.1 / v0.5.0 but
were only documented in rustdoc and
FEATURES.md: a Cargo features table (default = [],encryption,loom,fuzzwith semver-stability notes), anappend_allone-liner in Quickstart, aniter_fromexample next to the drain loop, and anopen_with_reportcrash-recovery example. The contents block and section anchors were updated to match. SegmentBufferrustdoc: added a# Concurrencysection documenting MPMC semantics, the single-processflockinvariant, the mutex-never-held-across-I/O rule, the loom-provendelete_acked+appendinterleaving, and the re-entrancy panic. Added#[doc(alias = "queue" | "spool" | "wal" | "writeahead" | "log")]for rustdoc search discoverability from other ecosystems. Cross-linked allexamples/*.rsfrom the crate-root rustdoc with a one-line-per-example table.
actionlintstep added toscripts/verify-gate.sh(with--no-actionlintskip flag) and a matchingactionlintjob added to.github/workflows/ci.yml. YAML parse is the floor; actionlint catches${{ }}expression syntax errors,needs:cycles, deprecated action versions, and runner/os typos that the YAML parser accepts silently.- lychee redirect URLs documented. The two redirects lychee reports on
the README (
docs.rs/segment-bufferanddocs.rs/segment-buffer/badge.svg) are idiomatic docs.rs patterns; added an explanatory comment to.github/lychee.tomlso future maintainers don't "fix" them and break the badge convention. - Historical doc annotations (
update-old-docspass). 12 of the 23docs/{status,planning,perf}/2026-07-2*files annotated with non-destructive resolution notes: inline corrections for stale TL;DR claims (MSRV 1.85→1.86 resolution, "Cargo.toml still at 0.4.2" → shipped, refuted anchor "may be broken" alarm), and end-of-file resolution appendices for self-review reports whose TODO items have since been completed. 11 files left untouched (already self-resolving, still-current design docs, or already annotated by a prior session). No historical file was rewritten — all annotations are non-destructive per theupdate-old-docsskill contract.
0.5.1 - 2026-07-20
Metadata-only patch: fixes the crates.io discovery surface (description + keywords) and the crate-root rustdoc, which still carried the pre-v0.5.0 "durable bounded queue" framing after the v0.5.0 release shipped. No API change, no on-disk format change, no migration.
Cargo.tomldescription and keywords updated to match the v0.5.0 reframing. The description now leads with "high-throughput local buffer for cloud sync" (was "durable bounded queue"); keywords now includecloud-syncandspool(dropped the genericdiskanddurable). This fixes the crates.io search/landing surface — the #1 discovery path for new users — which still showed the pre-reframing positioning after v0.5.0 shipped.
- Crate-root rustdoc (
src/lib.rs//!) and theSegmentBuffer<T>struct doc comment still described the crate as a "durable bounded queue". Both now lead with the cloud-sync positioning, matching the README andAGENTS.mdproduct-positioning section. Visible on the docs.rs module page and in IDE hovers.
0.5.0 - 2026-07-20
The v0.5.0 cloud-sync throughput batch — the first release that makes
the 2026-07-20 reframing (single-process throughput buffer for cloud sync,
with optional performant encryption, durability-configurable, at-least-once
delivery) literally true rather than aspirational. Breaking changes are
batched so users upgrade once. See
docs/planning/2026-07-20_03-40_v0.5.0-cloud-sync-throughput-batch.md for
the full Pareto plan and docs/planning/2026-07-20_05-50_envelope-v2-design-and-v0.6-deferrals.md
for the deferred-to-v0.6 items with rationale.
DurabilityPolicyenum (Maximal/Segment/Throughput) — selects per-flush fsync behavior.Segment(today's behavior) stays the default for one release for backward compatibility; cloud-sync deployments switch toThroughputto eliminate the per-flush fsync from the hot path (~12% faster thanSegment, ~26% faster thanMaximalon thebench_durability_policybenchmark).Maximaladdsdir.sync_all()after the rename, closing the rename-window gap that today'sSegmentalready has. Threaded throughSegmentStore::write_atomicas a third parameter; the trait signature change is a breaking change (the trait is documented as not-stable-semver-surface under theloomfeature, but external implementors must update).flock-based single-process lock —open()acquires an exclusiveflockon<dir>/.segment-buffer.lockviafs4::FileExt::try_lock, fail-fast with the newSegmentError::Locked { path }if another process holds it. The lock file handle is held for the lifetime of the buffer and released by an explicitDropimpl (the kernel would release it on fd close regardless). Loom tests bypass the lock viaopen_with_store(loom does not model the filesystem).XChaCha20Poly1305Cipherunder theencryptionfeature (alongside the legacy-compatibleAesGcmCipher). Writes `[24-byte nonce][ciphertext- 16-byte Poly1305 tag]
. Eliminates AES-GCM's 2³²-message-per-key limit via the 24-byte extended nonce; constant-time in software (no AES-NI dependency). TheSegmentConfigBuilder::recommended_cipher(key)helper installsXChaCha20Poly1305Cipher— the recommended choice for new buffers. Legacy AES-GCM segments still read viaAesGcmCipher`.
- 16-byte Poly1305 tag]
IoSiteenum (Dir/Segment(PathBuf)/Unknown) replaces theOption<PathBuf>onSegmentError::Io. Makes the "no path" case explicit (was an overload ofNonecovering both "directory-level failure" and "no context attached yet");with_pathand the newwith_dirtag Unknown Io errors at high-value call sites.SegmentError::Locked { path }— the typed error returned when the single-process lock is contended. Distinct fromIoso callers can pattern-match the contention case without sniffing error strings.SegmentIter<'_, T>— owned-item iterator yielded bySegmentBuffer::iter_from(start, limit). Returns(seq, item)pairs; works with standardIteratorcombinators (.take,.filter,.map) and theforloop. Materialises up tolimititems eagerly; the existingfor_each_fromlending iterator stays for the zero-copy in-memory tail.- mtime capability probe for the scan cache —
open()writes a sentinel file twice with a 15ms sleep and checks whether the kernel updated its mtime. On filesystems where the probe succeeds (ext4/xfs/ btrfs/apfs/ntfs defaults),scan_segmentsstat-checks the directory mtime against the cached value and re-scans if it moved (detects external manipulation: backup tools, manualrm, operator quarantine). On filesystems where the probe fails (some FUSE, network fs with coarse granularity), the cache stays warm until an in-process mutation invalidates it — the safe default that avoids the0 == 0false-positive. - Pooled read-side zstd
Decompressor— symmetric to the write-sideCompressorpooling that landed in v0.4.x. Cloud-sync drain loops are read-heavy, so the DCtx pooling matters symmetrically. Falls back tozstd::decode_allonly when the frame header lacks a content size (legacy or externally-written files). - Three new examples:
examples/cloud_sync.rs— runnable at-least-once drain loop with both aReliableUploader(happy path) and aFlakyUploader(transient failure + retry demonstration).examples/cloud_sync_disk_full.rs— the metrics-not-policy pattern: producer applies backpressure viastore_pressure() > threshold, NEVER evicts unacked segments (the at-least-once hard no).examples/idempotent_server.rs— the consumer-side(producer_id, seq)dedup pattern the at-least-once model requires on the server.
bench_durability_policy— criterion A/B benchmark comparingMaximalvsSegmentvsThroughputon a 1000-event flush. Sizes the headline perf claim for the reframed positioning.
SegmentStore::write_atomicsignature now takespolicy: DurabilityPolicyas a third parameter. The trait is reachable only under theloomfeature (documented as not-stable-semver-surface); external implementors must update.SegmentError::Iofield rename —path: Option<PathBuf>is nowsite: IoSite. Pattern-matching callers must update; thewith_pathmethod keeps its name but now producesSegment(path)site. Newwith_dirmethod tags Unknown Io errors asDirsite.SegmentConfig.ciphertype changed fromOption<Box<dyn SegmentCipher>>toOption<Arc<dyn SegmentCipher + Send + Sync>>. Callers using.cipher(Box::new(cipher))must update to.cipher(Arc::new(cipher)). The benefit:SegmentConfigandSegmentConfigBuilderare nowClone(M12) — the cipherArcis shared between clones rather than deep-copied.SegmentConfignew fieldpub durability: DurabilityPolicy—#[non_exhaustive]struct, so direct struct-literal construction was already forbidden externally; the builder pattern is unaffected. Internal callers using struct literals must adddurability: DurabilityPolicy::Segment(orDefault::default()).
fs4dep added for cross-platform advisory file locking (replaces the unmaintainedfs2). Pure-Rust viarustix, nolibcdep.chacha20poly1305dep added under theencryptionfeature.- Nix CI runs on macOS (aarch64-darwin via macos-latest runner) in addition to linux — catches platform-specific regressions earlier.
- Dependabot auto-merge enabled for github-actions, cargo, and fuzz workspaces. PRs auto-squash-merge after required status checks pass (prevented 8-PR pile-up during the 2026-07-20 CI-broken window).
- Latency histogram stress test —
stress_8_writers_4_readers_latency_histogramreports p50/p90/p99/p99.9 per-append latency so tail-regressions are visible in CI output. p99 soft guard at 50ms (debug-mode CI). - Commit signing verification —
gpg.ssh.allowedSignersFilenow configured globally sogit verify-commitsucceeds (was failing with a confusing "needs to be configured" error).
See docs/planning/2026-07-20_05-50_envelope-v2-design-and-v0.6-deferrals.md
for the full migration path. Summary:
- Streaming CBOR deserialise + early-stop at limit (M14) — blocked
by ciborium's private
Deserializerstruct. The format itself encodes no item count; the clean early-stop path requires either forking ciborium or changing the envelope. v0.6's envelope v2 includes an item-count field that retires this. - Per-segment Blake3 checksum (M17) — v1's 3 reserved bytes are too small for a useful checksum at scale. v0.6's envelope v2 ships a trailing-checksum design that covers this.
- Compression-algorithm negotiation, metadata block, streaming cipher,
async I/O,
SegmentStoresecond impl — all deferred pending a concrete consumer request that forces the surface area; the envelope v2 design doc explains each in detail.
These items shipped between v0.4.2 and the v0.5.0 batch; all are part of the v0.5.0 release.
- CI hang:
stress_8_writers_2_readers_throughputandconcurrency_4_writers_1_reader_10k_eventsusedFlushPolicy::Batch(4), creating 20 000 and 2 500 segment files respectively. Under parallel test execution this caused pathological I/O that hung CI for hours. Both now useFlushPolicy::Manual— items stay in-memory during the concurrent phase, testing mutex contention instead of the filesystem. - CI compile failure: The README encryption example doctest referenced
AesGcmCipherbutcargo test(default features) does not enable theencryptionfeature. The doctest is now#[cfg(feature = "encryption")]-gated via the hidden-fn main()pattern so it compiles under both feature sets. - Nix CI:
cachix/cachix-actionfailed because the binary cache does not exist. Addedcontinue-on-error: trueso builds proceed without caching. - Broken README doctest leaked into
cargo test --doc: the crate-root#![doc = include_str!("../README.md")]embedded the README, whose cloud-sync example called an undefinedcloud_uploadfn — turningcargo test --doc(and the Nixtestcheck) red on master. Fixed by removing the embedding entirely (see Removed). - Nix
doccheck: a recent crane version began emitting--no-depsitself, duplicating the flake's explicit--no-depsand turningcargo docinto a hard error ("--no-depscannot be used multiple times"). Removed the redundant arg fromcargoExtraArgs. This had been blockingnix flake check.
SegmentStoretrait +RealStoreimpl (src/store.rs) — the I/O boundary ofSegmentBufferis now an injectable trait object. Production code constructs aRealStoreinternally viaopen()/open_with_report()(signatures unchanged); the trait is only reachable externally under theloomCargo feature, whereSegmentBuffer::open_with_store(dir, config, store)accepts a caller-supplied store. The trait has seven methods covering exactly the formerstd::fssurface (create_dir_all,scan,clean_tmp,segment_size,remove_segment,write_atomic,read_bytes).RealStore's implementations are extracted verbatim from the pre-refactorsegment.rs— on-disk behaviour is byte-identical, the tmp→sync→rename atomicity ofwrite_atomicis preserved, andremove_segmentis idempotent onNotFound. Cost: ~5 ns per I/O call via the vtable (negligible next to zstd+CBOR+file I/O).- Loom coverage of
delete_acked+appendinterleaving — four new loom tests intests/loom.rsexhaustively enumerate thedelete_acked+appendinterleaving that was previously covered only statistically by the stress test. The tests provehead_seq <= pending_start(the "honest backlog" invariant — if it ever broke,pending_countwould under-report, silently dropping items from a durable queue) across every schedule. The mock is backed byloom::sync::Mutex<HashMap<SegmentRange, Vec<u8>>>and faithfully models write atomicity, remove idempotency, and scan ordering. #[track_caller]onassert_not_reenteredand all 9 public methods that call it — re-entrancy panics now point to the user's callback code instead of the internal guard function.supply-chain-report.yml— a new informational, non-gating GitHub Actions workflow (weekly cron + manual dispatch) that runscargo supply-chain publishersto report which crates.io accounts can publish crates in the dependency tree. This is the publisher-attribution layer that neithercargo audit(vulnerabilities) norcargo deny(policy) provides; it surfaces ownership concentration and new-publisher events (the compromised-maintainer vector). Every step iscontinue-on-error, so it never gates a release.
- Bumped 1.85 → 1.86. Driven by
criterion0.8 (requires rustc 1.86) andrand0.10 (edition 2024). Trait-upcasting coercion (stable in 1.86) letsCipherError::source()upcastArc<dyn Error + Send + Sync>to&dyn Errordirectly — theErrorExtworkaround trait that existed solely to bridge this gap under MSRV 1.85 has been deleted. Seedocs/MSRV.md.
- Three-layer separation in
src/:segment.rs(byte-level format) is now pure — its only remaining functions operate on&[u8]/Vec<u8>, never touchingstd::fs. The formerwrite/readfunctions are nowencode_segment/decode_segment. The I/O layer (scan,clean_tmp,write's tmp+sync+rename,read'sfs::read) moved toRealStorein the newsrc/store.rs.lib.rsorchestrates lock + flush policy and delegates every filesystem call throughArc<dyn SegmentStore>. No public API change; every existing test passes unchanged. - aes-gcm 0.10 → 0.11:
Nonce::from_slice→Nonce::from/try_into(upstream deprecation). No public API change. - rand 0.9 → 0.10:
RngCoreimport →Rng(thefill_bytesprovider moved toRngin 0.10). No public API change. - criterion 0.5 → 0.8 (dev-only): switched
criterion::black_box→std::hint::black_box(criterion deprecated the re-export) and adopted 0.8 now that MSRV is 1.86 (criterion 0.8 requires it). - GitHub Actions: bumped all workflows to latest major versions
(
actions/checkoutv4→v7,actions/upload-artifactv4→v7,peter-evans/create-pull-requestv6→v8,cachix/install-nix-actionv27→v31,cachix/cachix-actionv15→v17). - Relaxed
SegmentBuffer'sTbound: dropped the redundantT: 'static(the bound is implied byT: DeserializeOwned, since a borrowed type cannot satisfyfor<'de> Deserialize<'de>;parking_lot::Mutexonly needsT: SendforSend+Sync). Strictly more permissive — a semver-minor API widening. - Nix flake links system libzstd:
commonArgsnow setsZSTD_SYS_USE_PKG_CONFIG = "1", so zstd-sys probes the Nix-provided libzstd via pkg-config (preferring a static link) instead of compiling its bundled C on every cold build. Safe because zstd-sys 2.0.16 ships+zstd.1.5.7and nixpkgs provides exactly zstd 1.5.7. Eliminates the ~30-file C compile that dominated coldnixbuilds.
#![doc = include_str!("../README.md")]— the crate-root rustdoc no longer embeds the README. It caused the broken-doctest leak (see Fixed) and required apostUnpackband-aid becausecraneLib.cleanCargoSourcestrips README.md from the Nix sandbox. The hand-written crate-root doc already links to the README on GitHub; docs.rs renders the README via thereadmefield inCargo.tomlregardless. The deadpostUnpackcopy inflake.nixwas removed alongside it.
Process and verification hardening added after the 2026-07-20 session
discovered that v0.4.1/v0.4.2 had shipped with CI silently broken for 48+
hours (see docs/status/2026-07-20_01-05_*).
-
Stress test regression guard —
stress_8_writers_2_readers_throughputnow asserts zero.zstsegment files are created during the concurrent phase underFlushPolicy::Manual. Catches any future reintroduction of theBatch(4)config that hung CI (commit80257a0). -
Stress throughput re-measured under the corrected
Manualconfig: ~2.29M events/sec (was ~397k, which was actually captured underBatch(4)and mislabeled). Both numbers documented indocs/perf/2026-07-19_v0.4.1_stress_throughput.mdwith correct attribution. -
scripts/verify-gate.sh— the full local verification gate (fmt + clippy×3 + test×2 + doc +cargo deny+cargo audit+ loom) in one command. Encodes AGENTS.md verification rules 4–6 as an executable script. -
AGENTS.md verification rule 9 — before
git tagfor a release, the most recent CI + Nix runs on the target branch must be green (gh run list --limit 4). Local-only green is not release-ready. -
docs/RELEASE.mdpre-tag step now requires the samegh run listgreen-check. -
CONTRIBUTING.mdgained an MSRV-check subsection: dependency bumps must be verified against the declared MSRV before pushing (lesson from the criterion 0.8 MSRV violation). -
dependabot.ymlcriterion ignore retired — thecriterion >= 0.6block is removed because MSRV is now 1.86 and criterion 0.8 is adopted. -
publish.ymlgained acargo publish --dry-runjob that runs on PRs touchingCargo.toml, surfacing packaging issues before a real tag. -
ErrorExtworkaround deleted —src/cipher.rsno longer carries theErrorExttrait + blanket impl that existed only to bridge the pre-1.86 trait-upcasting gap.CipherError::source()now uses native trait-upcasting coercion. ~20 lines removed. -
MSRV audit:
cargo check --all-targets --features encryptionon Rust 1.86.0 passes — no transitive dependency inCargo.lockexceeds the declared MSRV. -
v0.4.2 status report annotated (
docs/status/2026-07-19_22-48_*) — the false "all green / Health 9/10" claims are now inline-corrected with pointers to the real fixes, per the update-old-docs non-destructive annotation discipline.
Profile-guided optimisation session on 2026-07-20, captured in
docs/perf/2026-07-20_hot-path-flamegraph.md and
docs/perf/2026-07-20_read-from-scan-cache.md.
- Pooled zstd
CCtxonSegmentBuffer— flamegraph showed 66% offlushCPU time was inside__memset_avx512_unaligned_erms, called fromZSTD_CCtx_init_compressStream2. The cause:segment::encode_payloadcalledzstd::encode_allper flush, andzstd::encode_allallocates and memsets a fresh ~200 KBCCtxon every call. Fixed by carrying aMutex<zstd::bulk::Compressor<'static>>onSegmentBuffer, allocated once inopen_with_reportand reused for every subsequent flush. Measured speedups onbench_append:batch_115.09 µs → 7.75 µs (2.07×),batch_10028.06 → 20.84 µs (1.32×),batch_100001.21 ms → 1.06 ms (1.11×). For the v0.1.0→v0.2.0 small-batch regression documented indocs/perf/2026-07-19_v0.1.0-vs-v0.2.0.md, this is now a 2.3× net speedup vs v0.1.0 (was a 30–65% slowdown). read_from_scan_cachebenchmark group — new criterion group inbenches/bench_read_from.rsmeasuring cold-vs-warmread_fromacross 10/100/1000 on-disk segment files. Quantifies the v0.4.0 scan-cache win: 6–9% faster at 10 and 100 segments (the bounded-queue design regime). At 1000 segments the readdir cost is no longer dominant and the cache benefit is lost in noise.SmallVec<[T; 16]>forunflushedrejected. A/B benchmarked against the post-compressor-pooling baseline:batch_1regressed 3.2%,batch_1000regressed 8.5%,batch_100/batch_10000within noise. SmallVec's spill-tracking overhead exceeds the saved initial heap allocation. No dependency added; the trade-off analysis is captured in the flamegraph doc.examples/hotpath_profile.rs— standalone driver for flamegraph profiling of the append+flush hot path, used to capture theperf recorddata behind the CCtx-pooling fix. Kept in-tree so future profiling runs reproduce the same workload.
0.4.2 - 2026-07-19
The "process debt + semver-leak closure" release. All changes are additive or
internal (no breaking changes; drop-in upgrade from v0.4.1). Driven by the
brutally honest v0.4.1 self-review, which uncovered four critical gaps
(fuzz_hooks semver leak, no CI loom job, missing dual audit+deny gate,
missing domain-language docs).
fuzzCargo feature — opt-in feature exposing thefuzz_hooksmodule (parse_filename,wrap_envelope,unwrap_envelope,SegmentRange) for out-of-tree fuzz targets. Items reachable through this feature are not part of the semver contract and may change in any release without a major bump. The in-tree fuzz crate enables this feature infuzz/Cargo.toml.- CI
loomjob —RUSTFLAGS="--cfg loom" cargo test --features loom --release --test loomruns on every push and PR. The#![cfg(loom)]test file is invisible tocargo testby default and rotted silently between v0.4.0 and v0.4.1 (the v0.4.0FlushPolicychange removed fields the loom test still referenced); this job prevents that class of regression. - Fuzz target:
fuzz_append_all— fuzzesappend_allover arbitrary iterator behavior (empty, single, large) with 4 invariants: never panics,pending_countgrows by batch size,last_seqadvances correctly, follow-up empty iterator is a no-op. 771k+ runs / 16s, zero crashes. - Property test:
append_all_assigns_contiguous_sequences_across_batches— varies batch sizes (0–50) across multipleappend_allcalls and asserts seqs stay contiguous at the batch boundary. Catches off-by-one regressions in thenext_seqcounter. - Property test:
sync_disk_bytes_matches_actual_disk_usage— after every mutation cycle (flush + append),sync_disk_bytes()must bringstats().approx_disk_bytesinto exact agreement with the sum of.zstfile sizes on disk. Catches reconciliation drift. docs/DOMAIN_LANGUAGE.md— glossary for segment, head_seq, next_seq, acked_seq, envelope, flush, recover. Codifies the ubiquitous vocabulary for issues, doc comments, and commit messages.docs/CIPHERS.md— cipher internals + worked bring-your-own-AEAD examples: ChaCha20-Poly1305 (via thechacha20poly1305crate), no-op cipher (testing only), and an explanation of what the cipher does and does not see (item boundaries, filename, envelope).docs/perf/2026-07-19_v0.4.1_stress_throughput.md— v0.4.1 stress test baseline: ~397k events/sec under 8-writer × 2-reader contention withFlushPolicy::Manual. Reproduction command + interpretation included.
fuzz_hooksis now#[cfg(any(test, feature = "fuzz"))]instead of#[doc(hidden)] pub. This closes a v0.4.1-introduced semver leak.#[doc(hidden)]hides items from rustdoc but does NOT remove them from the public API surface; the cfg gate does both. SeeCONTRIBUTING.md→ "Internal hooks:#[cfg]over#[doc(hidden)]" for the rationale.Cargo.tomldescription rewritten for crates.io search clarity: "Durable bounded queue: batch-spills to zstd+CBOR segment files with ack-based deletion and filename-based crash recovery. No WAL, no metadata db."- CI
supply-chainjob renamed tocargo audit + cargo denyfor discoverability (no behavior change).
- Broken
AesGcmCipherdoc link warning under default features (the[AesGcmCipher]intradoc link failed to resolve when theencryptionfeature was off). Replaced with a prose reference to "theAesGcmCipherbehind theencryptionfeature".RUSTDOCFLAGS="-D warnings" cargo docis now clean under all feature combinations.
- AGENTS.md verification discipline gained two new hard rules:
- Rule 5: "The supply-chain gate is BOTH
cargo auditANDcargo deny check." They pull from different advisory sources in edge cases. - Rule 6: "The loom gate is
RUSTFLAGS='--cfg loom' cargo test --features loom --test loom --release."#![cfg(loom)]files are invisible to defaultcargo testand silently rot.
- Rule 5: "The supply-chain gate is BOTH
- CONTRIBUTING.md gained a new section: "Internal hooks:
#[cfg]over#[doc(hidden)]" — codifies the lesson from the v0.4.1 semver leak so the next agent doesn't repeat it.
0.4.1 - 2026-07-19
The "safety + trust depth" release. All changes are additive (no breaking changes). On-disk format, encryption contract, and API shapes are unchanged from v0.4.0.
for_each_fromre-entrancy guard — calling any&selfmethod on the buffer from inside afor_each_fromcallback now panics with a clear message ({method}: cannot call from within a for_each_from callback) instead of silently deadlocking. The guard is Drop-cleared for panic safety, so a panicking callback does not brick the buffer. (Closes the v0.4.0 footgun.)append_all<I: IntoIterator<Item = T>>— batch append under a single lock acquisition. Returns the last assigned sequence number. The whole batch gets contiguous seqs atomically; flush is checked once at the end. Bench:benches/bench_append_all.rsquantifies the lock-acquisition saving vs a loop ofappend.SegmentBuffer::path()— returns&Pathto the segment directory. Removes the need toDebug-parse the buffer to reach the directory.SegmentBuffer::config()— returns&SegmentConfigthe buffer was opened with. Lets callers inspect the flush policy, compression level, and cipher presence without re-deriving them.SegmentBuffer::sync_disk_bytes()— re-stats the segment directory and stores the authoritative total. Corrects drift when an external process (backup, compaction, manual cleanup) touches the directory.fuzz_hooksmodule (#[doc(hidden)]) — exposesparse_filename,unwrap_envelope,wrap_envelope, andSegmentRangefor fuzz targets. Not part of the public API.- Two new fuzz targets:
fuzz_parse_filename(17M+ runs / 16s, zero crashes) andfuzz_envelope(15M+ runs / 16s, zero crashes; fuzzer discovered theSBF1magic dictionary entry organically). - Property tests:
FlushPolicy::Manualnever auto-flushes (up to 499 appends);read_from(start, limit)⊆read_from(start, larger_limit);delete_ackedpending_count is monotone non-increasing;for_each_fromvisits the same items asread_from. - Throughput stress test: 8 writers × 2 readers × 80k events, reports events/sec under contention.
- Loom test:
append_allbatch atomicity under concurrentappend. - CI workflows: nightly cargo-fuzz (
fuzz.yml), weekly flake.lock update (update-flake-lock.yml), cargo-audit + cargo-deny supply-chain job, dependabot.yml for GitHub Actions + cargo. - Docs:
docs/PERFORMANCE.md(methodology),docs/RELEASE.md(runbook),docs/MSRV.md(policy).
packages.defaultinflake.nixnow builds with Rust 1.85 (the declared MSRV) viacraneLibMsrv, proving the package builds on its floor — not just on whatever nixpkgs stable ships.dtolnay/rust-toolchainpinned to a commit hash in all CI workflows (supply-chain hygiene).nix.ymlcachix-action guarded to the canonical repo + optional token, so forks don't attempt uploads to a cache that doesn't exist.- README perf paragraph now carries the methodology caveat inline ("single- run, single-machine; see docs/PERFORMANCE.md").
- README comparison table now carries a freshness disclaimer.
- AGENTS.md session-end checklist gains "release scope approval" and "draft release notes before tagging" items (process guard against the v0.4.0 failure).
- Loom test was broken since v0.4.0 — referenced removed
max_batch_events/flush_interval_secsfields and had an inner attribute inside a function body. Now usesFlushPolicy::Manualvia the builder API. The breakage was invisible because#![cfg(loom)]skips compilation unless--cfg loomis set.
SegmentRangefields are nowpub(werepub(crate)) to support thefuzz_hooksre-export. Thesegmentmodule itself stays private; the fields are only reachable through#[doc(hidden)] fuzz_hooks.
0.4.0 - 2026-07-19
The "API ergonomic + perf" release. Breaking because it removes two
SegmentConfig fields (max_batch_events, flush_interval — replaced by
FlushPolicy), changes the SegmentError::Io variant from tuple to struct,
and renames the now-private flush_interval_secs builder method. On-disk
format, encryption contract, and trait shape are unchanged.
SegmentConfig::builder()— fluent builder overDefault + setters. Removes theDefault + field reassignmentworkaround every external caller had to use under#[non_exhaustive]. Convenience setters:flush_policy,flush_at_batch_size,flush_at_interval,flush_at_batch_or_interval,flush_manually,max_size_bytes,compression_level,cipher.FlushPolicyenum (Batch(usize)/Interval(Duration)/BatchOrInterval { batch_size, interval }/Manual). Replaces the silent OR-combination ofmax_batch_events+flush_interval_secsthat callers had no way to disable.RecoveryReport+SegmentBuffer::open_with_report()— returns(SegmentBuffer<T>, RecoveryReport)so callers can inspect what recovery found (segment count, head/next seq, disk bytes, removed tmp files) programmatically.open()is unchanged and delegates internally.for_each_from(start, limit, F)lending iterator — the zero-clone counterpart toread_from. Benched ~21× faster on 1000 in-memory items (1.2 µs vs 26 µs). Documented deadlock warning: the closure must not re-enter buffer methods while iterating the in-memory tail.examples/crash_recovery.rs— demonstrates that flushed segments survive a process restart and unflushed ones do not, plus the newopen_with_reportAPI.examples/mpmc.rs— 4 writers × 1 reader sharing oneArc<SegmentBuffer>, draining viaread_from + delete_acked..github/workflows/nix.yml— CI workflow runningnix flake check,nix build .#default, the test check, and treefmt.deny.toml— cargo-deny config (advisories, licenses, bans, sources). All four pass green as of release.renovate.json— weekly dependency updates, withnixandgithub-actionsenabled alongsidecargo.release.toml— cargo-release config (no auto-push; tags viasign-tag, hand-curated GitHub releases viagh).- Display snapshot test for
SegmentError::Iowithpath: Some(...)and a test forwith_path(the path-attach helper).
SegmentConfiglostmax_batch_eventsandflush_interval, replaced by a singleflush_policy: FlushPolicyfield. Migration:// before SegmentConfig { max_batch_events: 256, flush_interval_secs: 5, ..Default::default() } // after SegmentConfig::builder() .flush_at_batch_or_interval(256, Duration::from_secs(5)) .build()
SegmentError::Iois now a struct variant:Io { path: Option<PathBuf>, source: std::io::Error }. The bareFrom<io::Error>impl preserves?ergonomics withpath: None; thewith_pathhelper and direct construction attach path context at high-value call sites (write_segment,read_segment,scan_segments). Display:"I/O error: {source}"whenpathisNone, or"I/O error for {path}: {source}"when set.approx_disk_bytesis nowAtomicU64outsideBufferInner.flush()no longer re-acquires the mutex just to bump oneu64;store_pressure()loads the atomic without locking at all.scan_segments()results are cached — invalidated byflush,delete_acked,recover.read_fromfollowed bydelete_ackedno longer pays the directory-scan cost twice.- Tracing fields standardized — every event now carries
path,seq, andbyteswhere they make sense, replacing the inconsistenthead_seq/next_seq/disk_bytes/start/endmix.
- The pre-v0.4.0
SegmentError::Iovariant dropped path context. Operators saw"I/O error: ..."with no file. Now the offending path is carried whenever it is in scope.
0.3.0 - 2026-07-19
This release closes the v0.2.0 semver/honesty debt identified in the
post-v0.2.0 self-reviews. It is a breaking release because
BufferStats and SegmentConfig are now #[non_exhaustive] — downstream
code that uses struct literals to construct either type must switch to
Default::default() + field reassignment (or, in v0.4.0, the planned
SegmentConfig::builder()). The break is intentional and minor:
the on-disk format, the trait shape, the error types, and the encryption
contract are all unchanged from v0.2.0.
Debugimpl forSegmentBuffer<T>— mirrors theBufferStatsfield set plus the directory path. Does NOT print in-memoryunflusheditems, soT: Debugis not required. Snapshot test insrc/tests.rs.CipherError::with_sourcedoc-test — thesource()-chaining constructor now has a runnable example in its rustdoc.- Display snapshot tests for every
SegmentErrorvariant and bothCipherErrorconstructors (msg+with_source) — locks the operator-facing format strings so a carelessthiserror-attribute edit shows up as a test failure rather than silently shifting log output. benches/bench_stats.rs— criterion micro-bench comparingstats()(single lock + 7-field snapshot, ~12 ns) to three individual accessors (~31 ns). The "cheaper" doc claim now cites measured numbers.docs/perf/2026-07-19_v0.1.0-vs-v0.2.0.md— controlled baseline captured viagit worktree v0.1.0 vs HEAD: append 30–65% slower on small batches (envelope + stats bookkeeping has a per-write cost), recover 40–45% faster (recovery refactor paid off). README Status section cites this and the trade-off is honest.- Verification discipline section in
AGENTS.md— four hard rules and a session-end checklist, installed after three same-day sessions produced self-reviews that claimed success without running the verification gate, fabricated working-tree state, and invented baselines. - rust-overlay integration in
flake.nixwith two new devShells:nix develop .#msrv(pinned Rust 1.85.0) andnix develop .#fuzz(nightly forcargo-fuzz). All three MSRV checks (cargo check,cargo test,cargo clippy -- -D warnings) now run locally on the declared MSRV; both fuzz targets now run locally for ≥60s each.
BufferStatsandSegmentConfigare now#[non_exhaustive]— paying down the v0.2.0-introduced semver debt. Downstream struct-literal construction must switch toDefault::default()+ field reassignment. In-crate construction (tests, examples, benches) is unaffected. This is the breaking change that motivates cutting 0.3.0.
- Corrected the "auto-staging Crush hook" myth in the 03-14 and 04-22
self-reviews. Investigation during the v0.3.0 planning session found no
such hook exists; the only Crush hook is
commit-diff-context.sh, which fires when a commit runs (to inject diff context) and does not stage or commit. The three sessions' "lost track of working-tree state" was a real pattern, but the attribution was wrong — the cause was the assistant not runninggit status/git logbefore claiming state. Now codified as Verification discipline rule 1 inAGENTS.md. PROPTEST_CASES=256pinned in CI — removes a flaky-machine variable and matches the release-build default explicitly.
0.2.0 - 2026-07-19
This release hardens the format envelope's legacy-detection contract (the
headline correctness fix), makes CipherError a real error type with source
chaining, adds several API ergonomics (len/is_empty/stats/BufferStats),
and refactors recover() so the mutex is no longer held across filesystem
metadata calls. It is a breaking release because the SegmentError variant
shape and CipherError field visibility changed; bump your dependency with
cargo update -p segment-buffer.
- Format envelope: segment files now carry an 8-byte header (
SBF1magic + 1-byte version + 3-byte reserved), making the on-disk format forward-evolvable without breaking existing readers. Legacy files are auto-detected; existing monitor365 segments keep working with zero migration. The envelope is stripped before decryption, so cipher byte-compatibility is unchanged. - Envelope hardening (correctness): legacy detection now requires the
SBF1magic and the 3 reserved bytes to all be zero. This drops the false-positive rate on legacy encrypted files from 2⁻³² per file (~1 silent mis-detection per 7 full monitor365 deployments) to 2⁻⁵⁶ (negligible across the entire 597M-segment corpus). Existing on-disk files written by this crate still parse (they always wrote zeros for the reserved bytes). CipherErroris now opaque with private fields and two constructors —CipherError::msg(no cause) andCipherError::with_source(preserves the underlying AEAD error forstd::error::Error::source()chaining). The previouspub Stringfield is private; cipher implementations no longer need to know about segment paths (the I/O layer still attaches them when promoting toSegmentError::Cipher).AesGcmCiphernow routes its underlying AEAD failure throughsource()instead of flattening it into aformat!.len()andis_empty()standard collection methods onSegmentBuffer(semantic aliases ofpending_count() == 0for idiomatic call sites).BufferStatsstruct +SegmentBuffer::stats()— point-in-time snapshot ofpending_count,latest_sequence,head_sequence,next_sequence,approx_disk_bytes,max_size_bytes, andstore_pressurecaptured under a single mutex acquisition (no torn reads between calls).SegmentRange::new(start, end)constructor thatdebug_asserts thestart <= endinvariant at construction. Parse-time validation stays loose so legacy files in the wild are surfaced, not dropped.- Static
Send + Syncassertion onSegmentBuffer<T>— turns the documented MPMC thread-safety guarantee into a compile-time contract. #[must_use]onlatest_sequence,pending_count,len,is_empty,store_pressure,is_overloaded, andstatsso accidental discards surface as warnings.- Property-based tests (
proptest): filename bijection, payload bijection, envelope identity, encrypted roundtrip with a varied key (256 cases percargo test), plus CI-runnable analogues of bothcargo-fuzztargets (corrupted-segment read never panics; recovery over arbitrary directory contents never panics). - Encrypted-legacy read coverage: the headline monitor365 byte-compatibility guarantee (a
[nonce][ciphertext]segment file written without theSBF1envelope) is now covered by a regression test; previously the entire encrypted-legacy read path had zero coverage. - Error-matching doc-test:
error.rsnow shows how to match onSegmentError::Cbor { path, phase, .. }to recover the offending file path and quarantine it. - Fuzz scaffold (
cargo +nightly fuzz): two targets —fuzz_corrupted_read(reading bytes-corrupted segments never panics) andfuzz_recovery(opening over a directory of arbitrary garbage never panics). Seefuzz/README.md. FEATURES.md— honest feature inventory by status.ROADMAP.md— long-term direction and explicit non-goals.flake.nix— reproducible devShell withzstd,pkg-config, and the Rust toolchain (nix develop).- Shared
benches/support.rsmodule consolidating the benchmark helpers previously duplicated across all four criterion targets.
- Typed errors (breaking):
SegmentError::Cbor,Cipher, andIntegrityvariants now carry structured context (path: PathBuf,phase: &'static strorreason: &'static str) instead of opaqueStringpayloads. Operators see exactly which file failed and why, without spelunking through logs. CipherErrorfield visibility (breaking): the previouspub Stringfield is now private. UseDisplayor the new constructors.- Extracted
src/segment.rs: the on-disk format (filename contract, envelope, CBOR→zstd→cipher encode/decode pipeline, segment scan, tmp cleanup) now lives in its own module.SegmentBufferfocuses purely on in-memory orchestration and locking. - Renamed the private
BufferInner::pendingfield tounflushedfor precision: it holds items not yet written to a segment file, distinct from the publicpending_count()backlog metric. recover()no longer holds the mutex across filesystem metadata calls. Allfs::metadataI/O now happens before the lock is taken; the lock is held only long enough to publish the rebuilthead_seq/next_seq/approx_disk_bytes. Restores the invariant that the mutex is never held across file I/O.
- Envelope false-positive on legacy encrypted files. As shipped in 0.1.0 unreleased, the envelope magic-only check (2⁻³² false-positive rate) would silently mis-detect roughly 1 in 7 monitor365 deployments' encrypted segments as enveloped, producing spurious
SegmentError::Ciphererrors. Requiring the 3 reserved bytes to also be zero drops the rate to 2⁻⁵⁶. delete_acked(acked_seq)no longer under-reportspending_count()when called while items are still buffered in memory.head_seqis now clamped to the in-memory window so the backlog count stays honest even when the ack cannot remove unflushed items.SegmentBuffer::opendoc corrected: recovery reads filenames only, so it returnsSegmentError::Ioon failure — notCbor/Integrity(those surface atread_fromtime).fuzz/fuzz_targets/fuzz_recovery.rsparser had a dead-codelet _ = rest;and convolutedsplit/peeklogic; rewrote cleanly.
- Continuing the 0.1.0 baseline: extracted from monitor365 and proven on 597M+ events in production.
0.1.0 - 2026-07-19
SegmentBuffer<T>— durable bounded queue backed by zstd-compressed CBOR segment files. Generic over anyT: Serialize + DeserializeOwned + Clone + Send + 'static.SegmentConfigwith tunable batch size, flush interval, max disk usage, and compression level.SegmentBuffer::open(dir, config)constructor with filename-based crash recovery.append(),flush(),read_from(),delete_acked(),latest_sequence(),pending_count(),store_pressure(),is_overloaded()public API.SegmentCiphertrait for pluggable at-rest encryption.AesGcmCipher(AES-256-GCM with random 12-byte nonce prefix) behind theencryptionfeature.SegmentError(thiserror-based) withIo,Cbor,Cipher,Integrityvariants.- 26 unit tests + 2 doc tests covering: basic CRUD, partial reads, limits, crash recovery, concurrent writers/readers (10K events, 4 writers + 1 reader), time-based auto-flush, error paths (corrupted zstd, truncated encrypted, wrong key, no key), encryption roundtrips, and pressure/overload boundary conditions.
- Concurrency bug in
flush(): sequence numbers (start_seq,end_seq) are now computed atomically inside the mutex lock alongside taking the pending events. Previously, a race between concurrentappend()calls could corrupt segment filenames by computing the sequence range from a stalenext_seqread in a second lock acquisition. - Same race in
append(): the returned sequence number is now captured under the same lock as the push, not re-read after releasing the lock.
- Extracted from monitor365 and proven on 597M+ events in production.