Skip to content

Commit 01cdb03

Browse files
committed
cfsctl: Fix garbage collection
Signed-off-by: Chaser Huang <huangkangjing@gmail.com>
1 parent 1498349 commit 01cdb03

1 file changed

Lines changed: 103 additions & 52 deletions

File tree

‎crates/composefs/src/repository.rs‎

Lines changed: 103 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
//! verification and garbage collection support.
66
77
use std::{
8-
collections::HashSet,
9-
ffi::CStr,
8+
collections::{HashMap, HashSet},
9+
ffi::{CStr, CString, OsStr},
1010
fs::{canonicalize, File},
1111
io::{Read, Write},
1212
os::fd::{AsFd, OwnedFd},
13+
os::unix::ffi::OsStrExt,
1314
path::{Path, PathBuf},
1415
sync::Arc,
1516
};
@@ -572,7 +573,7 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
572573
Ok(ObjectID::from_object_pathname(link_content.to_bytes())?)
573574
}
574575

575-
fn walk_symlinkdir(fd: OwnedFd, objects: &mut HashSet<ObjectID>) -> Result<()> {
576+
fn walk_symlinkdir(fd: OwnedFd, entry_digests: &mut HashSet<CString>) -> Result<()> {
576577
for item in Dir::read_from(&fd)? {
577578
let entry = item?;
578579
// NB: the underlying filesystem must support returning filetype via direntry
@@ -582,11 +583,18 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
582583
let filename = entry.file_name();
583584
if filename != c"." && filename != c".." {
584585
let dirfd = openat(&fd, filename, OFlags::RDONLY, Mode::empty())?;
585-
Self::walk_symlinkdir(dirfd, objects)?;
586+
Self::walk_symlinkdir(dirfd, entry_digests)?;
586587
}
587588
}
588589
FileType::Symlink => {
589-
objects.insert(Self::read_symlink_hashvalue(&fd, entry.file_name())?);
590+
let link_content = readlinkat(&fd, entry.file_name(), [])?;
591+
let linked_path = Path::new(OsStr::from_bytes(link_content.as_bytes()));
592+
if let Some(entry_name) = linked_path.file_name() {
593+
entry_digests.insert(CString::new(entry_name.as_bytes())?);
594+
} else {
595+
// Does not have a proper file base name (i.e. "..")
596+
continue;
597+
}
590598
}
591599
_ => {
592600
bail!("Unexpected file type encountered");
@@ -608,53 +616,96 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
608616
)
609617
}
610618

611-
fn gc_category(&self, category: &str) -> Result<HashSet<ObjectID>> {
612-
let mut objects = HashSet::new();
613-
619+
// For a GC category (images / streams), return underlying entry digests and
620+
// object IDs for each entry, as a reverse lookup map
621+
// If refs_only == false, all entries will be returned, and only orphans in
622+
// objects/ will be GC'd
623+
// If refs_only == true, only entries explicitly referred to in <category>/refs
624+
// will be returned, allow unlinked entries to be GC'ed
625+
fn gc_category(&self, category: &str, refs_only: bool) -> Result<HashMap<ObjectID, String>> {
614626
let Some(category_fd) = self
615627
.openat(category, OFlags::RDONLY | OFlags::DIRECTORY)
616628
.filter_errno(Errno::NOENT)
617629
.context("Opening {category} dir in repository")?
618630
else {
619-
return Ok(objects);
631+
return Ok(HashMap::new());
620632
};
621633

622-
if let Some(refs) = openat(
623-
&category_fd,
624-
"refs",
625-
OFlags::RDONLY | OFlags::DIRECTORY,
626-
Mode::empty(),
627-
)
628-
.filter_errno(Errno::NOENT)
629-
.context("Opening {category}/refs dir in repository")?
630-
{
631-
Self::walk_symlinkdir(refs, &mut objects)?;
632-
}
633-
634-
for item in Dir::read_from(&category_fd)? {
635-
let entry = item?;
636-
let filename = entry.file_name();
637-
if filename != c"refs" && filename != c"." && filename != c".." {
638-
if entry.file_type() != FileType::Symlink {
639-
bail!("category directory contains non-symlink");
634+
let mut entry_digests = HashSet::new();
635+
if refs_only {
636+
if let Some(refs) = openat(
637+
&category_fd,
638+
"refs",
639+
OFlags::RDONLY | OFlags::DIRECTORY,
640+
Mode::empty(),
641+
)
642+
.filter_errno(Errno::NOENT)
643+
.context("Opening {category}/refs dir in repository")?
644+
{
645+
Self::walk_symlinkdir(refs, &mut entry_digests)?;
646+
}
647+
} else {
648+
// All first-level link entries should be directly object references
649+
for item in Dir::read_from(&category_fd)? {
650+
let entry = item?;
651+
let filename = entry.file_name();
652+
if filename != c"refs" && filename != c"." && filename != c".." {
653+
if entry.file_type() != FileType::Symlink {
654+
bail!("category directory contains non-symlink");
655+
}
656+
entry_digests.insert(entry.file_name().to_owned());
640657
}
658+
}
659+
}
641660

642-
// TODO: we need to sort this out. the symlink itself might be a sha256 content ID
643-
// (as for splitstreams), not an object/ to be preserved.
644-
continue;
661+
let objects = entry_digests
662+
.into_iter()
663+
.map(|entry_fn| {
664+
Ok((
665+
Self::read_symlink_hashvalue(&category_fd, &entry_fn)?,
666+
entry_fn.to_str()?.to_owned(),
667+
))
668+
})
669+
.collect::<Result<_>>()?;
645670

646-
/*
647-
let mut value = Sha256HashValue::EMPTY;
648-
hex::decode_to_slice(filename.to_bytes(), &mut value)?;
671+
Ok(objects)
672+
}
649673

650-
if !objects.contains(&value) {
651-
println!("rm {}/{:?}", category, filename);
652-
}
653-
*/
654-
}
674+
// Traverse split streams to resolve all linked objects
675+
fn walk_streams(
676+
&self,
677+
stream_digest: &str,
678+
walked_streams: &mut HashSet<String>,
679+
objects: &mut HashSet<ObjectID>,
680+
stream_map: &HashMap<ObjectID, String>,
681+
) -> Result<()> {
682+
// A split stream links to stored objects, but the the linked objects themselves could be streams that links
683+
// to other objects as well. For example an OCI image manifest links to layer objects which are split streams
684+
// linking to layer contents. This function walks the streams down and collects all linked objects
685+
if walked_streams.contains(stream_digest) {
686+
return Ok(());
655687
}
688+
walked_streams.insert(stream_digest.to_owned());
689+
690+
let mut split_stream = self.open_stream(stream_digest, None)?;
691+
let mut streams_to_walk = HashSet::new();
692+
split_stream.get_object_refs(|id| {
693+
println!("# with {id:?}");
694+
objects.insert(id.clone());
695+
if stream_map.contains_key(id) {
696+
let digest = stream_map.get(id).expect("key exists");
697+
println!("# which is streams/{digest:}");
698+
streams_to_walk.insert(digest);
699+
}
700+
})?;
701+
streams_to_walk
702+
.into_iter()
703+
.map(|stream_digest| {
704+
self.walk_streams(stream_digest, walked_streams, objects, stream_map)
705+
})
706+
.collect::<Result<()>>()?;
656707

657-
Ok(objects)
708+
Ok(())
658709
}
659710

660711
/// Given an image, return the set of all objects referenced by it.
@@ -675,21 +726,21 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
675726

676727
let mut objects = HashSet::new();
677728

678-
for ref object in self.gc_category("images")? {
679-
println!("{object:?} lives as an image");
680-
objects.insert(object.clone());
681-
objects.extend(self.objects_for_image(&object.to_hex())?);
729+
for ref image in self.gc_category("images", true)? {
730+
println!("# {image:?} lives as an image");
731+
objects.insert(image.0.clone());
732+
self.objects_for_image(&image.1)?.iter().for_each(|id| {
733+
println!("# with {id:?}");
734+
objects.insert(id.clone());
735+
});
682736
}
683737

684-
for object in self.gc_category("streams")? {
685-
println!("{object:?} lives as a stream");
686-
objects.insert(object.clone());
687-
688-
let mut split_stream = self.open_stream(&object.to_hex(), None)?;
689-
split_stream.get_object_refs(|id| {
690-
println!(" with {id:?}");
691-
objects.insert(id.clone());
692-
})?;
738+
let mut walked_streams = HashSet::new();
739+
let stream_map = self.gc_category("streams", false)?;
740+
for stream in self.gc_category("streams", true)? {
741+
println!("# {stream:?} lives as a stream");
742+
objects.insert(stream.0.clone());
743+
self.walk_streams(&stream.1, &mut walked_streams, &mut objects, &stream_map)?;
693744
}
694745

695746
for first_byte in 0x0..=0xff {

0 commit comments

Comments
 (0)