No-Panic Policy¶
BenoStreamDB is a long-running database process. A single unwrap() on an
unexpected None/Err in a request path, an ingest loop, or a decode path
crashes the whole daemon: in-flight queries are dropped, the process restarts,
and the WAL has to recover. Panics are therefore treated as defects in
production code, not as an acceptable error strategy.
Scope¶
Code |
Rule |
|---|---|
Production library code ( |
No |
Production binaries ( |
Same rule |
|
Exempt — tests may unwrap freely |
|
Exempt |
|
Build scripts are exempt in spirit, but |
The rule¶
Prefer, in order:
Propagate:
?with the native error type (HyperstreamError,DataFusionError,anyhow::Error) viaok_or_else/context.Make the function total: where Arrow’s
data_type()(or an equivalent guard) already proves the downcast cannot fail, use.map(|a| ...).unwrap_or(<neutral>)orif let ... else { <neutral> }instead ofunwrap(). A violated invariant then degrades tonull/false/skiprather than panicking.Documented invariant: only where the API genuinely has no infallible constructor and failure is a programming error caught at startup, add a narrowly scoped
#[allow(clippy::unwrap_used, clippy::expect_used)]with a justification comment. Every such site must also be listed below.
Enforcement¶
Ratchet (scripts/no_panic_check.sh)¶
Because src/lib.rs and benostreamdb-search/src/lib.rs already carry
#![deny(warnings)], flipping the restriction lints to hard errors immediately
would fail the build on every not-yet-remediated site. Until remediation
completes, enforcement is a ratchet:
bash scripts/no_panic_check.sh # fail if production panic sites grew
bash scripts/no_panic_check.sh --write # lower the baseline after a fix
The script runs cargo clippy --no-deps --lib with
-D clippy::unwrap_used -D clippy::expect_used -D clippy::panic. Using --lib
(not --all-targets) compiles the crate with cfg(test) off, so test-only
unwraps are excluded automatically. It compares the diagnostic count against
scripts/no_panic_baseline.txt and fails on an increase.
Run it in CI as the no-panic job in .github/workflows/ci.yml.
Hard gate (staged)¶
Once the baseline reaches the residual-invariant set below, the ratchet is
replaced by a hard lint gate. The attributes are already in place, staged behind
the no-panic cargo feature:
#![cfg_attr(
all(not(test), feature = "no-panic"),
deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
)]
At that point CI adds --features no-panic and the feature becomes the
permanent enforcement mechanism; the ratchet script can be retired.
Baseline and inventory¶
Initial production baseline (measured before remediation): 289 sites (247 core library + 42 search library). Current baseline: 0 — the library surfaces of both crates are clean and the hard gate is enforced in CI.
Two module-scoped exemptions remain, both justified in source:
Exemption |
Sites |
Why |
|---|---|---|
|
69 |
Vendored |
|
13 |
|
Everything else — the search crate, graph UDFs, the storage/read path, the
ingest and index-build path, merge/compaction/catalog, the algorithms, the GPU
backends, the Python/FFI bindings, and src/lib.rs — was converted to ?, made
total, or narrowly allowlisted at function scope (see the allowlist section).
Deferred¶
hnswiofile I/O. A truncated or corrupt index file can fail aread_exact/write_allinside the vendored dump/load path, which the module-level exemption therefore still covers. Converting those entry points toResultchanges upstream signatures; it is tracked here as the only user-reachable panic path left in the library.Binary targets.
src/bin/*andbenostreamdb-search/src/main.rsare not covered by this ratchet:cargo clippy --bin <name>re-lints the same-package lib and does not reliably emit the bin’s own diagnostics. They are remediated directly and are covered by the staged feature once bin-root attributes land.
Residual-invariant allowlist¶
These sites are permitted to keep a documented #[allow] because the API has no
infallible form and failure is a startup-time programming error, not a runtime
condition:
src/telemetry/metrics.rs—prometheusmetric constructors (IntGauge::new,IntCounterVec::new, …) returnResultand are only fallible on an invalid name or duplicate registration. All names/help strings are static constants. Failure happens once at startup before the server binds and is unreachable from a request. Guarded by#[allow(clippy::unwrap_used, clippy::expect_used)]onMetrics::new.Compile-time regex literals (e.g.
Regex::new(r"...")) — the pattern is a literal, so construction cannot fail. These may useexpectwith a message.chrono::NaiveDate::from_ymd_opt(1970, 1, 1)— replaced byNaiveDate::default()where possible (it is 1970-01-01), which is infallible.src/lib.rsjemalloc ctl —tikv_jemalloc_ctl::epoch::mib()is a process-lifetime singleton; failure indicates a broken allocator build.src/core/table/state.rsTable::runtime()—rtis set by every public constructor; theexpectasserts a structural invariant. ReturningOption/Resultwould ripple through every synchronous caller just to express “this cannot happen”. Guarded by a function-level#[allow(clippy::expect_used)].src/python/helpers.rsTOKIO_RUNTIME—Runtime::new()has no infallible form and fails only if the OS cannot give the process a reactor and worker threads. If that happens the binding layer is unusable, so failing once at first use with a clear message is correct. Guarded by#[allow(clippy::expect_used)].src/core/segment.rsindexing_pool()andsrc/core/table/maintenance.rsmaintenance_pool()—rayon’sThreadPoolBuilder::build()has no infallible form and fails only on OS resource exhaustion (thread creation). The pools are process-lifetime singletons built on first use; a failure is unrecoverable at startup. Guarded by a function-level#[allow(clippy::expect_used)].
Anything not in this list should be converted to ?, made total, or the
allowlist entry justified in review.