Skip to content

Commit

Permalink
lockfile: add functions for non blocking lock
Browse files Browse the repository at this point in the history
extend the public API to allow a non blocking usage.

Signed-off-by: Giuseppe Scrivano <[email protected]>
  • Loading branch information
giuseppe committed May 10, 2024
1 parent 76f8994 commit c06cd1c
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 4 deletions.
54 changes: 53 additions & 1 deletion pkg/lockfile/lockfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
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")
}
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
}
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))
}
13 changes: 12 additions & 1 deletion pkg/lockfile/lockfile_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

0 comments on commit c06cd1c

Please sign in to comment.