use std::path::{Path, PathBuf};
use std::sync::Arc;
use arrow_array::builder::{FixedSizeBinaryBuilder, Int64Builder, StringBuilder, UInt64Builder};
use arrow_array::{Array, FixedSizeBinaryArray, Int64Array, RecordBatch, StringArray, UInt64Array};
use arrow_ipc::reader::StreamReader;
use arrow_schema::{DataType, Field, Schema, SchemaRef};
use crate::arrow_log::{
arrow_err, check_schema, column, encode_stream, ipc_guard, ref_schema, sync_parent_dir,
valid_prefix_len, write_options, COL_ACTOR, COL_AT, COL_NEW_OID, COL_NEW_SYMREF, COL_OLD_OID,
COL_OLD_SYMREF, COL_REFNAME, COMPACTION_ACTOR, REFS_ARCHIVE_DIR, REFS_LOG,
};
use gunnar_store::{Error, Result};
pub const COL_PUSH_SEQ: &str = "push_seq";
pub const SEALED_HISTORY: &str = "refs-history.arrow";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RefTransaction {
pub push_seq: u64,
pub refname: String,
pub old_oid: Option<gix_hash::ObjectId>,
pub new_oid: Option<gix_hash::ObjectId>,
pub old_symref: Option<String>,
pub new_symref: Option<String>,
pub actor: String,
pub at_unix_ms: i64,
}
impl RefTransaction {
pub fn is_snapshot(&self) -> bool {
self.actor == COMPACTION_ACTOR && self.old_oid.is_none() && self.old_symref.is_none()
}
pub fn is_deletion(&self) -> bool {
self.new_oid.is_none() && self.new_symref.is_none()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SealReport {
pub pushes: usize,
pub rows: usize,
pub bytes: u64,
pub bytes_in: u64,
pub segments_read: usize,
}
impl SealReport {
pub fn bytes_per_push(&self) -> f64 {
if self.pushes == 0 {
return 0.0;
}
self.bytes as f64 / self.pushes as f64
}
pub fn shrink_x(&self) -> f64 {
if self.bytes == 0 {
return 0.0;
}
self.bytes_in as f64 / self.bytes as f64
}
}
#[derive(Clone, Debug)]
pub struct History {
hash_kind: gix_hash::Kind,
batches: Vec<RecordBatch>,
segments_read: usize,
live_batches_read: usize,
bytes_in: u64,
}
impl History {
pub fn read(store_dir: impl AsRef<Path>, hash_kind: gix_hash::Kind) -> Result<Self> {
Self::read_upto(store_dir, hash_kind, u64::MAX)
}
pub fn read_upto(
store_dir: impl AsRef<Path>,
hash_kind: gix_hash::Kind,
live_len: u64,
) -> Result<Self> {
let store_dir = store_dir.as_ref();
let expected = ref_schema(hash_kind);
let mut batches = Vec::new();
let mut bytes_in = 0u64;
let segments = archive_segments(&store_dir.join(REFS_ARCHIVE_DIR))?;
let segments_read = segments.len();
for segment in &segments {
bytes_in += read_stream(segment, Some(&expected), &mut batches)?;
}
let before_live = batches.len();
let live = store_dir.join(REFS_LOG);
if live.exists() {
let bytes = std::fs::read(&live)?;
bytes_in += bytes.len() as u64;
let valid = valid_prefix_len(&bytes).min(clamp_len(live_len));
decode_stream(&bytes[..valid], Some(&expected), &mut batches)?;
}
Ok(History {
hash_kind,
live_batches_read: batches.len() - before_live,
batches,
segments_read,
bytes_in,
})
}
pub fn hash_kind(&self) -> gix_hash::Kind {
self.hash_kind
}
pub fn pushes(&self) -> usize {
self.batches.len()
}
pub fn segments_read(&self) -> usize {
self.segments_read
}
pub fn live_pushes(&self) -> usize {
self.live_batches_read
}
pub fn archived_pushes(&self) -> usize {
self.batches.len() - self.live_batches_read
}
pub fn bytes_in(&self) -> u64 {
self.bytes_in
}
pub fn transactions(&self) -> Result<Vec<RefTransaction>> {
let mut out = Vec::new();
for (push_seq, batch) in self.batches.iter().enumerate() {
decode_batch(batch, push_seq as u64, &mut out)?;
}
Ok(out)
}
pub fn seal_to_vec(&self) -> Result<Vec<u8>> {
let schema = sealed_schema(self.hash_kind);
let batch = self.sealed_batch(&schema)?;
encode_stream(&schema, std::slice::from_ref(&batch), &write_options())
}
pub fn seal(&self, path: impl AsRef<Path>) -> Result<SealReport> {
use std::io::Write as _;
let path = path.as_ref();
let bytes = self.seal_to_vec()?;
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| SEALED_HISTORY.to_owned());
let temporary = path.with_file_name(format!("{name}.writing"));
{
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temporary)?;
file.write_all(&bytes)?;
file.flush()?;
file.sync_all()?;
}
std::fs::rename(&temporary, path)?;
sync_parent_dir(path)?;
let rows = self.batches.iter().map(|b| b.num_rows()).sum();
Ok(SealReport {
pushes: self.batches.len(),
rows,
bytes: bytes.len() as u64,
bytes_in: self.bytes_in,
segments_read: self.segments_read,
})
}
fn sealed_batch(&self, schema: &SchemaRef) -> Result<RecordBatch> {
let width = self.hash_kind.len_in_bytes() as i32;
let mut push_seqs = UInt64Builder::new();
let mut names = StringBuilder::new();
let mut old_oids = FixedSizeBinaryBuilder::new(width);
let mut new_oids = FixedSizeBinaryBuilder::new(width);
let mut old_symrefs = StringBuilder::new();
let mut new_symrefs = StringBuilder::new();
let mut actors = StringBuilder::new();
let mut ats = Int64Builder::new();
for (push_seq, batch) in self.batches.iter().enumerate() {
let refnames = column::<StringArray>(batch, COL_REFNAME)?;
let olds = column::<FixedSizeBinaryArray>(batch, COL_OLD_OID)?;
let news = column::<FixedSizeBinaryArray>(batch, COL_NEW_OID)?;
let old_syms = column::<StringArray>(batch, COL_OLD_SYMREF)?;
let new_syms = column::<StringArray>(batch, COL_NEW_SYMREF)?;
let batch_actors = column::<StringArray>(batch, COL_ACTOR)?;
let batch_ats = column::<Int64Array>(batch, COL_AT)?;
for row in 0..batch.num_rows() {
push_seqs.append_value(push_seq as u64);
names.append_value(refnames.value(row));
append_optional_binary(&mut old_oids, olds, row)?;
append_optional_binary(&mut new_oids, news, row)?;
append_optional_str(&mut old_symrefs, old_syms, row);
append_optional_str(&mut new_symrefs, new_syms, row);
actors.append_value(batch_actors.value(row));
ats.append_value(batch_ats.value(row));
}
}
RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(push_seqs.finish()),
Arc::new(names.finish()),
Arc::new(old_oids.finish()),
Arc::new(new_oids.finish()),
Arc::new(old_symrefs.finish()),
Arc::new(new_symrefs.finish()),
Arc::new(actors.finish()),
Arc::new(ats.finish()),
],
)
.map_err(arrow_err)
}
}
pub fn read_sealed(path: impl AsRef<Path>) -> Result<Vec<RefTransaction>> {
read_sealed_bytes(&std::fs::read(path.as_ref())?)
}
pub fn read_sealed_bytes(bytes: &[u8]) -> Result<Vec<RefTransaction>> {
let valid = valid_prefix_len(bytes);
if valid == 0 {
return Ok(Vec::new());
}
ipc_guard(&bytes[..valid])?;
let reader =
StreamReader::try_new(std::io::Cursor::new(&bytes[..valid]), None).map_err(arrow_err)?;
let batches: Vec<RecordBatch> = reader
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(arrow_err)?;
let mut out = Vec::new();
for batch in &batches {
let push_seqs = column::<UInt64Array>(batch, COL_PUSH_SEQ)?;
let mut rows = Vec::new();
decode_batch(batch, 0, &mut rows)?;
for (row, mut transaction) in rows.into_iter().enumerate() {
transaction.push_seq = push_seqs.value(row);
out.push(transaction);
}
}
Ok(out)
}
pub fn sealed_schema(hash_kind: gix_hash::Kind) -> SchemaRef {
let width = hash_kind.len_in_bytes() as i32;
Arc::new(Schema::new(vec![
Field::new(COL_PUSH_SEQ, DataType::UInt64, false),
Field::new(COL_REFNAME, DataType::Utf8, false),
Field::new(COL_OLD_OID, DataType::FixedSizeBinary(width), true),
Field::new(COL_NEW_OID, DataType::FixedSizeBinary(width), true),
Field::new(COL_OLD_SYMREF, DataType::Utf8, true),
Field::new(COL_NEW_SYMREF, DataType::Utf8, true),
Field::new(COL_ACTOR, DataType::Utf8, false),
Field::new(COL_AT, DataType::Int64, false),
]))
}
pub(crate) fn archive_segments(dir: &Path) -> Result<Vec<PathBuf>> {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err.into()),
};
let mut segments: Vec<PathBuf> = entries
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.extension().map(|e| e == "arrow").unwrap_or(false))
.collect();
segments.sort();
Ok(segments)
}
pub(crate) fn read_stream(
path: &Path,
expected: Option<&SchemaRef>,
into: &mut Vec<RecordBatch>,
) -> Result<u64> {
let bytes = std::fs::read(path)?;
let valid = valid_prefix_len(&bytes);
decode_stream(&bytes[..valid], expected, into)?;
Ok(bytes.len() as u64)
}
fn decode_stream(
valid: &[u8],
expected: Option<&SchemaRef>,
into: &mut Vec<RecordBatch>,
) -> Result<()> {
if valid.is_empty() {
return Ok(());
}
ipc_guard(valid)?;
let reader = StreamReader::try_new(std::io::Cursor::new(valid), None).map_err(arrow_err)?;
if let Some(expected) = expected {
check_schema(reader.schema().as_ref(), expected.as_ref())?;
}
for batch in reader {
into.push(batch.map_err(arrow_err)?);
}
Ok(())
}
fn clamp_len(len: u64) -> usize {
usize::try_from(len).unwrap_or(usize::MAX)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LiveMark {
dev: u64,
ino: u64,
schema_len: u64,
len: u64,
tail: u64,
}
const TAIL_WINDOW: usize = 8192;
impl LiveMark {
pub fn len(&self) -> u64 {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub const ENCODED_LEN: usize = 40;
pub fn encode(&self) -> [u8; Self::ENCODED_LEN] {
let mut out = [0u8; Self::ENCODED_LEN];
out[0..8].copy_from_slice(&self.dev.to_le_bytes());
out[8..16].copy_from_slice(&self.ino.to_le_bytes());
out[16..24].copy_from_slice(&self.schema_len.to_le_bytes());
out[24..32].copy_from_slice(&self.len.to_le_bytes());
out[32..40].copy_from_slice(&self.tail.to_le_bytes());
out
}
pub fn decode(bytes: &[u8]) -> Option<Self> {
if bytes.len() != Self::ENCODED_LEN {
return None;
}
let u = |at: usize| u64::from_le_bytes(bytes[at..at + 8].try_into().expect("8 bytes"));
Some(Self {
dev: u(0),
ino: u(8),
schema_len: u(16),
len: u(24),
tail: u(32),
})
}
}
pub fn live_mark(store_dir: impl AsRef<Path>) -> Result<LiveMark> {
let path = store_dir.as_ref().join(REFS_LOG);
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Ok(LiveMark {
dev: 0,
ino: 0,
schema_len: 0,
len: 0,
tail: window_digest(&[]),
});
}
Err(err) => return Err(err.into()),
};
let (dev, ino) = ids(&path)?;
let len = valid_prefix_len(&bytes);
let schema_len = crate::arrow_log::message_len(&bytes).unwrap_or(0);
Ok(LiveMark {
dev,
ino,
schema_len: schema_len as u64,
len: len as u64,
tail: window_digest(&bytes[..len]),
})
}
pub fn read_live_since(
store_dir: impl AsRef<Path>,
hash_kind: gix_hash::Kind,
mark: &LiveMark,
) -> Result<Option<Vec<RefTransaction>>> {
use std::os::unix::fs::FileExt as _;
let path = store_dir.as_ref().join(REFS_LOG);
let file = match std::fs::File::open(&path) {
Ok(file) => file,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Ok(mark.is_empty().then(Vec::new));
}
Err(err) => return Err(err.into()),
};
let (dev, ino, file_len) = {
use std::os::unix::fs::MetadataExt as _;
let meta = file.metadata()?;
(meta.dev(), meta.ino(), meta.size())
};
if !mark.is_empty() && ((dev, ino) != (mark.dev, mark.ino) || file_len < mark.len) {
return Ok(None);
}
let from = clamp_len(mark.len);
let window_at = mark.len.saturating_sub(TAIL_WINDOW as u64);
let mut window = vec![0u8; (mark.len - window_at) as usize];
file.read_exact_at(&mut window, window_at)?;
if digest_of(&window, from) != mark.tail {
return Ok(None);
}
let mut tail = vec![0u8; (file_len - mark.len) as usize];
file.read_exact_at(&mut tail, mark.len)?;
let tail_valid = valid_prefix_len(&tail);
if tail_valid == 0 {
return Ok(Some(Vec::new()));
}
let schema_len = clamp_len(mark.schema_len);
let mut stream = Vec::with_capacity(schema_len + tail_valid);
if schema_len > 0 {
let mut schema = vec![0u8; schema_len];
file.read_exact_at(&mut schema, 0)?;
stream.extend_from_slice(&schema);
}
stream.extend_from_slice(&tail[..tail_valid]);
let mut batches = Vec::new();
decode_stream(&stream, Some(&ref_schema(hash_kind)), &mut batches)?;
let mut out = Vec::new();
for (push_seq, batch) in batches.iter().enumerate() {
decode_batch(batch, push_seq as u64, &mut out)?;
}
Ok(Some(out))
}
fn ids(path: &Path) -> Result<(u64, u64)> {
use std::os::unix::fs::MetadataExt as _;
let meta = std::fs::metadata(path)?;
Ok((meta.dev(), meta.ino()))
}
fn window_digest(prefix: &[u8]) -> u64 {
digest_of(
&prefix[prefix.len().saturating_sub(TAIL_WINDOW)..],
prefix.len(),
)
}
fn digest_of(window: &[u8], prefix_len: usize) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in window {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h ^= prefix_len as u64;
h ^= h >> 30;
h = h.wrapping_mul(0xbf58_476d_1ce4_e5b9);
h ^= h >> 27;
h = h.wrapping_mul(0x94d0_49bb_1331_11eb);
h ^ (h >> 31)
}
pub(crate) fn decode_batch(
batch: &RecordBatch,
push_seq: u64,
into: &mut Vec<RefTransaction>,
) -> Result<()> {
let refnames = column::<StringArray>(batch, COL_REFNAME)?;
let olds = column::<FixedSizeBinaryArray>(batch, COL_OLD_OID)?;
let news = column::<FixedSizeBinaryArray>(batch, COL_NEW_OID)?;
let old_syms = column::<StringArray>(batch, COL_OLD_SYMREF)?;
let new_syms = column::<StringArray>(batch, COL_NEW_SYMREF)?;
let actors = column::<StringArray>(batch, COL_ACTOR)?;
let ats = column::<Int64Array>(batch, COL_AT)?;
for row in 0..batch.num_rows() {
into.push(RefTransaction {
push_seq,
refname: refnames.value(row).to_owned(),
old_oid: optional_oid(olds, row)?,
new_oid: optional_oid(news, row)?,
old_symref: optional_string(old_syms, row),
new_symref: optional_string(new_syms, row),
actor: actors.value(row).to_owned(),
at_unix_ms: ats.value(row),
});
}
Ok(())
}
fn optional_oid(array: &FixedSizeBinaryArray, row: usize) -> Result<Option<gix_hash::ObjectId>> {
if array.is_null(row) {
return Ok(None);
}
gix_hash::ObjectId::try_from(array.value(row))
.map(Some)
.map_err(|err| {
Error::backend(format!(
"the reference history holds a malformed oid: {err}"
))
})
}
fn optional_string(array: &StringArray, row: usize) -> Option<String> {
if array.is_null(row) {
None
} else {
Some(array.value(row).to_owned())
}
}
fn append_optional_binary(
builder: &mut FixedSizeBinaryBuilder,
array: &FixedSizeBinaryArray,
row: usize,
) -> Result<()> {
if array.is_null(row) {
builder.append_null();
} else {
builder.append_value(array.value(row)).map_err(arrow_err)?;
}
Ok(())
}
fn append_optional_str(builder: &mut StringBuilder, array: &StringArray, row: usize) {
if array.is_null(row) {
builder.append_null();
} else {
builder.append_value(array.value(row));
}
}