use std::borrow::Cow;
use std::fmt;
use std::fs::File;
use std::io::{self, Write};
use std::ops::Range;
use std::path::Path;
use std::str;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
use crate::Position;
use super::SegmentIndex;
use super::header::{INDEX_HEADER_SIZE, IndexHeaderError, IndexSegmentHeader};
use super::postings::{decode_postings, encode_postings, posting_len};
use super::tail::ActiveTail;
#[derive(Clone)]
struct SharedSlice {
data: Arc<[u8]>,
range: Range<usize>,
}
impl AsRef<[u8]> for SharedSlice {
fn as_ref(&self) -> &[u8] {
&self.data[self.range.clone()]
}
}
pub struct IndexSegment {
header: IndexSegmentHeader,
data: Arc<[u8]>,
map: fst::Map<SharedSlice>,
type_names: Vec<Box<str>>,
}
impl IndexSegment {
pub fn encode(index: &ActiveTail) -> Vec<u8> {
let created_at_nanos = u64::try_from(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time went backwards")
.as_nanos(),
)
.expect("is it really the year 2554 already?");
Self::encode_at(index, created_at_nanos)
}
pub fn encode_at(index: &ActiveTail, created_at_nanos: u64) -> Vec<u8> {
let mut type_column = Vec::with_capacity(index.len() as usize * 2);
for type_id in index.type_column() {
type_column.extend_from_slice(&type_id.to_le_bytes());
}
let mut type_dict = Vec::new();
for name in index.type_names() {
type_dict.extend_from_slice(&fit_u16(name.len(), "type name length").to_le_bytes());
type_dict.extend_from_slice(name.as_bytes());
}
let mut postings_region = Vec::new();
let mut builder = fst::MapBuilder::memory();
for (tag, postings) in index.terms_sorted_with_postings() {
let value = encode_postings(&postings, &mut postings_region);
builder
.insert(tag.as_bytes(), value)
.expect("terms are fed to the FST in sorted key order");
}
let fst_bytes = builder
.into_inner()
.expect("in-memory FST build cannot fail on I/O");
let typedict_off = fit_u32(INDEX_HEADER_SIZE + type_column.len(), "typedict offset");
let postings_off = fit_u32(typedict_off as usize + type_dict.len(), "postings offset");
let fst_off = fit_u32(postings_off as usize + postings_region.len(), "fst offset");
let fst_len = fit_u32(fst_bytes.len(), "fst length");
let mut out = Vec::with_capacity(fst_off as usize + fst_len as usize);
out.extend_from_slice(&[0u8; INDEX_HEADER_SIZE]);
out.extend_from_slice(&type_column);
out.extend_from_slice(&type_dict);
out.extend_from_slice(&postings_region);
out.extend_from_slice(&fst_bytes);
let body_crc = crc32fast::hash(&out[INDEX_HEADER_SIZE..]);
let header = IndexSegmentHeader {
version: IndexSegmentHeader::VERSION,
created_at_nanos,
base_position: index.base(),
event_count: index.len() as u64,
typedict_off,
postings_off,
fst_off,
fst_len,
body_crc,
};
out[..INDEX_HEADER_SIZE].copy_from_slice(&header.to_bytes());
out
}
pub fn from_bytes(data: Arc<[u8]>) -> Result<Self, IndexSegmentError> {
if data.len() < INDEX_HEADER_SIZE {
return Err(IndexSegmentError::TooShort { len: data.len() });
}
let header_bytes: &[u8; INDEX_HEADER_SIZE] = data[..INDEX_HEADER_SIZE].try_into().unwrap();
let header = IndexSegmentHeader::from_bytes(header_bytes)?;
if header.segment_len() != data.len() {
return Err(IndexSegmentError::LengthMismatch {
header: header.segment_len(),
actual: data.len(),
});
}
let computed = crc32fast::hash(&data[INDEX_HEADER_SIZE..]);
if computed != header.body_crc {
return Err(IndexSegmentError::BodyChecksumMismatch {
expected: header.body_crc,
computed,
});
}
let type_names = parse_type_dict(&data[header.type_dict_range()])?;
let fst_region = SharedSlice {
data: Arc::clone(&data),
range: header.fst_range(),
};
let map = fst::Map::new(fst_region).map_err(|source| IndexSegmentError::Fst {
detail: source.to_string(),
})?;
Ok(IndexSegment {
header,
data,
map,
type_names,
})
}
pub fn header(&self) -> &IndexSegmentHeader {
&self.header
}
fn postings_region(&self) -> &[u8] {
&self.data[self.header.postings_range()]
}
}
pub fn write_segment_file(path: &Path, bytes: &[u8]) -> io::Result<()> {
let mut file = File::create(path)?;
seglog::crash_io!("index_flush");
file.write_all(bytes)?;
seglog::crash_point!("index_after_write");
file.sync_all()?;
seglog::crash_point!("index_after_sync");
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
File::open(parent)?.sync_all()?;
}
Ok(())
}
impl SegmentIndex for IndexSegment {
fn base(&self) -> Position {
self.header.base_position
}
fn len(&self) -> u32 {
self.header.event_count as u32
}
fn term_postings(&self, tag: &str) -> Option<Cow<'_, [u32]>> {
let value = self.map.get(tag.as_bytes())?;
let postings = decode_postings(value, self.postings_region())
.expect("postings decode on a CRC-validated index segment");
Some(postings)
}
fn term_len(&self, tag: &str) -> Option<u32> {
let value = self.map.get(tag.as_bytes())?;
let len = posting_len(value, self.postings_region())
.expect("posting length on a CRC-validated index segment");
Some(len)
}
fn type_id(&self, name: &str) -> Option<u16> {
self.type_names
.iter()
.position(|n| n.as_ref() == name)
.map(|i| i as u16)
}
fn type_at(&self, local: u32) -> u16 {
let start = self.header.type_column_range().start + local as usize * 2;
u16::from_le_bytes(self.data[start..start + 2].try_into().unwrap())
}
}
impl fmt::Debug for IndexSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("IndexSegment")
.field("base_position", &self.header.base_position)
.field("event_count", &self.header.event_count)
.field("types", &self.type_names.len())
.finish_non_exhaustive()
}
}
fn parse_type_dict(mut buf: &[u8]) -> Result<Vec<Box<str>>, IndexSegmentError> {
let mut names = Vec::new();
while !buf.is_empty() {
let len = take_u16(&mut buf)? as usize;
if buf.len() < len {
return Err(IndexSegmentError::MalformedTypeDict {
detail: "type name runs past the dictionary",
});
}
let (name, rest) = buf.split_at(len);
let name = str::from_utf8(name).map_err(|_| IndexSegmentError::MalformedTypeDict {
detail: "type name is not valid UTF-8",
})?;
names.push(Box::from(name));
buf = rest;
}
Ok(names)
}
fn take_u16(buf: &mut &[u8]) -> Result<u16, IndexSegmentError> {
if buf.len() < 2 {
return Err(IndexSegmentError::MalformedTypeDict {
detail: "truncated length field",
});
}
let (head, rest) = buf.split_at(2);
*buf = rest;
Ok(u16::from_le_bytes(head.try_into().unwrap()))
}
fn fit_u16(value: usize, what: &str) -> u16 {
u16::try_from(value).unwrap_or_else(|_| panic!("{what} exceeds u16 in one index segment"))
}
fn fit_u32(value: usize, what: &str) -> u32 {
u32::try_from(value).unwrap_or_else(|_| {
panic!("{what} exceeds u32; segment_size <= u32::MAX guarantees it does not")
})
}
#[derive(Debug, Error)]
pub enum IndexSegmentError {
#[error("index segment is shorter ({len} bytes) than the header")]
TooShort { len: usize },
#[error(transparent)]
Header(#[from] IndexHeaderError),
#[error("index segment length mismatch: header says {header} bytes, file is {actual}")]
LengthMismatch { header: usize, actual: usize },
#[error(
"index segment body checksum mismatch: expected {expected:#010x}, computed {computed:#010x}"
)]
BodyChecksumMismatch { expected: u32, computed: u32 },
#[error("malformed type dictionary: {detail}")]
MalformedTypeDict { detail: &'static str },
#[error("fst term dictionary is invalid: {detail}")]
Fst { detail: String },
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{Event, EventType, Tag, Tags};
use crate::index::search;
use crate::query::{Query, QueryItem};
use smallvec::SmallVec;
fn tags(items: &[&str]) -> Tags {
Tags::new(
items
.iter()
.map(|s| Tag::new(*s).unwrap())
.collect::<SmallVec<[Tag; 4]>>(),
)
.unwrap()
}
fn event(ty: &str, tag_strs: &[&str]) -> Event {
Event::new(&EventType::new(ty).unwrap(), &tags(tag_strs), b"payload").unwrap()
}
fn fixture() -> ActiveTail {
let events = [
event("Registered", &[]),
event("Enrolled", &["course:c1"]),
event("Enrolled", &["course:c1", "student:s1"]),
event("Renamed", &["student:s1"]),
event("Registered", &["course:c1"]),
];
let index = ActiveTail::new(Position::new(1));
for (i, ev) in events.iter().enumerate() {
index
.push(Position::new(1 + i as u64), ev.as_ref())
.unwrap();
}
index
}
fn sealed(index: &ActiveTail) -> IndexSegment {
let bytes = IndexSegment::encode(index);
IndexSegment::from_bytes(Arc::from(bytes)).unwrap()
}
#[test]
fn round_trips_through_bytes() {
let seg = sealed(&fixture());
assert_eq!(seg.base(), Position::new(1));
assert_eq!(seg.len(), 5);
assert_eq!(seg.header().max_position(), Some(Position::new(5)));
}
#[test]
fn segment_answers_queries_identically_to_the_tail_index() {
let tail = fixture();
let seg = sealed(&tail);
let queries = [
Query::all(),
Query::items(Vec::new()),
Query::item(QueryItem::with_tags(tags(&["course:c1"]))),
Query::item(QueryItem::with_tags(tags(&["course:c1", "student:s1"]))),
Query::item(QueryItem::of_types(vec![
EventType::new("Registered").unwrap(),
])),
Query::item(QueryItem::new(
vec![EventType::new("Enrolled").unwrap()],
tags(&["course:c1"]),
)),
Query::items(vec![
QueryItem::of_types(vec![EventType::new("Renamed").unwrap()]),
QueryItem::with_tags(tags(&["course:c1"])),
]),
Query::item(QueryItem::with_tags(tags(&["ghost:x"]))),
];
for query in &queries {
for after in 0..=5 {
let from_tail: Vec<Position> =
search(&tail.view_full(), query, Position::new(after)).collect();
let from_seg: Vec<Position> = search(&seg, query, Position::new(after)).collect();
assert_eq!(from_tail, from_seg, "query {query:?} after {after}");
}
}
}
#[test]
fn singleton_and_multi_postings_both_survive() {
let index = ActiveTail::new(Position::new(1));
index
.push(Position::new(1), event("E", &["only:once"]).as_ref())
.unwrap();
index
.push(Position::new(2), event("E", &["twice:here"]).as_ref())
.unwrap();
index
.push(Position::new(3), event("E", &["twice:here"]).as_ref())
.unwrap();
let seg = sealed(&index);
assert_eq!(
seg.term_postings("only:once").unwrap().into_owned(),
vec![0]
);
assert_eq!(
seg.term_postings("twice:here").unwrap().into_owned(),
vec![1, 2]
);
assert!(seg.term_postings("absent").is_none());
}
#[test]
fn term_len_is_exact_and_matches_the_postings() {
let index = ActiveTail::new(Position::new(1));
index
.push(Position::new(1), event("E", &["only:once"]).as_ref())
.unwrap();
index
.push(Position::new(2), event("E", &["twice:here"]).as_ref())
.unwrap();
index
.push(Position::new(3), event("E", &["twice:here"]).as_ref())
.unwrap();
let seg = sealed(&index);
assert_eq!(seg.term_len("only:once"), Some(1));
assert_eq!(seg.term_len("twice:here"), Some(2));
assert_eq!(seg.term_len("absent"), None);
assert_eq!(
seg.term_len("twice:here").unwrap() as usize,
seg.term_postings("twice:here").unwrap().len()
);
}
#[test]
fn body_checksum_mismatch_is_detected() {
let mut bytes = IndexSegment::encode(&fixture());
let last = bytes.len() - 1;
bytes[last] ^= 0xFF;
assert!(matches!(
IndexSegment::from_bytes(Arc::from(bytes)),
Err(IndexSegmentError::BodyChecksumMismatch { .. })
));
}
#[test]
fn truncated_segment_is_a_length_mismatch() {
let mut bytes = IndexSegment::encode(&fixture());
bytes.truncate(bytes.len() - 1);
assert!(matches!(
IndexSegment::from_bytes(Arc::from(bytes)),
Err(IndexSegmentError::LengthMismatch { .. })
));
}
#[test]
fn empty_tail_index_round_trips() {
let index = ActiveTail::new(Position::new(1));
let seg = sealed(&index);
assert_eq!(seg.len(), 0);
assert_eq!(seg.header().max_position(), None);
assert!(seg.term_postings("anything").is_none());
}
#[test]
fn max_distinct_types_round_trips_without_panic() {
let n = u16::MAX as u64 + 1; let index = ActiveTail::new(Position::new(1));
for i in 0..n {
let ev = event(&format!("T{i}"), &[]);
index.push(Position::new(1 + i), ev.as_ref()).unwrap();
}
let seg = sealed(&index);
assert_eq!(seg.len(), n as u32);
let last_ty = format!("T{}", n - 1);
let id = seg.type_id(&last_ty).unwrap();
assert_eq!(id, u16::MAX);
assert_eq!(seg.type_at((n - 1) as u32), id);
let q = Query::item(QueryItem::of_types(vec![EventType::new(last_ty).unwrap()]));
let got: Vec<Position> = search(&seg, &q, Position::ZERO).collect();
assert_eq!(got, vec![Position::new(n)]);
}
}