This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-maintenance / src / lib.rs
11 kB 413 lines
1use std::collections::HashSet; 2use std::path::PathBuf; 3use std::time::Duration; 4 5use knot_git::{PackRefsReport, ReflogReport, Repo}; 6use knot_types::{Oid, UnixSeconds}; 7 8mod bitmap; 9mod commitgraph; 10mod cruft; 11mod fsio; 12mod midx; 13mod prune; 14mod repack; 15mod scheduler; 16#[cfg(test)] 17mod test_support; 18 19pub use midx::MidxStatus; 20pub use scheduler::{MaintenanceHandle, PushBytes, RepoSource, Scheduler}; 21 22pub const MIN_REFLOG_RETENTION_SECS: i64 = 30 * 24 * 60 * 60; 23 24#[derive(Debug, thiserror::Error)] 25pub enum MaintError { 26 #[error("git: {0}")] 27 Git(#[from] knot_git::GitError), 28 #[error("pack: {0}")] 29 Pack(String), 30 #[error("io {path}: {message}")] 31 Io { path: PathBuf, message: String }, 32 #[error("commit-graph: {0}")] 33 CommitGraph(String), 34} 35 36impl From<knot_resource::FsError> for MaintError { 37 fn from(error: knot_resource::FsError) -> Self { 38 MaintError::Io { 39 path: error.path, 40 message: error.source.to_string(), 41 } 42 } 43} 44 45pub use knot_types::ObjectCount; 46 47knot_types::scalar_newtype! { 48 pub struct FileCount(usize); 49} 50 51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 52pub struct GeometricFactor(u64); 53 54impl GeometricFactor { 55 pub const fn new(value: u64) -> Self { 56 Self(if value < 2 { 2 } else { value }) 57 } 58 59 pub const fn full_repack() -> Self { 60 Self(u64::MAX) 61 } 62 63 pub const fn get(self) -> u64 { 64 self.0 65 } 66} 67 68#[derive(Debug, Clone, Copy)] 69pub struct Options { 70 pub repack_max_objects: ObjectCount, 71 pub geometric_factor: GeometricFactor, 72 pub prune_grace: PruneGrace, 73 pub reflog_floor: ReflogRetention, 74 pub commit_graph: bool, 75 pub multi_pack_index: bool, 76 pub bitmap: bool, 77} 78 79impl Options { 80 pub fn from_config(config: &knot_config::MaintenanceConfig) -> Self { 81 Self { 82 repack_max_objects: ObjectCount::new(config.repack_max_objects as usize), 83 geometric_factor: GeometricFactor::new(config.repack_geometric_factor), 84 prune_grace: PruneGrace::from_secs(config.prune_grace_secs), 85 reflog_floor: ReflogRetention::from_secs(config.reflog_expire_secs), 86 commit_graph: config.commit_graph, 87 multi_pack_index: config.multi_pack_index, 88 bitmap: config.bitmap, 89 } 90 } 91} 92 93const LFS_GRACE_MIN: Duration = Duration::from_secs(86_400); 94 95#[derive(Debug, Clone, Copy)] 96pub struct GcGrace(Duration); 97 98impl GcGrace { 99 pub const fn from_secs(secs: u64) -> Self { 100 Self(Duration::from_secs(secs)) 101 } 102} 103 104#[derive(Debug, Clone, Copy)] 105pub struct ReflogRetention(Duration); 106 107impl ReflogRetention { 108 pub const fn from_secs(secs: u64) -> Self { 109 Self(Duration::from_secs(secs)) 110 } 111 112 pub const fn get(self) -> Duration { 113 self.0 114 } 115} 116 117#[derive(Debug, Clone, Copy)] 118pub struct PruneGrace(Duration); 119 120impl PruneGrace { 121 pub const fn from_secs(secs: u64) -> Self { 122 Self(Duration::from_secs(secs)) 123 } 124 125 pub const fn get(self) -> Duration { 126 self.0 127 } 128} 129 130#[derive(Debug, Clone, Copy, PartialEq, Eq)] 131pub struct LfsGrace(Duration); 132 133impl LfsGrace { 134 pub const fn get(self) -> Duration { 135 self.0 136 } 137} 138 139#[derive(Debug, Clone, Copy)] 140pub struct SweepInterval(Duration); 141 142impl SweepInterval { 143 pub const fn new(interval: Duration) -> Self { 144 Self(interval) 145 } 146 147 pub const fn get(self) -> Duration { 148 self.0 149 } 150} 151 152pub fn lfs_grace(gc_grace: GcGrace, reflog_retention: ReflogRetention) -> LfsGrace { 153 let ceiling = reflog_retention 154 .0 155 .max(Duration::from_secs(MIN_REFLOG_RETENTION_SECS as u64)) 156 .max(LFS_GRACE_MIN); 157 LfsGrace(gc_grace.0.clamp(LFS_GRACE_MIN, ceiling)) 158} 159 160#[derive(Debug, Clone, Copy, PartialEq, Eq)] 161pub enum RepackStatus { 162 Repacked, 163 Clean, 164 SkippedTooLarge, 165 ClosureFailed, 166 NothingReachable, 167} 168 169#[derive(Debug, Clone, Copy, PartialEq, Eq)] 170pub struct RepackReport { 171 pub status: RepackStatus, 172 pub packed_objects: ObjectCount, 173 pub removed_loose: FileCount, 174 pub removed_packs: FileCount, 175} 176 177impl RepackReport { 178 fn skipped(status: RepackStatus) -> Self { 179 Self { 180 status, 181 packed_objects: ObjectCount::new(0), 182 removed_loose: FileCount::new(0), 183 removed_packs: FileCount::new(0), 184 } 185 } 186} 187 188#[derive(Debug, Clone, Copy, PartialEq, Eq)] 189pub struct PruneReport { 190 pub removed: FileCount, 191 pub removed_packs: FileCount, 192 pub crufted: ObjectCount, 193 pub ran: bool, 194} 195 196impl PruneReport { 197 fn skipped() -> Self { 198 Self { 199 removed: FileCount::new(0), 200 removed_packs: FileCount::new(0), 201 crufted: ObjectCount::new(0), 202 ran: false, 203 } 204 } 205} 206 207#[derive(Debug, Clone, Copy, PartialEq, Eq)] 208pub struct Report { 209 pub packed_refs: PackRefsReport, 210 pub reflog: ReflogReport, 211 pub commit_graph: bool, 212 pub repack: RepackReport, 213 pub prune: PruneReport, 214 pub multi_pack_index: MidxStatus, 215 pub bitmap: bool, 216} 217 218impl Report { 219 fn noop() -> Self { 220 Self { 221 packed_refs: PackRefsReport { packed: 0 }, 222 reflog: ReflogReport { 223 files: 0, 224 dropped: 0, 225 }, 226 commit_graph: false, 227 repack: RepackReport::skipped(RepackStatus::Clean), 228 prune: PruneReport::skipped(), 229 multi_pack_index: MidxStatus::Absent, 230 bitmap: false, 231 } 232 } 233} 234 235pub fn run_repo( 236 repo: &Repo, 237 now_seconds: UnixSeconds, 238 opts: &Options, 239) -> Result<Report, MaintError> { 240 let objects_dir = repo.objects_dir(); 241 let kind = repo.object_format().kind(); 242 let loose = fsio::loose_objects(&objects_dir); 243 let pack_count = fsio::pack_idx_paths(&objects_dir).len(); 244 let loose_refs = fsio::has_loose_refs(repo.git().git_dir()); 245 246 let graph_pending = opts.commit_graph && pack_count >= 1 && !commitgraph::exists(repo); 247 let bitmap_pending = opts.bitmap && pack_count == 1 && !bitmap::exists(&objects_dir); 248 if !opts.commit_graph { 249 commitgraph::remove(repo)?; 250 } 251 if loose.is_empty() && pack_count <= 1 && !loose_refs && !graph_pending && !bitmap_pending { 252 return Ok(Report::noop()); 253 } 254 255 let packed_refs = if loose_refs { 256 repo.pack_refs()? 257 } else { 258 PackRefsReport { packed: 0 } 259 }; 260 let floor_secs = (opts.reflog_floor.get().as_secs() as i64).max(MIN_REFLOG_RETENTION_SECS); 261 let reflog = repo.expire_reflogs(now_seconds.saturating_sub_secs(floor_secs))?; 262 263 let commit_graph = if opts.commit_graph { 264 commitgraph::write(repo)? 265 } else { 266 false 267 }; 268 269 let retention_floor = now_seconds.saturating_sub_secs(floor_secs); 270 let (repack, reachable, roots, new_stem, kept_large) = if loose.is_empty() && pack_count <= 1 { 271 ( 272 RepackReport::skipped(RepackStatus::Clean), 273 None, 274 HashSet::new(), 275 None, 276 Vec::new(), 277 ) 278 } else { 279 let roots = collect_roots(repo, retention_floor)?; 280 let (report, reachable, new_stem, kept_large) = repack::run( 281 repo, 282 &objects_dir, 283 kind, 284 roots.iter().copied().collect(), 285 opts.repack_max_objects, 286 opts.geometric_factor, 287 &loose, 288 )?; 289 (report, reachable, roots, new_stem, kept_large) 290 }; 291 292 let prune = match &reachable { 293 Some(set) => repo.with_ref_lock(|| { 294 let current = collect_roots(repo, retention_floor)?; 295 if current != roots { 296 return Ok(PruneReport::skipped()); 297 } 298 if repack.status == RepackStatus::Repacked { 299 midx::clear(&objects_dir)?; 300 cruft::run( 301 &objects_dir, 302 kind, 303 set, 304 new_stem.as_ref(), 305 &kept_large, 306 &loose, 307 opts.prune_grace.get(), 308 ) 309 } else { 310 prune::run(&objects_dir, set, &loose, opts.prune_grace.get()) 311 } 312 })?, 313 None => PruneReport::skipped(), 314 }; 315 316 let multi_pack_index = if opts.multi_pack_index { 317 midx::write(repo)? 318 } else { 319 MidxStatus::Absent 320 }; 321 322 let bitmap = if opts.bitmap { 323 bitmap::refresh(repo, &objects_dir)? 324 } else { 325 false 326 }; 327 328 Ok(Report { 329 packed_refs, 330 reflog, 331 commit_graph, 332 repack, 333 prune, 334 multi_pack_index, 335 bitmap, 336 }) 337} 338 339fn collect_roots(repo: &Repo, retention_floor: UnixSeconds) -> Result<HashSet<Oid>, MaintError> { 340 let mut roots: HashSet<Oid> = repo 341 .references()? 342 .into_iter() 343 .map(|record| record.target) 344 .collect(); 345 repo.reflog_updates_since(retention_floor) 346 .into_iter() 347 .for_each(|update| { 348 roots.insert(update.new); 349 if let Some(old) = update.old { 350 roots.insert(old); 351 } 352 }); 353 Ok(roots 354 .into_iter() 355 .filter(|oid| repo.contains(*oid)) 356 .collect()) 357} 358 359#[cfg(test)] 360mod tests { 361 use super::{GcGrace, LFS_GRACE_MIN, MIN_REFLOG_RETENTION_SECS, ReflogRetention, lfs_grace}; 362 363 #[test] 364 fn the_default_grace_is_not_clamped_by_the_coupling() { 365 let fourteen_days = 14 * 86_400; 366 let ninety_days = 90 * 86_400; 367 assert_eq!( 368 lfs_grace( 369 GcGrace::from_secs(fourteen_days), 370 ReflogRetention::from_secs(ninety_days) 371 ) 372 .get() 373 .as_secs(), 374 fourteen_days, 375 "the 14-day default is within the window and never clamped" 376 ); 377 } 378 379 #[test] 380 fn a_small_grace_is_clamped_to_the_hard_minimum() { 381 assert_eq!( 382 lfs_grace( 383 GcGrace::from_secs(0), 384 ReflogRetention::from_secs(90 * 86_400) 385 ) 386 .get(), 387 LFS_GRACE_MIN 388 ); 389 assert_eq!( 390 lfs_grace( 391 GcGrace::from_secs(60), 392 ReflogRetention::from_secs(90 * 86_400) 393 ) 394 .get(), 395 LFS_GRACE_MIN 396 ); 397 } 398 399 #[test] 400 fn grace_never_exceeds_the_reflog_retention() { 401 let short_reflog = MIN_REFLOG_RETENTION_SECS as u64; 402 assert_eq!( 403 lfs_grace( 404 GcGrace::from_secs(u64::MAX), 405 ReflogRetention::from_secs(short_reflog) 406 ) 407 .get() 408 .as_secs(), 409 short_reflog, 410 "a grace above the reflog retention is clamped to it" 411 ); 412 } 413}