Skip to content

Commit

Permalink
Add PrepopulatedDirectory.FilterChildren()
Browse files Browse the repository at this point in the history
Inside the client-side FUSE daemon we want to add logic to automatically
remove files from the file system that are no longer present remotely.
We'll solve this by scanning the entire bazel-out/ directory at the
start of every build to gather the digests. For every batch of files,
we'll call FindMissingBlobs(), thereby causing all of those files to be
touched.

The files that are present will then be guaranteed to remain in
centralized storage for the duration of the build. The ones that are
absent can be removed.
  • Loading branch information
exbucks committed Jan 31, 2021
1 parent 3c380a4 commit 430f460
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 0 deletions.
1 change: 1 addition & 0 deletions internal/mock/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ gomock(
out = "fuse.go",
interfaces = [
"CASFileFactory",
"ChildFilter",
"EntryNotifier",
"FileAllocator",
"InitialContentsFetcher",
Expand Down
53 changes: 53 additions & 0 deletions pkg/filesystem/fuse/in_memory_prepopulated_directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,59 @@ func (i *inMemoryPrepopulatedDirectory) CreateAndEnterPrepopulatedDirectory(name
return child, nil
}

func (i *inMemoryPrepopulatedDirectory) filterChildrenRecursive(childFilter ChildFilter) bool {
i.lock.Lock()
if initialContentsFetcher := i.initialContentsFetcher; initialContentsFetcher != nil {
// Directory is not initialized. There is no need to
// instantiate it. Simply provide the
// InitialContentsFetcher to the callback.
i.lock.Unlock()
return childFilter(InitialNode{Directory: initialContentsFetcher}, func() error {
return i.RemoveAllChildren(false)
})
}

// Directory is already initialized. Gather the contents.
type leafInfo struct {
name path.Component
leaf NativeLeaf
}
leaves := make([]leafInfo, 0, len(i.contents.leaves))
for name, child := range i.contents.leaves {
leaves = append(leaves, leafInfo{
name: name,
leaf: child,
})
}

directories := make([]*inMemoryPrepopulatedDirectory, 0, len(i.contents.directories))
for _, child := range i.contents.directories {
directories = append(directories, child)
}
i.lock.Unlock()

// Invoke the callback for all children.
for _, child := range leaves {
name := child.name
if !childFilter(InitialNode{Leaf: child.leaf}, func() error {
return i.Remove(name)
}) {
return false
}
}
for _, child := range directories {
if !child.filterChildrenRecursive(childFilter) {
return false
}
}
return true
}

func (i *inMemoryPrepopulatedDirectory) FilterChildren(childFilter ChildFilter) error {
i.filterChildrenRecursive(childFilter)
return nil
}

func (i *inMemoryPrepopulatedDirectory) fuseGetContents() (*inMemoryDirectoryContents, fuse.Status) {
contents, err := i.getContents()
if err != nil {
Expand Down
65 changes: 65 additions & 0 deletions pkg/filesystem/fuse/in_memory_prepopulated_directory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,71 @@ func TestInMemoryPrepopulatedDirectoryInstallHooks(t *testing.T) {
require.Equal(t, s, go_fuse.Status(syscall.EIO))
}

func TestInMemoryPrepopulatedDirectoryFilterChildren(t *testing.T) {
ctrl := gomock.NewController(t)

fileAllocator := mock.NewMockFileAllocator(ctrl)
errorLogger := mock.NewMockErrorLogger(ctrl)
inodeNumberGenerator := mock.NewMockThreadSafeGenerator(ctrl)
entryNotifier := mock.NewMockEntryNotifier(ctrl)
d := fuse.NewInMemoryPrepopulatedDirectory(fileAllocator, errorLogger, 100, inodeNumberGenerator, entryNotifier.Call)

// In the initial state, InMemoryPrepopulatedDirectory will have
// an EmptyInitialContentsFetcher associated with it.
childFilter1 := mock.NewMockChildFilter(ctrl)
childFilter1.EXPECT().Call(fuse.InitialNode{Directory: fuse.EmptyInitialContentsFetcher}, gomock.Any()).Return(true)
require.NoError(t, d.FilterChildren(childFilter1.Call))

// After attempting to access the directory's contents, the
// InitialContentsFetcher should be evaluated. Successive
// FilterChildren() calls will no longer report it.
entries, err := d.ReadDir()
require.NoError(t, err)
require.Empty(t, entries)

childFilter2 := mock.NewMockChildFilter(ctrl)
require.NoError(t, d.FilterChildren(childFilter2.Call))

// Create some children and call FilterChildren() again. All
// children should be reported. Remove some of them.
inodeNumberGenerator.EXPECT().Uint64().Return(uint64(101))
directory1 := mock.NewMockInitialContentsFetcher(ctrl)
inodeNumberGenerator.EXPECT().Uint64().Return(uint64(102))
directory2 := mock.NewMockInitialContentsFetcher(ctrl)
leaf1 := mock.NewMockNativeLeaf(ctrl)
leaf2 := fuse.NewSymlink("target")
require.NoError(t, d.CreateChildren(map[path.Component]fuse.InitialNode{
path.MustNewComponent("directory1"): {Directory: directory1},
path.MustNewComponent("directory2"): {Directory: directory2},
path.MustNewComponent("leaf1"): {Leaf: leaf1},
path.MustNewComponent("leaf2"): {Leaf: leaf2},
}, false))

childFilter3 := mock.NewMockChildFilter(ctrl)
childFilter3.EXPECT().Call(fuse.InitialNode{Directory: directory1}, gomock.Any()).
DoAndReturn(func(initialNode fuse.InitialNode, remove func() error) bool {
require.NoError(t, remove())
return true
})
childFilter3.EXPECT().Call(fuse.InitialNode{Directory: directory2}, gomock.Any()).Return(true)
childFilter3.EXPECT().Call(fuse.InitialNode{Leaf: leaf1}, gomock.Any()).
DoAndReturn(func(initialNode fuse.InitialNode, remove func() error) bool {
leaf1.EXPECT().Unlink()
entryNotifier.EXPECT().Call(uint64(100), path.MustNewComponent("leaf1"))
require.NoError(t, remove())
return true
})
childFilter3.EXPECT().Call(fuse.InitialNode{Leaf: leaf2}, gomock.Any()).Return(true)
require.NoError(t, d.FilterChildren(childFilter3.Call))

// Another call to FilterChildren() should only report the
// children that were not removed previously.
childFilter4 := mock.NewMockChildFilter(ctrl)
childFilter4.EXPECT().Call(fuse.InitialNode{Directory: directory2}, gomock.Any()).Return(true)
childFilter4.EXPECT().Call(fuse.InitialNode{Leaf: leaf2}, gomock.Any()).Return(true)
require.NoError(t, d.FilterChildren(childFilter4.Call))
}

func TestInMemoryPrepopulatedDirectoryFUSECreateFileExists(t *testing.T) {
ctrl := gomock.NewController(t)

Expand Down
24 changes: 24 additions & 0 deletions pkg/filesystem/fuse/prepopulated_directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ import (
"github.com/buildbarn/bb-storage/pkg/util"
)

// ChildFilter is a callback that is invoked by
// PrepopulatedDirectory.FilterChildren() for each of the children
// underneath the current directory hierarchy.
//
// For each of the children, an InitialNode object is provided that
// describes the contents of that file or directory. In addition to
// that, a callback is provided that can remove the file or the contents
// of the directory. This callback may be invoked synchronously or
// asynchronously, potentially after FilterChildren() has completed.
//
// The boolean return value of this function signals whether traversal
// should continue. When false, traversal will stop immediately.
type ChildFilter func(node InitialNode, remove func() error) bool

// PrepopulatedDirectory is a Directory that is writable and can contain
// files of type NativeLeaf.
//
Expand Down Expand Up @@ -55,6 +69,16 @@ type PrepopulatedDirectory interface {
// except that it uses the FUSE specific FileAllocator instead
// of FilePool.
InstallHooks(fileAllocator FileAllocator, errorLogger util.ErrorLogger)
// FilterChildren() can be used to traverse over all of the
// InitialContentsFetcher and NativeLeaf objects stored in this
// directory hierarchy. For each of the objects, a callback is
// provided that can be used to remove the file or the contents
// of the directory associated with this object.
//
// This function can be used by bb_clientd to purge files or
// directories that are no longer present in the Content
// Addressable Storage at the start of the build.
FilterChildren(childFilter ChildFilter) error

// Functions inherited from filesystem.Directory.
ReadDir() ([]filesystem.FileInfo, error)
Expand Down

0 comments on commit 430f460

Please sign in to comment.