use anyhow::{Context, Result};
use backbeat::{
record::RecordView,
ring::walk,
wire::{DumpReader, OwnedSchema},
};
use bytes::Bytes;
use rayon::prelude::*;
use std::{
collections::{HashMap, HashSet},
fs,
path::{Path, PathBuf},
};
pub struct Rec {
pub ts_nanos: u64,
pub shard_id: u32,
pub local_seq: u64,
pub schema_idx: usize,
pub fields: Bytes,
}
pub struct Loaded {
pub path: PathBuf,
pub instance_id: u64,
pub host: String,
pub schemas: Vec<OwnedSchema>,
pub views: Vec<String>,
pub intern: HashMap<u32, String>,
pub records: Vec<Rec>,
}
pub type RecordKey<'a> = (u64, u64, u32, u64, &'a [u8]);
pub fn record_key<'a>(d: &Loaded, r: &'a Rec) -> RecordKey<'a> {
(
r.ts_nanos,
d.instance_id,
r.shard_id,
d.schemas[r.schema_idx].id.get(),
&r.fields,
)
}
pub fn load(path: &Path, bytes: Bytes) -> Result<Vec<Loaded>> {
let reader = DumpReader::new(bytes).map_err(|e| anyhow::anyhow!("{e}"))?;
let mut schemas = reader.schemas().map_err(|e| anyhow::anyhow!("{e}"))?;
let intern_tables = reader.intern_tables().map_err(|e| anyhow::anyhow!("{e}"))?;
let metas = reader.metas().map_err(|e| anyhow::anyhow!("{e}"))?;
let views = reader.views().map_err(|e| anyhow::anyhow!("{e}"))?;
let shards = reader.shards().map_err(|e| anyhow::anyhow!("{e}"))?;
schemas.sort_by(|a, b| a.qualified_name.cmp(&b.qualified_name));
let by_id: HashMap<u64, usize> = schemas
.iter()
.enumerate()
.map(|(i, s)| (s.id.get(), i))
.collect();
let mut intern_by_instance: HashMap<u64, HashMap<u32, String>> = HashMap::new();
for table in intern_tables {
let map = intern_by_instance.entry(table.instance_id).or_default();
for (id, bytes) in table.entries {
map.insert(id, String::from_utf8_lossy(&bytes).into_owned());
}
}
let walked: Vec<(u64, Vec<Rec>)> = shards
.par_iter()
.map(|shard| {
let mut shard_recs: Vec<Rec> = Vec::new();
let mut seq = u64::MAX;
walk(
&shard.region,
shard.head as usize,
shard.capacity as usize,
|payload| {
let Some(rec) = RecordView::parse(&payload[..]) else {
return false;
};
match by_id.get(&rec.event_id.get()) {
Some(&idx) if rec.fields.len() == schemas[idx].record_size as usize => {
let fields_off = payload.len() - rec.fields.len();
shard_recs.push(Rec {
ts_nanos: rec.ts_nanos,
shard_id: shard.shard_id,
local_seq: seq,
schema_idx: idx,
fields: payload.slice(fields_off..),
});
seq -= 1;
true
}
_ => false,
}
},
);
(shard.instance_id, shard_recs)
})
.collect();
let mut records_by_instance: HashMap<u64, Vec<Rec>> = HashMap::new();
for (instance_id, recs) in walked {
records_by_instance
.entry(instance_id)
.or_default()
.extend(recs);
}
let mut instance_ids: Vec<u64> = Vec::new();
let mut seen = HashSet::new();
for m in &metas {
if seen.insert(m.instance_id) {
instance_ids.push(m.instance_id);
}
}
for &id in records_by_instance.keys() {
if seen.insert(id) {
instance_ids.push(id);
}
}
if instance_ids.is_empty() {
instance_ids.push(0);
}
instance_ids.sort_unstable();
let host_of: HashMap<u64, String> =
metas.into_iter().map(|m| (m.instance_id, m.host)).collect();
let loaded = instance_ids
.into_iter()
.map(|instance_id| {
let mut records = records_by_instance.remove(&instance_id).unwrap_or_default();
records.sort_by_key(|r| (r.ts_nanos, r.shard_id, r.local_seq));
Loaded {
path: path.to_path_buf(),
instance_id,
host: host_of.get(&instance_id).cloned().unwrap_or_default(),
schemas: schemas.clone(),
views: views.clone(),
intern: intern_by_instance.remove(&instance_id).unwrap_or_default(),
records,
}
})
.collect();
Ok(loaded)
}
pub fn unique_records(dumps: &[Loaded]) -> Vec<(&Loaded, &Rec)> {
let total: usize = dumps.iter().map(|d| d.records.len()).sum();
let mut out: Vec<(&Loaded, &Rec)> = Vec::with_capacity(total);
for d in dumps {
for r in &d.records {
out.push((d, r));
}
}
out.sort_by(|a, b| record_key(a.0, a.1).cmp(&record_key(b.0, b.1)));
out.dedup_by(|a, b| record_key(a.0, a.1) == record_key(b.0, b.1));
out
}
pub fn load_many(paths: &[PathBuf]) -> Result<Vec<Loaded>> {
let per_file: Vec<Vec<Loaded>> = paths
.par_iter()
.map(|p| {
let bytes = fs::read(p).with_context(|| format!("reading dump {}", p.display()))?;
load(p, bytes.into()).with_context(|| format!("decoding dump {}", p.display()))
})
.collect::<Result<_>>()?;
Ok(per_file.into_iter().flatten().collect())
}