Initial commit.
This commit is contained in:
77
flock.go
Normal file
77
flock.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// The flock package provides a file-system mediated locking mechanism on linux
|
||||||
|
// using the `flock` system call.
|
||||||
|
|
||||||
|
package flock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrLocked = errors.New("locked")
|
||||||
|
|
||||||
|
// Lock gets an exclusive lock on the file at the given path. If the file
|
||||||
|
// doesn't exist, it's created.
|
||||||
|
func Lock(path string) (*os.File, error) {
|
||||||
|
return lock(path, syscall.LOCK_EX)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryLock attempts to lock the file at path. It will return ErrLocked if the
|
||||||
|
// file is already locked.
|
||||||
|
func TryLock(path string) (*os.File, error) {
|
||||||
|
return lock(path, syscall.LOCK_EX|syscall.LOCK_NB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockFile is like Lock, but takes an *os.File.
|
||||||
|
func LockFile(f *os.File) error {
|
||||||
|
return lockFile(f, syscall.LOCK_EX)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryLockFile is like TryLock, but takes an *os.File.
|
||||||
|
func TryLockFile(f *os.File) error {
|
||||||
|
return lockFile(f, syscall.LOCK_EX|syscall.LOCK_NB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lockFile(f *os.File, flags int) error {
|
||||||
|
if err := flock(int(f.Fd()), flags); err != nil {
|
||||||
|
if flags&syscall.LOCK_NB != 0 && errors.Is(err, syscall.EAGAIN) {
|
||||||
|
return ErrLocked
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flock(fd int, how int) error {
|
||||||
|
_, _, e1 := syscall.Syscall(syscall.SYS_FLOCK, uintptr(fd), uintptr(how), 0)
|
||||||
|
if e1 != 0 {
|
||||||
|
return syscall.Errno(e1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lock(path string, flags int) (*os.File, error) {
|
||||||
|
perm := os.O_CREATE | os.O_RDWR
|
||||||
|
f, err := os.OpenFile(path, perm, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = lockFile(f, flags)
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
f = nil
|
||||||
|
}
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock releases the lock acquired via the Lock function.
|
||||||
|
func Unlock(f *os.File) error {
|
||||||
|
if err := flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
111
flock_test.go
Normal file
111
flock_test.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package flock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLock_basic(t *testing.T) {
|
||||||
|
const path = "/tmp/fsutil-test-lock"
|
||||||
|
|
||||||
|
f, err := Lock(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// locked is closed once the goroutine acquires the lock.
|
||||||
|
locked := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
f2, err := Lock(path) // blocks until f is unlocked
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
close(locked)
|
||||||
|
Unlock(f2)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Goroutine should be blocked while we hold the lock.
|
||||||
|
select {
|
||||||
|
case <-locked:
|
||||||
|
t.Fatal("goroutine acquired lock while we hold it")
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
Unlock(f)
|
||||||
|
|
||||||
|
// Now the goroutine should unblock and acquire the lock.
|
||||||
|
select {
|
||||||
|
case <-locked:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("goroutine never acquired lock after release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLock_badPath(t *testing.T) {
|
||||||
|
_, err := Lock("./dne/file.lock")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTryLock(t *testing.T) {
|
||||||
|
const path = "/tmp/fsutil-test-lock"
|
||||||
|
|
||||||
|
f, err := TryLock(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer Unlock(f)
|
||||||
|
|
||||||
|
// While f holds the lock, TryLock should return ErrLocked.
|
||||||
|
_, err = TryLock(path)
|
||||||
|
if !errors.Is(err, ErrLocked) {
|
||||||
|
t.Fatalf("expected ErrLocked, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After unlocking, TryLock should succeed.
|
||||||
|
if err := Unlock(f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f2, err := TryLock(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected lock after unlock, got %v", err)
|
||||||
|
}
|
||||||
|
Unlock(f2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTryLockFile(t *testing.T) {
|
||||||
|
const path = "/tmp/fsutil-test-lockfile"
|
||||||
|
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := TryLockFile(f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f2, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f2.Close()
|
||||||
|
|
||||||
|
if err := TryLockFile(f2); !errors.Is(err, ErrLocked) {
|
||||||
|
t.Fatalf("expected ErrLocked, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Unlock(f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := TryLockFile(f2); err != nil {
|
||||||
|
t.Fatalf("expected lock after unlock, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user