#[cfg(unix)]
use std::collections::BTreeSet;
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::io;
#[cfg(unix)]
use std::path::Path;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(unix)]
use super::repack::{Leg, LegProgress};
#[cfg(unix)]
use super::retire::{multi_pack_index_paths, read_cause, recheck, same_old_generation, snapshot};
use super::{MaintenanceIsolation, RepackLimits, RepackPublished, RetentionPolicy, RetireCause};
#[cfg(unix)]
use super::{PartialClonePolicy, RepackError, RetentionError};
use crate::Repository;
#[cfg(unix)]
use crate::pack::FilePack;
#[cfg(unix)]
use crate::pack::publish::{Step, sync_directory};
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct IsolatedRepackReport {
pub ordinary: Option<RepackPublished>,
pub promisor: Option<RepackPublished>,
pub durable: bool,
pub visible: Vec<PathBuf>,
pub removed: Vec<PathBuf>,
pub retained: Vec<RetainedPack>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct RetainedPack {
pub pack: PathBuf,
pub reason: RetainReason,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RetainReason {
Protected,
NoDurablePromisorCoverage,
}
#[derive(Debug, thiserror::Error)]
#[error("isolated repack failed")]
#[non_exhaustive]
pub struct IsolatedRepackFailure {
#[source]
pub cause: RetireCause,
pub report: Box<IsolatedRepackReport>,
pub uncertain: Option<PathBuf>,
}
#[cfg(unix)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum Checkpoint {
Promisor(Step),
Ordinary(Step),
Published,
RemoveIndex,
RemovePack,
RemoveMarker,
}
impl Repository {
pub fn repack_retained_isolated<I: MaintenanceIsolation>(
&self,
isolation: &mut I,
policy: &RetentionPolicy,
limits: RepackLimits,
cancel: &AtomicBool,
) -> Result<IsolatedRepackReport, IsolatedRepackFailure> {
if cancel.load(Ordering::Relaxed) {
return Err(failed(
RetireCause::Cancelled,
&IsolatedRepackReport::default(),
));
}
let _guard = isolation.acquire(self).map_err(|error| {
failed(
RetireCause::Isolation(error),
&IsolatedRepackReport::default(),
)
})?;
#[cfg(not(unix))]
{
let _ = (policy, limits);
Err(failed(
RetireCause::UnsupportedPlatform,
&IsolatedRepackReport::default(),
))
}
#[cfg(unix)]
self.repack_isolated_at(policy, limits, cancel, &mut |_| Ok(()))
}
#[cfg(unix)]
pub(super) fn repack_isolated_at(
&self,
policy: &RetentionPolicy,
limits: RepackLimits,
cancel: &AtomicBool,
checkpoint: &mut dyn FnMut(Checkpoint) -> io::Result<()>,
) -> Result<IsolatedRepackReport, IsolatedRepackFailure> {
let _headers = crate::pack::HeaderScope::new();
let mut report = IsolatedRepackReport::default();
check(cancel, &report)?;
let current = self.reopen_for_repack(&report)?;
let partial = current.is_partial_clone();
if partial && policy.partial_clone != PartialClonePolicy::Promised {
return Err(failed(
RetireCause::Incomplete(RetentionError::PartialClone),
&report,
));
}
let directory = current.object_dir().join("pack");
let created_directory = !directory.exists();
let before = snapshot(&directory, policy.max_entries, cancel)
.map_err(|cause| failed(cause, &report))?;
let mut plan = current.plan_retention(policy, cancel);
if let super::RetentionOutcome::Incomplete(reason) = plan.outcome {
let cause = if cancel.load(Ordering::Relaxed) {
RetireCause::Cancelled
} else {
RetireCause::Incomplete(reason)
};
return Err(failed(cause, &report));
}
if self.reopen_for_repack(&report)?.is_partial_clone() != partial {
return Err(failed(RetireCause::Changed, &report));
}
let protected = std::mem::take(&mut plan.protected_packs);
let store = plan.store.as_ref().ok_or_else(|| {
failed(
RetireCause::Incomplete(RetentionError::MissingPinnedPlanStore),
&report,
)
})?;
let mut promisor_ids = BTreeSet::new();
let mut sources = BTreeSet::new();
if partial {
for (path, pack) in store.primary_promisor_packs() {
sources.insert(path.to_path_buf());
for id in pack.identities() {
check(cancel, &report)?;
promisor_ids.insert(id.map_err(|error| read_failure(error, &report))?);
if promisor_ids.len() > policy.max_entries {
return Err(failed(
RetireCause::Incomplete(RetentionError::PromisedObjectLimit),
&report,
));
}
}
}
}
let ordinary_ids: BTreeSet<_> = plan.required.difference(&promisor_ids).copied().collect();
let promisor_leg = Leg {
ids: &promisor_ids,
marker: Some(b""),
partial_clone: true,
};
if !promisor_ids.is_empty() {
let mut progress = LegProgress::default();
let (published, _) = current
.publish_leg(
&plan,
promisor_leg,
policy,
limits,
cancel,
|| {},
&mut progress,
|step| checkpoint(Checkpoint::Promisor(step)).map_err(RepackError::Io),
)
.map_err(|error| {
repack_failure(error, std::mem::take(&mut progress), &mut report, false)
})?;
report.visible.extend(progress.publish.visible);
report.promisor = Some(published);
}
let ordinary_leg = Leg {
ids: &ordinary_ids,
marker: None,
partial_clone: partial,
};
if !ordinary_ids.is_empty() {
let mut progress = LegProgress::default();
let (published, _) = current
.publish_leg(
&plan,
ordinary_leg,
policy,
limits,
cancel,
|| {},
&mut progress,
|step| checkpoint(Checkpoint::Ordinary(step)).map_err(RepackError::Io),
)
.map_err(|error| {
repack_failure(error, std::mem::take(&mut progress), &mut report, true)
})?;
report.visible.extend(progress.publish.visible);
report.ordinary = Some(published);
}
if report.promisor.is_some() || report.ordinary.is_some() {
if created_directory {
sync_directory(current.object_dir())
.map_err(|error| uncertain(error, &report, &directory))?;
}
report.durable = true;
}
checkpoint(Checkpoint::Published)
.map_err(|error| failed(RetireCause::Io(error), &report))?;
if !partial {
return Ok(report);
}
let basename = |published: &RepackPublished| {
directory.join(format!("pack-{}", published.written.checksum))
};
let new_promisor = report.promisor.as_ref().map(basename);
let mut replacement_paths = Vec::new();
for (published, marker) in [(&report.promisor, true), (&report.ordinary, false)] {
if let Some(published) = published {
let base = basename(published);
replacement_paths.push(base.with_extension("pack"));
replacement_paths.push(base.with_extension("idx"));
if marker {
replacement_paths.push(base.with_extension("promisor"));
}
}
}
let mut retiring = Vec::new();
for marker in before
.keys()
.filter(|path| path.extension().is_some_and(|ext| ext == "promisor"))
{
let pack = marker.with_extension("pack");
let index = marker.with_extension("idx");
if !before.contains_key(&pack) || !before.contains_key(&index) {
continue;
}
if new_promisor.as_deref() == Some(&*marker.with_extension("")) {
continue;
}
let sidecar = before.keys().any(|path| {
path.file_stem() == pack.file_stem()
&& !matches!(
path.extension().and_then(|ext| ext.to_str()),
Some("idx" | "pack" | "rev" | "bitmap" | "promisor")
)
});
let reason = if protected.contains(&pack) || sidecar {
Some(RetainReason::Protected)
} else if new_promisor.is_none() || !sources.contains(&pack) {
Some(RetainReason::NoDurablePromisorCoverage)
} else {
None
};
match reason {
Some(reason) => report.retained.push(RetainedPack { pack, reason }),
None => retiring.push(index),
}
}
let Some(new_promisor) = new_promisor.filter(|_| !retiring.is_empty()) else {
return Ok(report);
};
let mut budget = policy.packs;
let replacement = FilePack::open(
self.object_format(),
&new_promisor.with_extension("idx"),
&mut budget,
cancel,
)
.map_err(|error| read_failure(error, &report))?;
replacement
.verify_index()
.map_err(|error| read_failure(error, &report))?;
let mut positions = BTreeSet::new();
for (path, old) in store.primary_promisor_packs() {
if !retiring.contains(&path.with_extension("idx")) {
continue;
}
for id in old.identities() {
check(cancel, &report)?;
let id = id.map_err(|error| read_failure(error, &report))?;
let position = replacement
.find(id)
.map_err(|error| read_failure(error, &report))?
.ok_or_else(|| failed(RetireCause::ReplacementMissing(id), &report))?;
let offset = replacement
.offset(position)
.map_err(|error| read_failure(error, &report))?;
positions.insert((offset, position));
}
}
replacement
.verify(cancel)
.map_err(|error| failed(read_cause(error), &report))?;
let positions: Vec<usize> = positions
.into_iter()
.map(|(_, position)| position)
.collect();
replacement
.read_each(&positions, policy.read, policy.max_threads, cancel)
.map_err(|error| failed(read_cause(error), &report))?;
let after = snapshot(&directory, policy.max_entries, cancel)
.map_err(|cause| failed(cause, &report))?;
if !same_old_generation(&before, &after, &replacement_paths) {
return Err(failed(RetireCause::Changed, &report));
}
plan.check_packs_unchanged(cancel)
.map_err(|error| failed(RetireCause::PackChanged(Box::new(error)), &report))?;
if !self.reopen_for_repack(&report)?.is_partial_clone() {
return Err(failed(RetireCause::Changed, &report));
}
let indices = multi_pack_index_paths(&directory)
.map_err(|error| failed(RetireCause::Io(error), &report))?;
let removed_index = !indices.is_empty();
for path in indices {
let result = if path.is_dir() {
fs::remove_dir_all(&path)
} else {
fs::remove_file(&path)
};
result.map_err(|error| uncertain(error, &report, &path))?;
report.removed.push(path);
}
if removed_index {
sync_directory(&directory).map_err(|error| uncertain(error, &report, &directory))?;
}
for index in retiring {
check(cancel, &report)?;
let marker = index.with_extension("promisor");
let mut artifacts = vec![index.clone()];
for extension in ["rev", "bitmap"] {
let path = index.with_extension(extension);
if before.contains_key(&path) {
artifacts.push(path);
}
}
artifacts.push(index.with_extension("pack"));
for path in artifacts.iter().chain([&marker]) {
recheck(path, before[path]).map_err(|cause| failed(cause, &report))?;
}
for path in artifacts {
replacement
.check_unchanged()
.map_err(|error| failed(RetireCause::PackChanged(Box::new(error)), &report))?;
let step = match path.extension().and_then(|ext| ext.to_str()) {
Some("idx") => Some(Checkpoint::RemoveIndex),
Some("pack") => Some(Checkpoint::RemovePack),
_ => None,
};
step.map_or(Ok(()), &mut *checkpoint)
.and_then(|()| fs::remove_file(&path))
.map_err(|error| uncertain(error, &report, &path))?;
let index_removed = path == index;
report.removed.push(path);
if index_removed {
sync_directory(&directory)
.map_err(|error| uncertain(error, &report, &index))?;
}
}
sync_directory(&directory)
.map_err(|error| uncertain(error, &report, &index.with_extension("pack")))?;
checkpoint(Checkpoint::RemoveMarker)
.and_then(|()| fs::remove_file(&marker))
.map_err(|error| uncertain(error, &report, &marker))?;
report.removed.push(marker.clone());
}
Ok(report)
}
#[cfg(unix)]
fn reopen_for_repack(
&self,
report: &IsolatedRepackReport,
) -> Result<Repository, IsolatedRepackFailure> {
let current = Repository::open(self.git_dir()).map_err(|error| {
failed(RetireCause::Incomplete(RetentionError::read(error)), report)
})?;
if current.object_dir() != self.object_dir()
|| current.object_format() != self.object_format()
{
return Err(failed(RetireCause::Changed, report));
}
Ok(current)
}
}
#[cfg(unix)]
fn repack_failure(
error: RepackError,
progress: LegProgress,
report: &mut IsolatedRepackReport,
ordinary: bool,
) -> IsolatedRepackFailure {
let leg = if ordinary {
&mut report.ordinary
} else {
&mut report.promisor
};
if let RepackError::Bitmap { published, .. } = &error {
*leg = Some(**published);
} else if progress.publish.pair_visible()
&& let Some(written) = progress.written
{
*leg = Some(RepackPublished {
written,
bitmap: super::BitmapOutcome::Disabled,
});
}
report.visible.extend(progress.publish.visible);
let cause = match error {
RepackError::Cancelled => RetireCause::Cancelled,
RepackError::Incomplete(reason) => RetireCause::Incomplete(reason),
error => RetireCause::Repack(Box::new(error)),
};
IsolatedRepackFailure {
cause,
report: Box::new(report.clone()),
uncertain: progress.publish.uncertain,
}
}
#[cfg(unix)]
fn read_failure(
error: crate::ObjectReadError,
report: &IsolatedRepackReport,
) -> IsolatedRepackFailure {
failed(RetireCause::Read(Box::new(error)), report)
}
#[cfg(unix)]
fn uncertain(
error: io::Error,
report: &IsolatedRepackReport,
path: &Path,
) -> IsolatedRepackFailure {
IsolatedRepackFailure {
cause: RetireCause::Io(error),
report: Box::new(report.clone()),
uncertain: Some(path.to_path_buf()),
}
}
#[cfg(unix)]
fn check(cancel: &AtomicBool, report: &IsolatedRepackReport) -> Result<(), IsolatedRepackFailure> {
if cancel.load(Ordering::Relaxed) {
Err(failed(RetireCause::Cancelled, report))
} else {
Ok(())
}
}
fn failed(cause: RetireCause, report: &IsolatedRepackReport) -> IsolatedRepackFailure {
IsolatedRepackFailure {
cause,
report: Box::new(report.clone()),
uncertain: None,
}
}