Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6628f26bf1 |
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