#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use chronix::prelude::*;
use chronix::{Chronix, fields, tags};
const SEC: i64 = 1_000_000_000;
const HOUR: i64 = 3_600 * SEC;
const DAY: i64 = 24 * HOUR;
fn now_ns() -> i64 {
i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos(),
)
.unwrap()
}
fn open(dir: &tempfile::TempDir, shard: Duration, retention: Option<Duration>) -> Arc<Chronix> {
Arc::new(
Chronix::open(
ChronixConfig::builder()
.data_dir(dir.path())
.shard_duration(shard)
.retention(retention)
.maintenance_interval(Duration::from_secs(86_400 * 365))
.build()
.unwrap(),
)
.unwrap(),
)
}
#[test]
fn a_scan_in_flight_outlives_a_retention_pass() {
let dir = tempfile::tempdir().unwrap();
let db = open(
&dir,
Duration::from_secs(3600),
Some(Duration::from_secs(2 * 86_400)),
);
let now = now_ns();
let base = now - 5 * DAY;
let key = SeriesKey::new("cpu", tags! { "host" => "h1" }).unwrap();
for h in 0..10i64 {
let pts: Vec<Point> = (0..60)
.map(|m| {
Point::new(
key.clone(),
fields! { "v" => 1.0 },
base + h * HOUR + m * 60 * SEC,
)
.unwrap()
})
.collect();
assert!(db.insert_batch(&pts).unwrap().is_complete());
db.flush().unwrap();
}
let fresh = SeriesKey::new("cpu", tags! { "host" => "h2" }).unwrap();
assert!(
db.insert_batch(&[Point::new(fresh, fields! { "v" => 1.0 }, now).unwrap()])
.unwrap()
.is_complete()
);
db.flush().unwrap();
let plan = db
.query()
.measurement("cpu")
.range(0, i64::MAX)
.build()
.unwrap();
let mut stream = db.execute_iter(&plan).unwrap();
let mut rows = stream.next().unwrap().unwrap().num_rows();
let result = db
.enforce_retention(Duration::from_secs(2 * 86_400))
.unwrap();
assert!(
result.segments_deleted > 0,
"the pass has to remove something for this test to mean anything"
);
assert!(
result.segments_awaiting_readers > 0,
"the scan holds those segments, so their files cannot have gone yet"
);
assert_eq!(
result.bytes_freed, 0,
"nothing was unlinked, so no bytes were freed — a pass that says \
otherwise is the one an operator reads when the disk will not shrink"
);
for batch in stream {
rows += batch.unwrap().num_rows();
}
assert_eq!(
rows, 601,
"the scan must be answered from the set it was given"
);
assert!(db.gc().unwrap() > 0);
let after = db.execute(&plan).unwrap().num_rows();
assert_eq!(after, 1, "a new query sees only what retention left");
}
#[test]
fn a_tag_filtered_read_stays_complete_across_a_compaction() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir, Duration::from_secs(86_400), None);
let key = SeriesKey::new("cpu", tags! { "host" => "h1", "region" => "eu" }).unwrap();
let base = 1_700_000_000 * SEC;
const SEGMENTS: i64 = 16;
const PER_SEGMENT: i64 = 50;
let expected = usize::try_from(SEGMENTS * PER_SEGMENT).unwrap();
for seg in 0..SEGMENTS {
let pts: Vec<Point> = (0..PER_SEGMENT)
.map(|i| {
let n = seg * PER_SEGMENT + i;
Point::new(key.clone(), fields! { "v" => n as f64 }, base + n * SEC).unwrap()
})
.collect();
assert!(db.insert_batch(&pts).unwrap().is_complete());
db.flush().unwrap();
}
let stop = Arc::new(AtomicBool::new(false));
let bad = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let readers: Vec<_> = (0..3)
.map(|_| {
let db = Arc::clone(&db);
let stop = Arc::clone(&stop);
let bad = Arc::clone(&bad);
std::thread::spawn(move || {
let plan = db
.query()
.measurement("cpu")
.tag("host", "h1")
.range(0, i64::MAX)
.build()
.unwrap();
let mut issued = 0u64;
while !stop.load(Ordering::Relaxed) {
issued += 1;
match db.execute(&plan) {
Ok(b) if b.num_rows() == expected => {}
Ok(b) => bad
.lock()
.unwrap()
.push(format!("{} rows, expected {expected}", b.num_rows())),
Err(e) => bad.lock().unwrap().push(format!("failed: {e}")),
}
}
issued
})
})
.collect();
for _ in 0..40 {
let _ = db.compact().unwrap();
for seg in 0..4i64 {
let pts: Vec<Point> = (0..10)
.map(|i| {
let n = seg * PER_SEGMENT + i;
Point::new(key.clone(), fields! { "v" => n as f64 }, base + n * SEC).unwrap()
})
.collect();
assert!(db.insert_batch(&pts).unwrap().is_complete());
db.flush().unwrap();
}
}
stop.store(true, Ordering::Relaxed);
let issued: u64 = readers.into_iter().map(|h| h.join().unwrap()).sum();
let bad = bad.lock().unwrap();
assert!(
bad.is_empty(),
"{} of {issued} concurrent reads were wrong; first three: {:?}",
bad.len(),
&bad[..bad.len().min(3)]
);
assert!(
issued > 100,
"only {issued} reads — the race had no room to show"
);
}
#[test]
fn a_scan_outlives_a_drop_of_its_own_measurement() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir, Duration::from_secs(3600), None);
let key = SeriesKey::new("cpu", tags! { "host" => "h1" }).unwrap();
let base = 1_700_000_000 * SEC;
for seg in 0..6i64 {
let pts: Vec<Point> = (0..20)
.map(|i| {
Point::new(
key.clone(),
fields! { "v" => 1.0 },
base + seg * HOUR + i * SEC,
)
.unwrap()
})
.collect();
assert!(db.insert_batch(&pts).unwrap().is_complete());
db.flush().unwrap();
}
let plan = db
.query()
.measurement("cpu")
.range(0, i64::MAX)
.build()
.unwrap();
let mut stream = db.execute_iter(&plan).unwrap();
let mut rows = stream.next().unwrap().unwrap().num_rows();
db.drop_measurement("cpu").unwrap();
for batch in stream {
rows += batch.unwrap().num_rows();
}
assert_eq!(
rows, 120,
"the scan keeps the answer it was already reading"
);
assert!(db.schema("cpu").is_none());
assert_eq!(db.statistics().leased_segments, 0, "the scan has finished");
assert_eq!(
db.gc().unwrap(),
6,
"every file the drop could not unlink goes on the next collection"
);
}
#[test]
fn a_finished_or_abandoned_scan_holds_nothing() {
let dir = tempfile::tempdir().unwrap();
let db = open(&dir, Duration::from_secs(3600), None);
let key = SeriesKey::new("cpu", tags! { "host" => "h1" }).unwrap();
let base = 1_700_000_000 * SEC;
for seg in 0..4i64 {
let pts: Vec<Point> = (0..20)
.map(|i| {
Point::new(
key.clone(),
fields! { "v" => 1.0 },
base + seg * HOUR + i * SEC,
)
.unwrap()
})
.collect();
assert!(db.insert_batch(&pts).unwrap().is_complete());
db.flush().unwrap();
}
assert_eq!(db.statistics().leased_segments, 0);
let plan = db
.query()
.measurement("cpu")
.range(0, i64::MAX)
.build()
.unwrap();
{
let mut stream = db.execute_iter(&plan).unwrap();
let _ = stream.next();
assert!(
db.statistics().leased_segments > 0,
"a live stream holds its snapshot"
);
}
assert_eq!(db.statistics().leased_segments, 0, "dropped mid-scan");
let _ = db.execute(&plan).unwrap();
assert_eq!(db.statistics().leased_segments, 0);
let limited = db
.query()
.measurement("cpu")
.range(0, i64::MAX)
.limit(1)
.build()
.unwrap();
let _ = db.execute(&limited).unwrap();
assert_eq!(db.statistics().leased_segments, 0);
}