use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::SystemTime;
use super::{Limits, MultiPackIndex};
use crate::{ObjectId, PackLimits, Repository};
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub struct BatchLimits {
pub index: Limits,
pub packs: PackLimits,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct BatchPack {
pub path: PathBuf,
pub objects: usize,
pub estimated_bytes: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum BatchSkipReason {
Kept,
Promisor,
Cruft,
NoOwnedObjects,
TooLarge,
OutsideBatch,
InsufficientPacks,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct BatchSkipped {
pub path: PathBuf,
pub reason: BatchSkipReason,
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct RepackBatch {
pub packs: Vec<BatchPack>,
pub skipped: Vec<BatchSkipped>,
pub estimated_bytes: u64,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum BatchError {
#[error("invalid incremental repack index")]
Index(#[source] super::Error),
#[error("cannot inspect incremental repack packs")]
Read(#[source] Box<crate::ObjectReadError>),
#[error("observing incremental repack input {path}")]
Path {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("incremental-chain repack selection is unsupported")]
IncrementalChain,
#[error("incremental repack selection cancelled")]
Cancelled,
#[error("incremental repack inputs changed")]
Changed,
}
impl Repository {
pub fn plan_incremental_repack(
&self,
batch_bytes: u64,
limits: BatchLimits,
cancel: &AtomicBool,
) -> Result<RepackBatch, BatchError> {
prepare(self, batch_bytes, limits, cancel).map(|prepared| prepared.report)
}
}
pub(super) struct PreparedBatch {
pub objects: crate::Objects,
pub index: Vec<u8>,
pub report: RepackBatch,
}
pub(super) fn prepare(
repository: &Repository,
batch_bytes: u64,
limits: BatchLimits,
cancel: &AtomicBool,
) -> Result<PreparedBatch, BatchError> {
check(cancel)?;
let directory = repository.object_dir().join("pack");
if present(&directory.join("multi-pack-index.d/multi-pack-index-chain"))? {
return Err(BatchError::IncrementalChain);
}
let bytes = super::verify::read(&directory.join("multi-pack-index"), limits.index.max_bytes)
.map_err(BatchError::Index)?
.ok_or(BatchError::Index(super::Error::MissingIndex))?;
let index = MultiPackIndex::parse_with(&bytes, repository.object_format(), limits.index)
.map_err(BatchError::Index)?;
if index.base_count() != 0 {
return Err(BatchError::IncrementalChain);
}
check(cancel)?;
let objects = repository
.objects_controlled(limits.packs, Default::default(), cancel)
.map_err(|error| BatchError::Read(Box::new(error)))?;
let primary: BTreeMap<_, _> = objects.primary_packs_with_paths().into_iter().collect();
let mut named = Vec::with_capacity(index.pack_names().len());
for name in index.pack_names() {
check(cancel)?;
let path = directory.join(name).with_extension("pack");
let pack = primary
.get(path.as_path())
.ok_or(BatchError::Index(super::Error::MissingPack))?;
pack.verify_index()
.map_err(|error| BatchError::Read(Box::new(error)))?;
named.push((path, *pack));
}
let mut owned = vec![0usize; named.len()];
for (position, bytes) in index
.ids
.chunks_exact(repository.object_format().digest_len())
.enumerate()
{
check(cancel)?;
let id = ObjectId::from_bytes(repository.object_format(), bytes)
.map_err(|_| BatchError::Index(super::Error::Malformed("object identity")))?;
let location = index.location(position).map_err(BatchError::Index)?;
let pack = named[location.pack as usize].1;
let found = pack
.find(id)
.map_err(|error| BatchError::Read(Box::new(error)))?
.ok_or(BatchError::Index(super::Error::Malformed(
"object absent from named pack",
)))?;
if pack
.offset(found)
.map_err(|error| BatchError::Read(Box::new(error)))? as u64
!= location.offset
{
return Err(BatchError::Index(super::Error::Malformed(
"object offset disagrees with pack index",
)));
}
owned[location.pack as usize] += 1;
}
let mut report = RepackBatch::default();
let mut candidates = Vec::new();
for ((path, pack), count) in named.into_iter().zip(owned) {
check(cancel)?;
let reason = if present(&path.with_extension("keep"))? {
Some(BatchSkipReason::Kept)
} else if present(&path.with_extension("promisor"))? {
Some(BatchSkipReason::Promisor)
} else if present(&path.with_extension("mtimes"))? {
Some(BatchSkipReason::Cruft)
} else if count == 0 {
Some(BatchSkipReason::NoOwnedObjects)
} else {
None
};
let estimated_bytes = if pack.object_count() == 0 {
0
} else {
(u128::from(pack.artifact_bytes().0) * count as u128 / pack.object_count() as u128)
as u64
};
let reason = reason.or_else(|| {
(batch_bytes != 0 && estimated_bytes >= batch_bytes)
.then_some(BatchSkipReason::TooLarge)
});
if let Some(reason) = reason {
report.skipped.push(BatchSkipped { path, reason });
continue;
}
let metadata = fs::symlink_metadata(&path).map_err(|source| BatchError::Path {
path: path.clone(),
source,
})?;
if !metadata.is_file() {
return Err(BatchError::Changed);
}
let modified = metadata.modified().map_err(|source| BatchError::Path {
path: path.clone(),
source,
})?;
candidates.push((
BatchPack {
path,
objects: count,
estimated_bytes,
},
modification_seconds(modified),
));
}
select(candidates, batch_bytes, &mut report)?;
objects
.check_packs_unchanged(cancel)
.map_err(|error| BatchError::Read(Box::new(error)))?;
check(cancel)?;
drop(primary);
drop(index);
Ok(PreparedBatch {
objects,
index: bytes,
report,
})
}
fn select(
mut candidates: Vec<(BatchPack, i128)>,
target: u64,
report: &mut RepackBatch,
) -> Result<(), BatchError> {
candidates.sort_by(|(left, left_time), (right, right_time)| {
left_time.cmp(right_time).then(left.path.cmp(&right.path))
});
if candidates.len() < 2 {
report
.skipped
.extend(candidates.into_iter().map(|(pack, _)| BatchSkipped {
path: pack.path,
reason: BatchSkipReason::InsufficientPacks,
}));
return Ok(());
}
for (pack, _) in candidates {
if target != 0 && report.estimated_bytes >= target {
report.skipped.push(BatchSkipped {
path: pack.path,
reason: BatchSkipReason::OutsideBatch,
});
} else {
report.estimated_bytes = report
.estimated_bytes
.checked_add(pack.estimated_bytes)
.ok_or(BatchError::Index(super::Error::Limit(
"estimated batch bytes",
)))?;
report.packs.push(pack);
}
}
Ok(())
}
fn modification_seconds(time: SystemTime) -> i128 {
match time.duration_since(SystemTime::UNIX_EPOCH) {
Ok(duration) => i128::from(duration.as_secs()),
Err(error) => {
let duration = error.duration();
-i128::from(duration.as_secs()) - i128::from(duration.subsec_nanos() != 0)
}
}
}
fn present(path: &Path) -> Result<bool, BatchError> {
match fs::symlink_metadata(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(source) => Err(BatchError::Path {
path: path.to_owned(),
source,
}),
}
}
fn check(cancel: &AtomicBool) -> Result<(), BatchError> {
if cancel.load(Ordering::Relaxed) {
Err(BatchError::Cancelled)
} else {
Ok(())
}
}