From c06cd1c168049071b1a66fe1a9d287dc4d7d21de Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Tue, 30 Apr 2024 14:56:57 +0200 Subject: [PATCH] lockfile: add functions for non blocking lock extend the public API to allow a non blocking usage. Signed-off-by: Giuseppe Scrivano --- pkg/lockfile/lockfile.go | 54 +++++++++++++++++++++++++++++- pkg/lockfile/lockfile_test.go | 57 ++++++++++++++++++++++++++++++++ pkg/lockfile/lockfile_unix.go | 16 +++++++-- pkg/lockfile/lockfile_windows.go | 13 +++++++- 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/pkg/lockfile/lockfile.go b/pkg/lockfile/lockfile.go index 19694be3bc..5101475786 100644 --- a/pkg/lockfile/lockfile.go +++ b/pkg/lockfile/lockfile.go @@ -138,6 +138,20 @@ 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() @@ -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") + } + 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() + 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) + l.rwMutex.Unlock() + return err + } } l.lockType = lType l.locked = true l.counter++ + return nil } diff --git a/pkg/lockfile/lockfile_test.go b/pkg/lockfile/lockfile_test.go index 9512fbf74e..824bb32ae3 100644 --- a/pkg/lockfile/lockfile_test.go +++ b/pkg/lockfile/lockfile_test.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "sync" "sync/atomic" "testing" @@ -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") diff --git a/pkg/lockfile/lockfile_unix.go b/pkg/lockfile/lockfile_unix.go index 38e737e265..0eff003bcd 100644 --- a/pkg/lockfile/lockfile_unix.go +++ b/pkg/lockfile/lockfile_unix.go @@ -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 @@ -85,7 +85,15 @@ 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) } } @@ -93,3 +101,7 @@ func lockHandle(fd fileHandle, lType lockType) { func unlockAndCloseHandle(fd fileHandle) { unix.Close(int(fd)) } + +func closeHandle(fd fileHandle) { + unix.Close(int(fd)) +} diff --git a/pkg/lockfile/lockfile_windows.go b/pkg/lockfile/lockfile_windows.go index 304c92b158..6482529b3e 100644 --- a/pkg/lockfile/lockfile_windows.go +++ b/pkg/lockfile/lockfile_windows.go @@ -81,19 +81,30 @@ 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 { flags := 0 if lType != readLock { flags = windows.LOCKFILE_EXCLUSIVE_LOCK } + if nonblocking { + flags |= windows.LOCKFILE_FAIL_IMMEDIATELY + } ol := new(windows.Overlapped) if err := windows.LockFileEx(windows.Handle(fd), uint32(flags), reserved, allBytes, allBytes, ol); err != nil { + if nonblocking { + return err + } panic(err) } + return nil } func unlockAndCloseHandle(fd fileHandle) { ol := new(windows.Overlapped) windows.UnlockFileEx(windows.Handle(fd), reserved, allBytes, allBytes, ol) + closeHandle(fd) +} + +func closeHandle(fd fileHandle) { windows.Close(windows.Handle(fd)) }