use std::time::{Duration, Instant};
pub const STAGGER_BUCKETS: u64 = 64;
#[cfg_attr(not(test), allow(dead_code))]
#[doc(hidden)]
pub const SINGLE_SOURCE_KEY: &str = "";
pub struct SealPolicy {
pub max_bytes: usize,
pub max_rows: usize,
pub max_age: Duration,
pub align: Option<i64>,
}
impl Default for SealPolicy {
fn default() -> Self {
Self {
max_bytes: 8 * 1024 * 1024,
max_rows: 900,
max_age: Duration::from_secs(300),
align: None,
}
}
}
pub struct SegmentAccount {
align: Option<i64>,
first_bucket: Option<i64>,
last_bucket: Option<i64>,
rows: usize,
approx_bytes: usize,
opened_at: Instant,
max_bytes: usize,
max_rows: usize,
max_age: Duration,
}
impl SegmentAccount {
pub fn open_first(stream: &str, source_key: &str, policy: &SealPolicy) -> Self {
let bucket = stagger_bucket(stream, source_key);
let row_offset = (policy.max_rows / (2 * STAGGER_BUCKETS as usize)) * bucket as usize;
let age_offset = (policy.max_age / (2 * STAGGER_BUCKETS as u32)) * bucket as u32;
let byte_offset = (policy.max_bytes / (2 * STAGGER_BUCKETS as usize)) * bucket as usize;
Self {
rows: 0,
approx_bytes: 0,
opened_at: Instant::now(),
max_bytes: policy.max_bytes.saturating_sub(byte_offset).max(1),
max_rows: policy.max_rows.saturating_sub(row_offset).max(1),
max_age: policy.max_age.saturating_sub(age_offset),
align: policy.align,
first_bucket: None,
last_bucket: None,
}
}
pub fn add_row(&mut self, bytes: usize, ts: i64) {
self.rows += 1;
self.approx_bytes += bytes;
if let Some(bucket) = self.bucket_of(ts) {
self.first_bucket.get_or_insert(bucket);
self.last_bucket = Some(bucket);
}
}
fn bucket_of(&self, ts: i64) -> Option<i64> {
self.align.filter(|a| *a > 0).map(|a| ts.div_euclid(a))
}
pub fn starts_new_bucket(&self, ts: i64) -> bool {
match (self.bucket_of(ts), self.first_bucket) {
(Some(next), Some(first)) => next != first,
_ => false,
}
}
pub fn is_due(&self, now: Instant) -> bool {
self.rows > 0
&& (self.approx_bytes >= self.max_bytes
|| self.rows >= self.max_rows
|| now.duration_since(self.opened_at) >= self.max_age
|| self.first_bucket != self.last_bucket)
}
pub fn rotate(&mut self, policy: &SealPolicy, now: Instant) {
self.rows = 0;
self.approx_bytes = 0;
self.opened_at = now;
self.align = policy.align;
self.first_bucket = None;
self.last_bucket = None;
self.max_bytes = policy.max_bytes;
self.max_rows = policy.max_rows;
self.max_age = policy.max_age;
}
pub fn rows(&self) -> usize {
self.rows
}
#[cfg(any(test, feature = "test-support"))]
pub fn targets(&self) -> (usize, Duration) {
(self.max_rows, self.max_age)
}
#[cfg(test)]
pub fn byte_target(&self) -> usize {
self.max_bytes
}
}
pub fn stagger_bucket(stream: &str, source_key: &str) -> u64 {
const PRIME: u64 = 0x0000_0100_0000_01b3;
let absorb = |h: &mut u64, b: u64| {
*h ^= b;
*h = h.wrapping_mul(PRIME);
*h ^= b >> 6;
*h = h.wrapping_mul(PRIME);
};
let mut h: u64 = 0xcbf2_9ce4_8422_2325; for b in stream.as_bytes() {
absorb(&mut h, *b as u64);
}
absorb(&mut h, 0xff);
for b in source_key.as_bytes() {
absorb(&mut h, *b as u64);
}
h % STAGGER_BUCKETS
}
pub fn staggers_identically(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
let mut bit5_flips = 0usize;
for (x, y) in a.bytes().zip(b.bytes()) {
match x ^ y {
0 => {}
0x20 => bit5_flips += 1,
_ => return false,
}
}
bit5_flips % 2 == 0
}
pub fn source_stagger_key(labels: &std::collections::BTreeMap<String, String>) -> String {
let mut out = String::new();
for (k, v) in labels {
if !out.is_empty() {
out.push('\u{1}');
}
out.push_str(k);
out.push('=');
out.push_str(v);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const STREAMS: [&str; 26] = [
"cpu_usage",
"cpu_perf",
"cpu_bandwidth",
"cpu_migrations",
"cpu_tlb_flush",
"cpu_frequency",
"scheduler",
"blockio_latency",
"blockio_requests",
"network_interfaces",
"network_traffic",
"syscall_counts",
"syscall_latency",
"tcp_connect_latency",
"tcp_packet_latency",
"tcp_receive",
"tcp_retransmit",
"tcp_traffic",
"memory",
"page_cache",
"gpu_nvidia",
"softirq",
"weather",
"cgroup_cpu",
"cgroup_memory",
"filesystem",
];
#[test]
fn alignment_cuts_on_the_bucket_a_row_belongs_to() {
let policy = SealPolicy {
max_bytes: usize::MAX,
max_rows: usize::MAX,
max_age: Duration::from_secs(3600),
align: Some(100),
};
let mut a = SegmentAccount::open_first("s", SINGLE_SOURCE_KEY, &policy);
assert!(!a.starts_new_bucket(250));
a.add_row(1, 250);
assert!(!a.starts_new_bucket(299), "same bucket");
a.add_row(1, 299);
assert!(!a.is_due(Instant::now()), "one bucket, no cap reached");
assert!(a.starts_new_bucket(300), "300 opens the next bucket");
a.rotate(&policy, Instant::now());
a.add_row(1, 300);
assert!(!a.is_due(Instant::now()));
let mut b = SegmentAccount::open_first("s", SINGLE_SOURCE_KEY, &policy);
b.add_row(1, 299);
b.add_row(1, 300);
assert!(b.is_due(Instant::now()), "two buckets in one segment");
}
#[test]
fn alignment_buckets_negative_timestamps_correctly() {
let policy = SealPolicy {
max_bytes: usize::MAX,
max_rows: usize::MAX,
max_age: Duration::from_secs(3600),
align: Some(100),
};
let mut a = SegmentAccount::open_first("s", SINGLE_SOURCE_KEY, &policy);
a.add_row(1, -1);
assert!(a.starts_new_bucket(1), "-1 and 1 are not the same bucket");
assert!(!a.starts_new_bucket(-100), "-100..-1 is one bucket");
}
#[test]
fn without_alignment_timestamps_do_not_affect_sealing() {
let policy = SealPolicy {
max_bytes: usize::MAX,
max_rows: 4,
max_age: Duration::from_secs(3600),
align: None,
};
let mut a = SegmentAccount::open_first("s", SINGLE_SOURCE_KEY, &policy);
for ts in [0, 1_000_000, -5, 7] {
assert!(!a.starts_new_bucket(ts));
a.add_row(1, ts);
}
assert!(a.is_due(Instant::now()), "the row cap still applies");
}
#[test]
fn staggers_identically_agrees_with_the_hash() {
let cases = [
("arm=valkey", "arm=VALKEY"),
("arm=Ab", "arm=aB"),
("host=Web-01", "host=weB-01"),
("arm=redis", "arm=REDIS"),
("arm=ab", "arm=aB"),
("host=web-01", "host=webm01"),
("host=web-01", "host=web-02"),
("arm=a", "arm=ab"),
("arm=a", "arm=a"),
];
for (a, b) in cases {
let predicted = staggers_identically(a, b);
let shares_all = STREAMS
.iter()
.all(|s| stagger_bucket(s, a) == stagger_bucket(s, b));
assert_eq!(
predicted, shares_all,
"staggers_identically({a:?}, {b:?}) said {predicted}, but the hash \
shares-all is {shares_all}"
);
}
}
#[test]
fn the_stagger_spreads_a_real_stream_set_better_than_random() {
fn clumping(key: &str, n: usize) -> f64 {
let mut counts = [0u32; STAGGER_BUCKETS as usize];
for s in &STREAMS[..n] {
counts[stagger_bucket(s, key) as usize] += 1;
}
let pairs: f64 = counts
.iter()
.map(|&c| f64::from(c) * (f64::from(c) - 1.0) / 2.0)
.sum();
let expected = (n as f64) * (n as f64 - 1.0) / 2.0 / STAGGER_BUCKETS as f64;
pairs / expected
}
for i in 0..200 {
let key = format!("host=web-{i:03}\u{1}source=weather");
assert_eq!(
clumping(&key, 12),
0.0,
"12 streams must each get their own bucket ({key})"
);
assert!(
clumping(&key, 26) < 1.0,
"26 streams must clump less than random ({key})"
);
}
}
#[test]
fn stagger_is_deterministic() {
assert_eq!(stagger_bucket("cpu_usage", SINGLE_SOURCE_KEY), 32);
assert_eq!(stagger_bucket("scheduler", SINGLE_SOURCE_KEY), 19);
assert!(
(0..STAGGER_BUCKETS).contains(&stagger_bucket("anything_at_all", SINGLE_SOURCE_KEY))
);
}
#[test]
fn hosts_that_alias_in_the_low_bits_still_desync() {
let key = |host: &str| {
source_stagger_key(
&[
("host".to_string(), host.to_string()),
("source".to_string(), "weather".to_string()),
]
.into_iter()
.collect(),
)
};
let streams = [
"cpu_usage",
"scheduler",
"blockio_latency",
"tcp_traffic",
"syscall_latency",
"cpu_bandwidth",
];
for (a, b) in [
("web-01", "webm01"),
("node1", "nodeq"),
("web.01", "webn01"),
] {
let (ka, kb) = (key(a), key(b));
let collisions = streams
.iter()
.filter(|s| stagger_bucket(s, &ka) == stagger_bucket(s, &kb))
.count();
assert_eq!(
collisions,
0,
"{a} and {b} share {collisions} of {} buckets — the reduction is \
discarding the bits that separate them. `collisions < len` would be \
too weak a bar here: 5 of 6 coincident is still the lockstep this \
test exists to catch",
streams.len()
);
}
}
#[test]
fn two_sources_do_not_share_a_streams_bucket() {
let a = source_stagger_key(
&[("host".to_string(), "alpha".to_string())]
.into_iter()
.collect(),
);
let b = source_stagger_key(
&[("host".to_string(), "beta".to_string())]
.into_iter()
.collect(),
);
let shared = ["cpu_usage", "scheduler", "blockio_latency", "tcp_traffic"];
let collisions = shared
.iter()
.filter(|s| stagger_bucket(s, &a) == stagger_bucket(s, &b))
.count();
assert_eq!(
collisions, 0,
"identical stream sets must not draw identical buckets across sources"
);
}
#[test]
fn the_byte_cap_is_staggered_across_sources() {
let policy = SealPolicy {
max_bytes: 8 * 1024 * 1024,
max_rows: 900,
max_age: Duration::from_secs(300),
align: None,
};
let key = |host: &str| {
source_stagger_key(
&[("host".to_string(), host.to_string())]
.into_iter()
.collect(),
)
};
let mut a = SegmentAccount::open_first("cpu_usage", &key("alpha"), &policy);
let mut b = SegmentAccount::open_first("cpu_usage", &key("beta"), &policy);
let now = Instant::now();
let mut split = None;
for row in 1..=policy.max_rows {
a.add_row(64 * 1024, 0);
b.add_row(64 * 1024, 0);
if a.is_due(now) != b.is_due(now) {
split = Some(row);
break;
}
assert!(
row < policy.max_rows,
"the byte cap must fire before the row cap, or this tests the wrong cap"
);
}
assert!(
split.is_some(),
"both sources' byte-bound tables sealed on the same row — the byte \
cap escaped the stagger"
);
for host in ["alpha", "beta", "gamma"] {
let acct = SegmentAccount::open_first("cpu_usage", &key(host), &policy);
assert!(acct.byte_target() > policy.max_bytes / 2 - 1);
assert!(acct.byte_target() <= policy.max_bytes);
}
}
#[test]
fn same_host_different_arms_still_desync() {
let base = |arm: &str| {
source_stagger_key(
&[
("host".to_string(), "alpha".to_string()),
("arm".to_string(), arm.to_string()),
]
.into_iter()
.collect(),
)
};
let (a, b) = (base("redis"), base("valkey"));
assert_ne!(a, b, "the arm label must reach the key");
assert_ne!(
stagger_bucket("cpu_usage", &a),
stagger_bucket("cpu_usage", &b)
);
}
#[test]
fn the_two_arms_of_an_ab_land_in_different_buckets() {
let key = |arm: &str| {
source_stagger_key(
&[
("host".to_string(), "alpha".to_string()),
("arm".to_string(), arm.to_string()),
]
.into_iter()
.collect(),
)
};
assert_ne!(
stagger_bucket("cpu_usage", &key("redis")),
stagger_bucket("cpu_usage", &key("valkey"))
);
}
}