use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use chronix_core::SeriesKey;
use crate::index::bloom::SeriesBloomFilter;
use crate::index::error::IndexError;
pub const EXTENSION: &str = "series";
const MAGIC: &[u8; 4] = b"CXSI";
const VERSION: u8 = 1;
const BLOOM_FPR: f64 = 0.01;
#[derive(Serialize, Deserialize)]
struct Entry {
measurement: String,
tags: Vec<(String, String)>,
}
#[derive(Serialize, Deserialize)]
struct Record {
stamp: SegmentStamp,
entries: Vec<Entry>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SegmentStamp {
pub created_at: i64,
pub row_count: u64,
pub series_count: u32,
}
impl SegmentStamp {
#[must_use]
pub fn of(header: &crate::segment::header::SegmentHeader) -> Self {
Self {
created_at: header.created_at,
row_count: header.row_count,
series_count: header.series_count,
}
}
pub fn read(segment_path: &Path) -> Result<Self, IndexError> {
use std::io::Read;
let mut buf = [0u8; crate::segment::header::HEADER_SIZE];
let mut file = std::fs::File::open(segment_path)?;
file.read_exact(&mut buf)?;
let header = crate::segment::header::SegmentHeader::from_bytes(&buf).map_err(|e| {
IndexError::Corrupt {
detail: format!("segment header: {e}"),
}
})?;
Ok(Self::of(&header))
}
}
#[must_use]
pub fn sidecar_path(segment_path: &Path) -> PathBuf {
segment_path.with_extension(EXTENSION)
}
pub fn write(
segment_path: &Path,
keys: &[SeriesKey],
stamp: SegmentStamp,
) -> Result<(), IndexError> {
let entries: Vec<Entry> = keys
.iter()
.map(|k| Entry {
measurement: k.measurement().to_string(),
tags: k
.tags()
.iter()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect(),
})
.collect();
let raw = postcard::to_stdvec(&Record { stamp, entries })
.map_err(|e| IndexError::BinarySerialization(e.to_string()))?;
let body = lz4_flex::compress_prepend_size(&raw);
let mut buf = Vec::with_capacity(9 + body.len());
buf.extend_from_slice(MAGIC);
buf.push(VERSION);
buf.extend_from_slice(&crc32c::crc32c(&body).to_le_bytes());
buf.extend_from_slice(&body);
let path = sidecar_path(segment_path);
let tmp = path.with_extension("series.tmp");
{
let mut file = std::fs::File::create(&tmp)?;
file.write_all(&buf)?;
file.sync_all()?;
}
std::fs::rename(&tmp, &path)?;
Ok(())
}
pub fn read(segment_path: &Path, expected: SegmentStamp) -> Result<Vec<SeriesKey>, IndexError> {
let data = std::fs::read(sidecar_path(segment_path))?;
if data.len() < 9 || &data[..4] != MAGIC {
return Err(IndexError::Corrupt {
detail: "series index: bad magic".into(),
});
}
if data[4] != VERSION {
return Err(IndexError::Corrupt {
detail: format!("series index: unsupported version {}", data[4]),
});
}
let crc = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
let body = &data[9..];
if crc32c::crc32c(body) != crc {
return Err(IndexError::Corrupt {
detail: "series index: checksum mismatch".into(),
});
}
let raw = lz4_flex::decompress_size_prepended(body).map_err(|e| IndexError::Corrupt {
detail: format!("series index: {e}"),
})?;
let record: Record =
postcard::from_bytes(&raw).map_err(|e| IndexError::BinarySerialization(e.to_string()))?;
if record.stamp != expected {
return Err(IndexError::Corrupt {
detail: format!(
"series index: stamped for another segment ({:?}, expected {expected:?})",
record.stamp
),
});
}
record
.entries
.into_iter()
.map(|e| {
SeriesKey::new(
e.measurement,
e.tags.into_iter().collect::<BTreeMap<_, _>>(),
)
.map_err(|err| IndexError::Corrupt {
detail: format!("series index: invalid key: {err}"),
})
})
.collect()
}
pub fn remove_series(
segment_path: &Path,
gone: &std::collections::HashSet<&str>,
) -> Result<bool, IndexError> {
let stamp = SegmentStamp::read(segment_path)?;
let keys = read(segment_path, stamp)?;
let kept: Vec<SeriesKey> = keys
.iter()
.filter(|k| !gone.contains(k.canonical_form()))
.cloned()
.collect();
if kept.len() == keys.len() {
return Ok(false);
}
write(segment_path, &kept, stamp)?;
Ok(true)
}
pub fn remove(segment_path: &Path) -> std::io::Result<()> {
match std::fs::remove_file(sidecar_path(segment_path)) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
#[must_use]
pub fn bloom(keys: &[SeriesKey]) -> Option<SeriesBloomFilter> {
if keys.is_empty() {
return None;
}
let mut bloom = SeriesBloomFilter::new(keys.len(), BLOOM_FPR);
for key in keys {
bloom.insert(key);
}
Some(bloom)
}
#[must_use]
pub fn tag_pairs(keys: &[SeriesKey]) -> Vec<(&str, &str)> {
let mut pairs: Vec<(&str, &str)> = keys
.iter()
.flat_map(|k| k.tags().iter().map(|(a, b)| (a.as_ref(), b.as_ref())))
.collect();
pairs.sort_unstable();
pairs.dedup();
pairs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sidecar_is_bound_to_its_segment_and_can_forget_a_series() {
let dir = tempfile::tempdir().unwrap();
let seg = dir.path().join("s1.csx");
let keys = vec![key("cpu", &[("host", "a")]), key("cpu", &[("host", "b")])];
let stamp = SegmentStamp {
created_at: 10,
row_count: 2,
series_count: 2,
};
write(&seg, &keys, stamp).unwrap();
let other = SegmentStamp {
created_at: 11,
..stamp
};
assert!(
matches!(read(&seg, other), Err(IndexError::Corrupt { .. })),
"a sidecar for another segment must not be trusted"
);
let header = crate::segment::header::SegmentHeader {
version: crate::segment::header::VERSION,
flags: 0,
created_at: 10,
min_timestamp: 0,
max_timestamp: 0,
row_count: 2,
column_count: 2,
series_count: 2,
compression: 2,
sort_order: 1,
};
std::fs::write(&seg, header.to_bytes()).unwrap();
assert_eq!(SegmentStamp::read(&seg).unwrap(), stamp);
let gone: std::collections::HashSet<&str> = [keys[0].canonical_form()].into();
assert!(remove_series(&seg, &gone).unwrap());
let back = read(&seg, stamp).unwrap();
assert_eq!(back.len(), 1);
assert_eq!(back[0].tag("host"), Some("b"));
assert!(!remove_series(&seg, &gone).unwrap(), "already gone");
}
fn key(m: &str, tags: &[(&str, &str)]) -> SeriesKey {
SeriesKey::new(
m,
tags.iter()
.map(|(a, b)| ((*a).to_string(), (*b).to_string()))
.collect(),
)
.unwrap()
}
#[test]
fn round_trips_keys_and_rejects_corruption() {
let dir = tempfile::tempdir().unwrap();
let seg = dir.path().join("s1.csx");
let keys = vec![
key("cpu", &[("host", "a"), ("region", "eu")]),
key("cpu", &[("host", "b"), ("region", "eu")]),
key("mem", &[]),
];
let stamp = SegmentStamp {
created_at: 1,
row_count: 3,
series_count: 3,
};
write(&seg, &keys, stamp).unwrap();
let back = read(&seg, stamp).unwrap();
assert_eq!(back.len(), 3);
for (a, b) in keys.iter().zip(&back) {
assert_eq!(a.canonical_form(), b.canonical_form());
}
assert_eq!(
tag_pairs(&back),
vec![("host", "a"), ("host", "b"), ("region", "eu")]
);
let bloom = bloom(&back).unwrap();
assert!(bloom.may_contain(&keys[0]));
assert!(!bloom.may_contain(&key("cpu", &[("host", "zzz")])));
let mut bytes = std::fs::read(sidecar_path(&seg)).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 0xFF;
std::fs::write(sidecar_path(&seg), bytes).unwrap();
assert!(matches!(read(&seg, stamp), Err(IndexError::Corrupt { .. })));
remove(&seg).unwrap();
remove(&seg).unwrap();
assert!(matches!(read(&seg, stamp), Err(IndexError::Io(_))));
}
}