Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions storage/drivers/chown_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ type inode struct {
}

type platformChowner struct {
mutex sync.Mutex
mutex sync.Mutex
// inodes to path map, so the chowner can reconstruct hard links
// To save memory only elements that are known to be hard links are added.
inodes map[inode]string
}

Expand All @@ -40,16 +42,22 @@ func (c *platformChowner) LChown(path string, info os.FileInfo, toHost, toContai
Ino: st.Ino,
}

// Directories are assumed to never have hard links, they cannot be linked on Linux and the BSDs.
// Macos is the only one we know of that can but does not use the code here due "!darwin" build tag.
isHardLink := !info.IsDir() && st.Nlink > 1

c.mutex.Lock()

oldTarget, found := c.inodes[i]
if !found {
// Only add entries when we know hard links exists to safe memory allocations and
// the total map size. Otherwise we risk going out of memory for very large directory trees.
if !found && isHardLink {
c.inodes[i] = path
}

// If we are dealing with a file with multiple links then keep the lock until the file is
// chowned to avoid a race where we link to the old version if the file is copied up.
if found || st.Nlink > 1 {
if found || isHardLink {
defer c.mutex.Unlock()
} else {
c.mutex.Unlock()
Expand Down
69 changes: 69 additions & 0 deletions storage/drivers/chown_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//go:build !windows && !darwin

package graphdriver

import (
"io/fs"
"os"
"path/filepath"
"strconv"
"testing"

"github.com/stretchr/testify/require"
"go.podman.io/storage/pkg/idtools"
)

func fillTestFiles(b *testing.B, path string, amount int) {
dirCount := 0
dir := path
for i := range amount {
if i%256 == 0 {
dir = filepath.Join(path, "dir"+strconv.Itoa(dirCount))
err := os.Mkdir(dir, 0o700)
require.NoError(b, err)
dirCount++
}
f, err := os.Create(filepath.Join(dir, strconv.Itoa(i)))
require.NoError(b, err)
f.Close()
}
}

func Benchmark_LChown(b *testing.B) {
uid := os.Getuid()
gid := os.Getgid()
ids := idtools.NewIDMappingsFromMaps([]idtools.IDMap{
{
ContainerID: uid,
HostID: uid,
Size: 1,
},
}, []idtools.IDMap{
{
ContainerID: gid,
HostID: gid,
Size: 1,
},
})

for _, amount := range []int{1, 10, 100, 1_000, 10_000, 100_000} {
b.Run(strconv.Itoa(amount), func(b *testing.B) {
path := b.TempDir()
fillTestFiles(b, path, amount)
for b.Loop() {
chowner := newLChowner()
var chown fs.WalkDirFunc = func(path string, d fs.DirEntry, _ error) error {
info, err := d.Info()
if err != nil {
return err
}
return chowner.LChown(path, info, ids, nil)
}
err := filepath.WalkDir(path, chown)
require.NoError(b, err)
// log as custom metric the number of elements in the inodes map to show the improvement best
b.ReportMetric(float64(len(chowner.inodes)), "inodes_count/op")
}
})
}
}
Loading