Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

overlay: lock staging directories #1916

Merged
merged 8 commits into from
May 13, 2024
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
10 changes: 8 additions & 2 deletions cmd/containers-storage/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,14 @@ func applyDiffUsingStagingDirectory(flags *mflag.FlagSet, action string, m stora
if err != nil {
return 1, err
}
if err := m.ApplyDiffFromStagingDirectory(layer, out.Target, out, &options); err != nil {
m.CleanupStagingDirectory(out.Target)

applyStagedLayerArgs := storage.ApplyStagedLayerOptions{
ID: layer,
DiffOutput: out,
DiffOptions: &options,
}
if _, err := m.ApplyStagedLayer(applyStagedLayerArgs); err != nil {
m.CleanupStagedLayer(out)
return 1, err
}
return 0, nil
Expand Down
2 changes: 0 additions & 2 deletions deprecated.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,6 @@ type LayerStore interface {
ParentOwners(id string) (uids, gids []int, err error)
ApplyDiff(to string, diff io.Reader) (int64, error)
ApplyDiffWithDiffer(to string, options *drivers.ApplyDiffOpts, differ drivers.Differ) (*drivers.DriverWithDifferOutput, error)
CleanupStagingDirectory(stagingDirectory string) error
ApplyDiffFromStagingDirectory(id, stagingDirectory string, diffOutput *drivers.DriverWithDifferOutput, options *drivers.ApplyDiffOpts) error
DifferTarget(id string) (string, error)
LoadLocked() error
PutAdditionalLayer(id string, parentLayer *Layer, names []string, aLayer drivers.AdditionalLayer) (layer *Layer, err error)
Expand Down
91 changes: 81 additions & 10 deletions drivers/overlay/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/containers/storage/pkg/fsutils"
"github.com/containers/storage/pkg/idmap"
"github.com/containers/storage/pkg/idtools"
"github.com/containers/storage/pkg/lockfile"
"github.com/containers/storage/pkg/mount"
"github.com/containers/storage/pkg/parsers"
"github.com/containers/storage/pkg/system"
Expand Down Expand Up @@ -83,6 +84,8 @@ const (
lowerFile = "lower"
maxDepth = 500

stagingLockFile = "staging.lock"

tocArtifact = "toc"
fsVerityDigestsArtifact = "fs-verity-digests"

Expand Down Expand Up @@ -127,6 +130,8 @@ type Driver struct {
usingMetacopy bool
usingComposefs bool

stagingDirsLocks map[string]*lockfile.LockFile

supportsIDMappedMounts *bool
}

Expand Down Expand Up @@ -460,6 +465,7 @@ func Init(home string, options graphdriver.Options) (graphdriver.Driver, error)
supportsVolatile: supportsVolatile,
usingComposefs: opts.useComposefs,
options: *opts,
stagingDirsLocks: make(map[string]*lockfile.LockFile),
}

d.naiveDiff = graphdriver.NewNaiveDiffDriver(d, graphdriver.NewNaiveLayerIDMapUpdater(d))
Expand Down Expand Up @@ -876,14 +882,48 @@ func (d *Driver) Metadata(id string) (map[string]string, error) {
return metadata, nil
}

// Cleanup any state created by overlay which should be cleaned when daemon
// is being shutdown. For now, we just have to unmount the bind mounted
// we had created.
// Cleanup any state created by overlay which should be cleaned when
// the storage is being shutdown. The only state created by the driver
// is the bind mount on the home directory.
func (d *Driver) Cleanup() error {
_ = os.RemoveAll(filepath.Join(d.home, stagingDir))
anyPresent := d.pruneStagingDirectories()
if anyPresent {
return nil
}
return mount.Unmount(d.home)
}

// pruneStagingDirectories cleans up any staging directory that was leaked.
// It returns whether any staging directory is still present.
func (d *Driver) pruneStagingDirectories() bool {
for _, lock := range d.stagingDirsLocks {
lock.Unlock()
}
d.stagingDirsLocks = make(map[string]*lockfile.LockFile)

anyPresent := false

homeStagingDir := filepath.Join(d.home, stagingDir)
dirs, err := os.ReadDir(homeStagingDir)
if err == nil {
for _, dir := range dirs {
stagingDirToRemove := filepath.Join(homeStagingDir, dir.Name())
lock, err := lockfile.GetLockFile(filepath.Join(stagingDirToRemove, stagingLockFile))
if err != nil {
anyPresent = true
continue
}
if err := lock.TryLock(); err != nil {
anyPresent = true
continue
}
_ = os.RemoveAll(stagingDirToRemove)
lock.Unlock()
}
}
return anyPresent
}

// LookupAdditionalLayer looks up additional layer store by the specified
// digest and ref and returns an object representing that layer.
// This API is experimental and can be changed without bumping the major version number.
Expand Down Expand Up @@ -2029,7 +2069,14 @@ func (d *Driver) DiffGetter(id string) (graphdriver.FileGetCloser, error) {

// CleanupStagingDirectory cleanups the staging directory.
func (d *Driver) CleanupStagingDirectory(stagingDirectory string) error {
return os.RemoveAll(stagingDirectory)
parentStagingDir := filepath.Dir(stagingDirectory)

if lock, ok := d.stagingDirsLocks[parentStagingDir]; ok {
delete(d.stagingDirsLocks, parentStagingDir)
lock.Unlock()
}

return os.RemoveAll(parentStagingDir)
}

func supportsDataOnlyLayersCached(home, runhome string) (bool, error) {
Expand All @@ -2050,8 +2097,8 @@ func supportsDataOnlyLayersCached(home, runhome string) (bool, error) {
return supportsDataOnly, err
}

// ApplyDiff applies the changes in the new layer using the specified function
func (d *Driver) ApplyDiffWithDiffer(id, parent string, options *graphdriver.ApplyDiffWithDifferOpts, differ graphdriver.Differ) (output graphdriver.DriverWithDifferOutput, err error) {
// ApplyDiffWithDiffer applies the changes in the new layer using the specified function
func (d *Driver) ApplyDiffWithDiffer(id, parent string, options *graphdriver.ApplyDiffWithDifferOpts, differ graphdriver.Differ) (output graphdriver.DriverWithDifferOutput, errRet error) {
var idMappings *idtools.IDMappings
if options != nil {
idMappings = options.Mappings
Expand All @@ -2068,17 +2115,31 @@ func (d *Driver) ApplyDiffWithDiffer(id, parent string, options *graphdriver.App
if err != nil && !os.IsExist(err) {
return graphdriver.DriverWithDifferOutput{}, err
}
applyDir, err = os.MkdirTemp(stagingDir, "")
layerDir, err := os.MkdirTemp(stagingDir, "")
if err != nil {
return graphdriver.DriverWithDifferOutput{}, err
}
perms := defaultPerms
if d.options.forceMask != nil {
perms = *d.options.forceMask
}
if err := os.Chmod(applyDir, perms); err != nil {
applyDir = filepath.Join(layerDir, "dir")
if err := os.Mkdir(applyDir, perms); err != nil {
return graphdriver.DriverWithDifferOutput{}, err
}

lock, err := lockfile.GetLockFile(filepath.Join(layerDir, stagingLockFile))
if err != nil {
return graphdriver.DriverWithDifferOutput{}, err
}
defer func() {
if errRet != nil {
delete(d.stagingDirsLocks, layerDir)
lock.Unlock()
}
}()
d.stagingDirsLocks[layerDir] = lock
lock.Lock()
} else {
var err error
applyDir, err = d.getDiffPath(id)
Expand Down Expand Up @@ -2112,9 +2173,19 @@ func (d *Driver) ApplyDiffWithDiffer(id, parent string, options *graphdriver.App
// ApplyDiffFromStagingDirectory applies the changes using the specified staging directory.
func (d *Driver) ApplyDiffFromStagingDirectory(id, parent string, diffOutput *graphdriver.DriverWithDifferOutput, options *graphdriver.ApplyDiffWithDifferOpts) error {
stagingDirectory := diffOutput.Target
if filepath.Dir(stagingDirectory) != d.getStagingDir(id) {
parentStagingDir := filepath.Dir(stagingDirectory)

defer func() {
if lock, ok := d.stagingDirsLocks[parentStagingDir]; ok {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this, ApplyDiffFromStagingDirectory unlocks the lock on some error return paths, but not all. A caller can’t deal with that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… but there are also various error paths on callers. So maybe the staging area should only be unlocked when this succeeds?

Or maybe this does not matter?

Note to self: FIXME investigate.

delete(d.stagingDirsLocks, parentStagingDir)
lock.Unlock()
}
}()

if filepath.Dir(parentStagingDir) != d.getStagingDir(id) {
return fmt.Errorf("%q is not a staging directory", stagingDirectory)
}

diffPath, err := d.getDiffPath(id)
if err != nil {
return err
Expand Down
56 changes: 54 additions & 2 deletions pkg/lockfile/lockfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,25 @@ func (l *LockFile) Lock() {
}
}

// LockRead locks the lockfile as a reader.
// RLock locks the lockfile as a reader.
func (l *LockFile) RLock() {
l.lock(readLock)
}

// TryLock attempts to lock the lockfile as a writer. Panic if the lock is a read-only one.
func (l *LockFile) TryLock() error {
if l.ro {
panic("can't take write lock on read-only lock file")
} else {
return l.tryLock(writeLock)
}
}

// TryRLock attempts to lock the lockfile as a reader.
func (l *LockFile) TryRLock() error {
return l.tryLock(readLock)
}

// Unlock unlocks the lockfile.
func (l *LockFile) Unlock() {
l.stateMutex.Lock()
Expand Down Expand Up @@ -401,9 +415,47 @@ func (l *LockFile) lock(lType lockType) {
// Optimization: only use the (expensive) syscall when
// the counter is 0. In this case, we're either the first
// reader lock or a writer lock.
lockHandle(l.fd, lType)
lockHandle(l.fd, lType, false)
}
l.lockType = lType
l.locked = true
l.counter++
}

// lock locks the lockfile via syscall based on the specified type and
// command.
func (l *LockFile) tryLock(lType lockType) error {
var success bool
if lType == readLock {
success = l.rwMutex.TryRLock()
} else {
success = l.rwMutex.TryLock()
}
if !success {
return fmt.Errorf("resource temporarily unavailable")
}
giuseppe marked this conversation as resolved.
Show resolved Hide resolved
l.stateMutex.Lock()
defer l.stateMutex.Unlock()
if l.counter == 0 {
// If we're the first reference on the lock, we need to open the file again.
fd, err := openLock(l.file, l.ro)
if err != nil {
l.rwMutex.Unlock()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ For readLock, this must be RUnlock.

return err
}
l.fd = fd

// Optimization: only use the (expensive) syscall when
// the counter is 0. In this case, we're either the first
// reader lock or a writer lock.
if err = lockHandle(l.fd, lType, true); err != nil {
closeHandle(fd)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is, still, unlockAndCloseHandle in semantics, on UNIX.

And looking at openLock, AFAICS calling that when not holding the lock is already expected to work. So I don’t think adding the closeHandle function should be necessary. Or maybe openLock is buggy? I didn’t test on Windows.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've splitted the functions so that we don't call UnlockFileEx on Windows without having a lock. Not sure how it is handled, as I've not tested it (nor I've a way to do it), but it seems incorrect to call unlock if we don't hold the lock.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked for unlockAndCloseHandle in #1662 (comment) to make it clear to non-UNIX programmers (or programmers unfamiliar with this weird UNIX gotcha) that a non-unlocking close is impossible.

But then https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex says that CloseHandle also unlocks locks…


WDYT about unlockAndCloseHandle(explicitClose bool)?

Ultimately I can live with the two functions split, as the PR is now. Maybe add a comment warning about the automatic unlock?

l.rwMutex.Unlock()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ For readLock, this must be RUnlock.

return err
}
}
l.lockType = lType
l.locked = true
l.counter++
return nil
}
57 changes: 57 additions & 0 deletions pkg/lockfile/lockfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -221,6 +222,62 @@ func TestLockfileName(t *testing.T) {
assert.NotEmpty(t, l.name, "lockfile name should be recorded correctly")
}

func TestTryWriteLockFile(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

l, err := getTempLockfile()
require.Nil(t, err, "error getting temporary lock file")
defer os.Remove(l.name)

err = l.TryLock()
assert.Nil(t, err)

l.AssertLocked()

errChan := make(chan error)
go func() {
errChan <- l.TryRLock()
errChan <- l.TryLock()
}()
assert.NotNil(t, <-errChan)
assert.NotNil(t, <-errChan)

l.Unlock()
}

func TestTryReadLockFile(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

l, err := getTempLockfile()
require.Nil(t, err, "error getting temporary lock file")
defer os.Remove(l.name)

err = l.TryRLock()
assert.Nil(t, err)

l.AssertLocked()

errChan := make(chan error)
go func() {
errChan <- l.TryRLock()
l.Unlock()

errChan <- l.TryLock()
}()
assert.Nil(t, <-errChan)
assert.NotNil(t, <-errChan)

l.Unlock()

go func() {
errChan <- l.TryLock()
l.Unlock()
}()
assert.Nil(t, <-errChan)
}

func TestLockfileRead(t *testing.T) {
l, err := getTempLockfile()
require.Nil(t, err, "error getting temporary lock file")
Expand Down
16 changes: 14 additions & 2 deletions pkg/lockfile/lockfile_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func openHandle(path string, mode int) (fileHandle, error) {
return fileHandle(fd), err
}

func lockHandle(fd fileHandle, lType lockType) {
func lockHandle(fd fileHandle, lType lockType, nonblocking bool) error {
fType := unix.F_RDLCK
if lType != readLock {
fType = unix.F_WRLCK
Expand All @@ -85,11 +85,23 @@ func lockHandle(fd fileHandle, lType lockType) {
Start: 0,
Len: 0,
}
for unix.FcntlFlock(uintptr(fd), unix.F_SETLKW, &lk) != nil {
cmd := unix.F_SETLKW
if nonblocking {
cmd = unix.F_SETLK
}
for {
err := unix.FcntlFlock(uintptr(fd), cmd, &lk)
if err == nil || nonblocking {
return err
}
time.Sleep(10 * time.Millisecond)
}
}

func unlockAndCloseHandle(fd fileHandle) {
unix.Close(int(fd))
}

func closeHandle(fd fileHandle) {
unix.Close(int(fd))
}
Loading