use crate::model::{self, Loaded, Rec};
use anyhow::{Context, Result};
use backbeat::{
format::SectionKind,
record::{FIELDS_OFFSET, ID_OFFSET, TS_OFFSET},
ring::{LEN_SUFFIX, MAX_RECORD},
wire::{DumpReader, DumpWriter, OwnedSchema},
};
use rayon::prelude::*;
use std::{
collections::{BTreeMap, HashSet},
fs,
path::{Path, PathBuf},
};
pub fn merge(inputs: &[PathBuf], output: &Path, dedup: bool) -> Result<usize> {
if dedup {
merge_dedup(inputs, output)
} else {
merge_splice(inputs, output)
}
}
struct Spliceable {
schemas: Vec<OwnedSchema>,
metas: Vec<Vec<u8>>,
interns: Vec<Vec<u8>>,
shards: Vec<Vec<u8>>,
views: Vec<String>,
}
fn read_spliceable(path: &Path, bytes: bytes::Bytes) -> Result<Spliceable> {
let reader = DumpReader::new(bytes)
.map_err(|e| anyhow::anyhow!("{e}"))
.with_context(|| format!("reading dump {}", path.display()))?;
let schemas = reader.schemas().map_err(|e| anyhow::anyhow!("{e}"))?;
let views = reader.views().map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(Spliceable {
schemas,
metas: reader.raw_bodies(SectionKind::Meta),
interns: reader.raw_bodies(SectionKind::Intern),
shards: reader.raw_bodies(SectionKind::Shard),
views,
})
}
fn merge_splice(inputs: &[PathBuf], output: &Path) -> Result<usize> {
let spliceables: Vec<Spliceable> = inputs
.par_iter()
.map(|p| {
let bytes = fs::read(p).with_context(|| format!("reading dump {}", p.display()))?;
read_spliceable(p, bytes.into())
})
.collect::<Result<_>>()?;
let mut writer = DumpWriter::new();
let mut seen = HashSet::new();
let mut registry: Vec<OwnedSchema> = Vec::new();
for s in &spliceables {
for schema in &s.schemas {
if seen.insert(schema.id.get()) {
registry.push(schema.clone());
}
}
}
writer.schema_registry_owned(®istry);
let mut seen_views = HashSet::new();
for s in &spliceables {
for sql in &s.views {
if seen_views.insert(sql.clone()) {
writer.views(sql);
}
}
}
for s in spliceables {
for body in s.metas {
writer.raw_section(SectionKind::Meta, body);
}
for body in s.interns {
writer.raw_section(SectionKind::Intern, body);
}
for body in s.shards {
writer.raw_section(SectionKind::Shard, body);
}
}
let bytes = writer.finish();
fs::write(output, &bytes).with_context(|| format!("writing {}", output.display()))?;
Ok(registry.len())
}
fn merge_dedup(inputs: &[PathBuf], output: &Path) -> Result<usize> {
let dumps = model::load_many(inputs)?;
let mut writer = DumpWriter::new();
let mut seen = HashSet::new();
let mut registry: Vec<OwnedSchema> = Vec::new();
for d in &dumps {
for schema in &d.schemas {
if seen.insert(schema.id.get()) {
registry.push(schema.clone());
}
}
}
writer.schema_registry_owned(®istry);
let mut seen_views = HashSet::new();
for sql in dumps.iter().flat_map(|d| &d.views) {
if seen_views.insert(sql.clone()) {
writer.views(sql);
}
}
let mut by_shard: BTreeMap<(u64, u32), Vec<(&Loaded, &Rec)>> = BTreeMap::new();
let mut hosts: BTreeMap<u64, String> = BTreeMap::new();
let mut interns: BTreeMap<u64, Vec<(u32, Vec<u8>)>> = BTreeMap::new();
let mut instance_ids: HashSet<u64> = HashSet::new();
for (d, r) in model::unique_records(&dumps) {
by_shard
.entry((d.instance_id, r.shard_id))
.or_default()
.push((d, r));
instance_ids.insert(d.instance_id);
}
for d in &dumps {
instance_ids.insert(d.instance_id);
hosts.entry(d.instance_id).or_insert_with(|| d.host.clone());
let table = interns.entry(d.instance_id).or_default();
let present: HashSet<u32> = table.iter().map(|(id, _)| *id).collect();
for (id, s) in &d.intern {
if !present.contains(id) {
table.push((*id, s.clone().into_bytes()));
}
}
}
let mut ordered: Vec<u64> = instance_ids.into_iter().collect();
ordered.sort_unstable();
for instance_id in ordered {
writer.meta(
instance_id,
hosts.get(&instance_id).map_or("", |h| h.as_str()),
);
if let Some(table) = interns.get(&instance_id) {
if !table.is_empty() {
let entries = table.iter().map(|(id, b)| (*id, b.as_slice()));
writer.intern_table(instance_id, entries);
}
}
}
for ((instance_id, shard_id), recs) in &by_shard {
let (region, head) = repack_shard(recs);
writer.shard(*instance_id, *shard_id, head, ®ion);
}
let bytes = writer.finish();
fs::write(output, &bytes).with_context(|| format!("writing {}", output.display()))?;
Ok(registry.len())
}
fn repack_shard(recs: &[(&Loaded, &Rec)]) -> (Vec<u8>, u64) {
let needed: usize = recs
.iter()
.map(|(_, r)| FIELDS_OFFSET + r.fields.len() + LEN_SUFFIX)
.sum();
let capacity = needed.max(1).next_power_of_two();
let mut region = vec![0u8; capacity];
let mut at = 0usize;
for (d, r) in recs {
let event_id = d.schemas[r.schema_idx].id.get();
at += write_record(&mut region[at..], r.ts_nanos, event_id, &r.fields);
}
(region, at as u64)
}
fn write_record(out: &mut [u8], ts_nanos: u64, event_id: u64, fields: &[u8]) -> usize {
let payload_len = FIELDS_OFFSET + fields.len();
debug_assert!(
payload_len <= MAX_RECORD,
"re-packed record exceeds MAX_RECORD"
);
out[TS_OFFSET..ID_OFFSET].copy_from_slice(&ts_nanos.to_le_bytes());
out[ID_OFFSET..FIELDS_OFFSET].copy_from_slice(&event_id.to_le_bytes());
out[FIELDS_OFFSET..payload_len].copy_from_slice(fields);
out[payload_len..payload_len + LEN_SUFFIX].copy_from_slice(&(payload_len as u16).to_le_bytes());
payload_len + LEN_SUFFIX
}