Skip to content

Commit f2c0f93

Browse files
g-talbotclaude
andcommitted
feat(31): check invariants in release builds, add pluggable recorder
The check_invariant! macro now always evaluates the condition — not just in debug builds. This implements Layer 4 (Production) of the verification stack: invariant checks run in release, with results forwarded to a pluggable InvariantRecorder for Datadog metrics emission. - Debug builds: panic on violation (debug_assert, Layer 3) - All builds: evaluate condition, call recorder (Layer 4) - set_invariant_recorder() wires up statsd at process startup - No recorder registered = no-op (single OnceLock load) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 26fd89d commit f2c0f93

3 files changed

Lines changed: 157 additions & 11 deletions

File tree

quickwit/quickwit-dst/src/invariants/check.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@
1717
// You should have received a copy of the GNU Affero General Public License
1818
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1919

20-
//! Invariant checking macro.
20+
//! Invariant checking macro — Layers 3 + 4 of the verification stack.
2121
//!
22-
//! Wraps `debug_assert!` with the invariant ID, providing a single hook point
23-
//! for future Datadog metrics emission (Layer 4 of the verification stack).
22+
//! The condition is **always evaluated** (debug and release). Results are:
23+
//!
24+
//! - **Debug builds (Layer 3 — Prevention):** panics on violation via
25+
//! `debug_assert!`, catching bugs during development and testing.
26+
//! - **All builds (Layer 4 — Production):** forwards the result to the
27+
//! registered [`InvariantRecorder`](super::recorder::InvariantRecorder)
28+
//! for Datadog metrics emission. No-op if no recorder is set.
2429
25-
/// Check an invariant condition. In debug builds, panics on violation.
26-
/// In release builds, currently a no-op (future: emit Datadog metric).
30+
/// Check an invariant condition in all build profiles.
31+
///
32+
/// The condition is always evaluated. In debug builds, a violation panics.
33+
/// In all builds, the result is forwarded to the registered invariant
34+
/// recorder for metrics emission (see [`set_invariant_recorder`]).
35+
///
36+
/// [`set_invariant_recorder`]: crate::invariants::set_invariant_recorder
2737
///
2838
/// # Examples
2939
///
@@ -36,10 +46,14 @@
3646
/// ```
3747
#[macro_export]
3848
macro_rules! check_invariant {
39-
($id:expr, $cond:expr) => {
40-
debug_assert!($cond, "{} violated", $id);
41-
};
42-
($id:expr, $cond:expr, $fmt:literal $($arg:tt)*) => {
43-
debug_assert!($cond, concat!("{} violated", $fmt), $id $($arg)*);
44-
};
49+
($id:expr, $cond:expr) => {{
50+
let passed = $cond;
51+
$crate::invariants::record_invariant_check($id, passed);
52+
debug_assert!(passed, "{} violated", $id);
53+
}};
54+
($id:expr, $cond:expr, $fmt:literal $($arg:tt)*) => {{
55+
let passed = $cond;
56+
$crate::invariants::record_invariant_check($id, passed);
57+
debug_assert!(passed, concat!("{} violated", $fmt), $id $($arg)*);
58+
}};
4559
}

quickwit/quickwit-dst/src/invariants/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
//! No external dependencies — only `std`.
2727
2828
mod check;
29+
pub mod recorder;
2930
pub mod registry;
3031
pub mod sort;
3132
pub mod window;
3233

34+
pub use recorder::{record_invariant_check, set_invariant_recorder};
3335
pub use registry::InvariantId;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (C) 2024 Quickwit, Inc.
2+
//
3+
// Quickwit is offered under the AGPL v3.0 and as commercial software.
4+
// For commercial licensing, contact us at hello@quickwit.io.
5+
//
6+
// AGPL:
7+
// This program is free software: you can redistribute it and/or modify
8+
// it under the terms of the GNU Affero General Public License as
9+
// published by the Free Software Foundation, either version 3 of the
10+
// License, or (at your option) any later version.
11+
//
12+
// This program is distributed in the hope that it will be useful,
13+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
// GNU Affero General Public License for more details.
16+
//
17+
// You should have received a copy of the GNU Affero General Public License
18+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
20+
//! Pluggable invariant recorder — Layer 4 of the verification stack.
21+
//!
22+
//! Every call to [`check_invariant!`](crate::check_invariant) evaluates the
23+
//! condition in **all** build profiles (debug and release). The result is
24+
//! forwarded to a recorder function that can emit Datadog metrics, log
25+
//! violations, or take any other action.
26+
//!
27+
//! # Wiring up Datadog metrics
28+
//!
29+
//! Call [`set_invariant_recorder`] once at process startup:
30+
//!
31+
//! ```rust
32+
//! use quickwit_dst::invariants::{InvariantId, set_invariant_recorder};
33+
//!
34+
//! fn my_recorder(id: InvariantId, passed: bool) {
35+
//! // statsd.count("pomsky.invariant.checked", 1, &[&format!("name:{}", id)]);
36+
//! // if !passed {
37+
//! // statsd.count("pomsky.invariant.violated", 1, &[&format!("name:{}", id)]);
38+
//! // }
39+
//! if !passed {
40+
//! eprintln!("{} violated in production", id);
41+
//! }
42+
//! }
43+
//!
44+
//! set_invariant_recorder(my_recorder);
45+
//! ```
46+
47+
use std::sync::OnceLock;
48+
49+
use super::InvariantId;
50+
51+
/// Signature for an invariant recorder function.
52+
///
53+
/// Called on every `check_invariant!` invocation with the invariant ID and
54+
/// whether the check passed. Implementations must be cheap — this is called
55+
/// on hot paths.
56+
pub type InvariantRecorder = fn(InvariantId, bool);
57+
58+
/// Global recorder. When unset, [`record_invariant_check`] is a no-op.
59+
static RECORDER: OnceLock<InvariantRecorder> = OnceLock::new();
60+
61+
/// Register a global invariant recorder.
62+
///
63+
/// Should be called once at process startup. Subsequent calls are ignored
64+
/// (first writer wins). This is safe to call from any thread.
65+
pub fn set_invariant_recorder(recorder: InvariantRecorder) {
66+
// OnceLock::set returns Err if already initialized — that's fine.
67+
let _ = RECORDER.set(recorder);
68+
}
69+
70+
/// Record an invariant check result.
71+
///
72+
/// Called by [`check_invariant!`](crate::check_invariant) on every invocation,
73+
/// in both debug and release builds. If no recorder has been registered via
74+
/// [`set_invariant_recorder`], this is a no-op (single atomic load).
75+
#[inline]
76+
pub fn record_invariant_check(id: InvariantId, passed: bool) {
77+
if let Some(recorder) = RECORDER.get() {
78+
recorder(id, passed);
79+
}
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use std::sync::atomic::{AtomicU32, Ordering};
85+
86+
use super::*;
87+
88+
// Note: these tests use a process-global OnceLock, so only the first
89+
// test to run can set the recorder. We test the no-recorder path and
90+
// the recorder path in a single test to avoid ordering issues.
91+
92+
#[test]
93+
fn record_without_recorder_is_noop() {
94+
// Before any recorder is set, this should not panic.
95+
// (In the test binary, another test may have set it, so we just
96+
// verify it doesn't panic either way.)
97+
record_invariant_check(InvariantId::SS1, true);
98+
record_invariant_check(InvariantId::SS1, false);
99+
}
100+
101+
#[test]
102+
fn recorder_receives_calls() {
103+
static CHECKS: AtomicU32 = AtomicU32::new(0);
104+
static VIOLATIONS: AtomicU32 = AtomicU32::new(0);
105+
106+
fn test_recorder(_id: InvariantId, passed: bool) {
107+
CHECKS.fetch_add(1, Ordering::Relaxed);
108+
if !passed {
109+
VIOLATIONS.fetch_add(1, Ordering::Relaxed);
110+
}
111+
}
112+
113+
// May fail if another test already set the recorder — that's OK,
114+
// the test still verifies the function doesn't panic.
115+
let _ = RECORDER.set(test_recorder);
116+
117+
let before_checks = CHECKS.load(Ordering::Relaxed);
118+
let before_violations = VIOLATIONS.load(Ordering::Relaxed);
119+
120+
record_invariant_check(InvariantId::TW2, true);
121+
record_invariant_check(InvariantId::TW2, false);
122+
123+
// If our recorder was set, we should see the increments.
124+
// If another recorder was set first, we can't assert on counts.
125+
if RECORDER.get() == Some(&(test_recorder as InvariantRecorder)) {
126+
assert_eq!(CHECKS.load(Ordering::Relaxed), before_checks + 2);
127+
assert_eq!(VIOLATIONS.load(Ordering::Relaxed), before_violations + 1);
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)