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 / midx.rs
8.2 kB 230 lines
1use std::path::{Path, PathBuf}; 2use std::sync::atomic::AtomicBool; 3 4use gix::progress::Discard; 5use knot_git::Repo; 6 7use crate::fsio; 8use crate::{FileCount, MaintError}; 9 10const FILE_NAME: &str = "multi-pack-index"; 11 12pub(crate) const MIDX_ALLOC_LIMIT_BYTES: usize = 16 * 1024 * 1024; 13 14#[derive(Debug, Clone, Copy, PartialEq, Eq)] 15pub enum MidxStatus { 16 Written(FileCount), 17 Removed, 18 Absent, 19} 20 21pub(crate) fn clear(objects_dir: &Path) -> Result<(), MaintError> { 22 let pack_dir = objects_dir.join("pack"); 23 knot_resource::clear_temps(&pack_dir, FILE_NAME); 24 knot_resource::fsync_path(&pack_dir).map_err(Into::into) 25} 26 27pub fn write(repo: &Repo) -> Result<MidxStatus, MaintError> { 28 let objects_dir = repo.objects_dir(); 29 let kind = repo.object_format().kind(); 30 let target = objects_dir.join("pack").join(FILE_NAME); 31 let idx_paths = fsio::pack_idx_paths(&objects_dir); 32 match FileCount::new(idx_paths.len()) { 33 count if count.get() >= 2 => { 34 write_atomic(idx_paths, kind, &target)?; 35 Ok(MidxStatus::Written(count)) 36 } 37 _ => remove_if_present(&target), 38 } 39} 40 41fn write_atomic( 42 idx_paths: Vec<PathBuf>, 43 kind: gix::hash::Kind, 44 target: &Path, 45) -> Result<(), MaintError> { 46 knot_resource::atomic_write(target, knot_resource::FileMode::Inherited, |file| { 47 let mut writer = std::io::BufWriter::new(file); 48 gix_pack::multi_index::write_from_index_paths( 49 idx_paths, 50 &mut writer, 51 &mut Discard, 52 &AtomicBool::new(false), 53 gix_pack::multi_index::write::Options { object_hash: kind }, 54 ) 55 .map_err(|e| MaintError::Pack(e.to_string()))?; 56 std::io::Write::flush(&mut writer).map_err(|e| fsio::io_error(target, e)) 57 }) 58} 59 60fn remove_if_present(target: &Path) -> Result<MidxStatus, MaintError> { 61 match std::fs::remove_file(target) { 62 Ok(()) => { 63 if let Some(dir) = target.parent() { 64 knot_resource::fsync_path(dir)?; 65 } 66 Ok(MidxStatus::Removed) 67 } 68 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(MidxStatus::Absent), 69 Err(error) => Err(fsio::io_error(target, error)), 70 } 71} 72 73#[cfg(test)] 74mod tests { 75 use knot_git::{Layout, RefUpdate}; 76 use knot_types::{BranchName, ObjectFormat, Oid, RefName, RepoDid}; 77 78 use super::*; 79 use crate::test_support::{commit_on, empty_tree}; 80 81 fn midx_file(repo: &knot_git::Repo) -> PathBuf { 82 repo.objects_dir().join("pack").join(FILE_NAME) 83 } 84 85 fn pack_from(repo: &knot_git::Repo, tip: Oid, kind: gix::hash::Kind) { 86 let closure = repo 87 .select_pack_objects(knot_git::Wants::new(&[tip]), knot_git::Haves::new(&[])) 88 .unwrap(); 89 let mut bytes = Vec::new(); 90 knot_pack::write_pack(&repo.objects_dir(), closure, None, &mut bytes, kind).unwrap(); 91 knot_pack::ingest_pack( 92 &repo.objects_dir(), 93 &bytes, 94 &knot_pack::PackLimits::default(), 95 kind, 96 ) 97 .unwrap(); 98 } 99 100 fn two_pack_repo(format: ObjectFormat) -> (tempfile::TempDir, knot_git::Repo, Oid, Oid) { 101 let dir = tempfile::tempdir().unwrap(); 102 let layout = Layout::new(dir.path()) 103 .with_object_format(format) 104 .with_default_branch(BranchName::new("main").unwrap()); 105 let did = RepoDid::new("did:plc:limpet").unwrap(); 106 let repo = layout.create(&did).unwrap(); 107 let kind = format.kind(); 108 let first = commit_on(&repo, empty_tree(format), Vec::new(), "scallop"); 109 repo.update_ref(&RefUpdate::Create { 110 name: RefName::new("refs/heads/main").unwrap(), 111 new: first, 112 }) 113 .unwrap(); 114 pack_from(&repo, first, kind); 115 let second = commit_on(&repo, empty_tree(format), Vec::new(), "whelk"); 116 repo.update_ref(&RefUpdate::Create { 117 name: RefName::new("refs/heads/side").unwrap(), 118 new: second, 119 }) 120 .unwrap(); 121 pack_from(&repo, second, kind); 122 (dir, layout.open(&did).unwrap(), first, second) 123 } 124 125 fn assert_two_pack_midx(format: ObjectFormat) { 126 let (_dir, repo, scallop, whelk) = two_pack_repo(format); 127 assert_eq!(fsio::pack_idx_paths(&repo.objects_dir()).len(), 2); 128 129 let status = write(&repo).unwrap(); 130 let count = match status { 131 MidxStatus::Written(count) => count, 132 other => panic!("expected a written midx, got {other:?}"), 133 }; 134 assert_eq!(count, FileCount::new(2)); 135 assert!(midx_file(&repo).exists()); 136 137 let parsed = 138 gix_pack::multi_index::File::at(midx_file(&repo), Some(MIDX_ALLOC_LIMIT_BYTES)) 139 .unwrap(); 140 assert_eq!(parsed.num_indices() as usize, 2); 141 assert!(parsed.num_objects() >= 4); 142 143 assert!(repo.contains(scallop)); 144 assert!(repo.contains(whelk)); 145 assert_eq!(repo.object_format().kind(), format.kind()); 146 } 147 148 #[test] 149 fn writes_a_multi_pack_index_over_two_packs_sha1() { 150 assert_two_pack_midx(ObjectFormat::SHA1); 151 } 152 153 #[test] 154 fn writes_a_multi_pack_index_over_two_packs_sha256() { 155 assert_two_pack_midx(ObjectFormat::SHA256); 156 } 157 158 #[test] 159 fn clear_removes_the_index_and_sidecars_but_keeps_packs() { 160 let dir = tempfile::tempdir().unwrap(); 161 let objects_dir = dir.path(); 162 let pack_dir = objects_dir.join("pack"); 163 std::fs::create_dir_all(&pack_dir).unwrap(); 164 let make = |name: &str| std::fs::write(pack_dir.join(name), b"x").unwrap(); 165 make(FILE_NAME); 166 make("multi-pack-index-abc.bitmap"); 167 make("multi-pack-index-abc.rev"); 168 make("pack-scallop.idx"); 169 make("pack-scallop.pack"); 170 171 clear(objects_dir).unwrap(); 172 173 assert!(!pack_dir.join(FILE_NAME).exists()); 174 assert!(!pack_dir.join("multi-pack-index-abc.bitmap").exists()); 175 assert!(!pack_dir.join("multi-pack-index-abc.rev").exists()); 176 assert!( 177 pack_dir.join("pack-scallop.idx").exists(), 178 "real packs are left in place" 179 ); 180 assert!(pack_dir.join("pack-scallop.pack").exists()); 181 } 182 183 #[test] 184 fn lookup_resolves_through_the_midx_once_idx_files_are_gone() { 185 let (_dir, repo, scallop, whelk) = two_pack_repo(ObjectFormat::SHA1); 186 let objects_dir = repo.objects_dir(); 187 assert!(matches!(write(&repo).unwrap(), MidxStatus::Written(_))); 188 189 fsio::loose_objects(&objects_dir) 190 .iter() 191 .for_each(|(_, path)| std::fs::remove_file(path).unwrap()); 192 fsio::pack_idx_paths(&objects_dir) 193 .iter() 194 .for_each(|idx| std::fs::remove_file(idx).unwrap()); 195 assert!( 196 midx_file(&repo).exists(), 197 "the multi-pack-index is the only index left on disk" 198 ); 199 200 let via_midx = knot_git::Repo::open(repo.git().git_dir()).unwrap(); 201 assert!( 202 via_midx.contains(scallop) && via_midx.contains(whelk), 203 "objects resolve through the multi-pack-index with no per-pack idx present" 204 ); 205 206 std::fs::remove_file(midx_file(&repo)).unwrap(); 207 let bare = knot_git::Repo::open(repo.git().git_dir()).unwrap(); 208 assert!( 209 !bare.contains(scallop) && !bare.contains(whelk), 210 "with the index gone the packs are unreadable, proving the midx served the lookup" 211 ); 212 } 213 214 #[test] 215 fn fewer_than_two_packs_writes_nothing_and_clears_stale() { 216 let dir = tempfile::tempdir().unwrap(); 217 let layout = Layout::new(dir.path()).with_default_branch(BranchName::new("main").unwrap()); 218 let did = RepoDid::new("did:plc:conch").unwrap(); 219 let repo = layout.create(&did).unwrap(); 220 221 assert_eq!(write(&repo).unwrap(), MidxStatus::Absent); 222 assert!(!midx_file(&repo).exists()); 223 224 let target = midx_file(&repo); 225 std::fs::create_dir_all(target.parent().unwrap()).unwrap(); 226 std::fs::write(&target, b"stale").unwrap(); 227 assert_eq!(write(&repo).unwrap(), MidxStatus::Removed); 228 assert!(!midx_file(&repo).exists()); 229 } 230}