mirror of
https://github.com/taigrr/bitcask
synced 2025-01-18 04:03:17 -08:00
* add failing test case to highlight the race condition on bug note : the test "TestLock" is non deterministic, its outcome depends on the sequence of instructions yielded by the go scheduler on each run. There are two values, "goroutines" and "succesfulLockCount", which can be edited to see how the test performs. With the committed value, resp "20" and "50", I had a 100% failure on my local machine, running linux (Ubuntu 20.04). Sample test output : $ go test . -run TestLock --- FAIL: TestLock (0.17s) lock_test.go:91: [runner 14] lockCounter was > 1 on 5 occasions, max seen value was 2 lock_test.go:91: [runner 03] lockCounter was > 1 on 2 occasions, max seen value was 3 lock_test.go:91: [runner 02] lockCounter was > 1 on 3 occasions, max seen value was 3 lock_test.go:91: [runner 00] lockCounter was > 1 on 1 occasions, max seen value was 2 lock_test.go:91: [runner 12] lockCounter was > 1 on 7 occasions, max seen value was 3 lock_test.go:91: [runner 01] lockCounter was > 1 on 8 occasions, max seen value was 2 lock_test.go:91: [runner 04] lockCounter was > 1 on 6 occasions, max seen value was 4 lock_test.go:91: [runner 13] lockCounter was > 1 on 1 occasions, max seen value was 2 lock_test.go:91: [runner 17] lockCounter was > 1 on 4 occasions, max seen value was 2 lock_test.go:91: [runner 10] lockCounter was > 1 on 3 occasions, max seen value was 2 lock_test.go:91: [runner 08] lockCounter was > 1 on 6 occasions, max seen value was 2 lock_test.go:91: [runner 09] lockCounter was > 1 on 4 occasions, max seen value was 2 lock_test.go:91: [runner 05] lockCounter was > 1 on 1 occasions, max seen value was 2 lock_test.go:91: [runner 19] lockCounter was > 1 on 3 occasions, max seen value was 3 lock_test.go:91: [runner 07] lockCounter was > 1 on 4 occasions, max seen value was 3 lock_test.go:91: [runner 11] lockCounter was > 1 on 9 occasions, max seen value was 2 lock_test.go:91: [runner 15] lockCounter was > 1 on 1 occasions, max seen value was 3 lock_test.go:91: [runner 16] lockCounter was > 1 on 1 occasions, max seen value was 3 FAIL FAIL github.com/prologic/bitcask 0.176s FAIL * flock: create a wrapper module, local to bitcask, around gofrs.Flock the racy TestLock has been moved to bitcask/flock * flock: add test for expected regular locking behavior * flock: replace gofrs/flock with local implementation * update go.sum * Add build constraint for flock_unix.go Co-authored-by: James Mills <prologic@shortcircuit.net.au>
98 lines
1.9 KiB
Go
98 lines
1.9 KiB
Go
package flock
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"sync"
|
|
)
|
|
|
|
type Flock struct {
|
|
path string
|
|
m sync.Mutex
|
|
fh *os.File
|
|
}
|
|
|
|
var (
|
|
ErrAlreadyLocked = errors.New("Double lock: already own the lock")
|
|
ErrLockFailed = errors.New("Could not acquire lock")
|
|
ErrLockNotHeld = errors.New("Could not unlock, lock is not held")
|
|
|
|
ErrInodeChangedAtPath = errors.New("Inode changed at path")
|
|
)
|
|
|
|
// New returns a new instance of *Flock. The only parameter
|
|
// it takes is the path to the desired lockfile.
|
|
func New(path string) *Flock {
|
|
return &Flock{path: path}
|
|
}
|
|
|
|
// Path returns the file path linked to this lock.
|
|
func (f *Flock) Path() string {
|
|
return f.path
|
|
}
|
|
|
|
// Lock will acquire the lock. This function may block indefinitely if some other process holds the lock. For a non-blocking version, see Flock.TryLock().
|
|
func (f *Flock) Lock() error {
|
|
f.m.Lock()
|
|
defer f.m.Unlock()
|
|
|
|
if f.fh != nil {
|
|
return ErrAlreadyLocked
|
|
}
|
|
|
|
var fh *os.File
|
|
|
|
fh, err := lock_sys(f.path, false)
|
|
// treat "ErrInodeChangedAtPath" as "some other process holds the lock, retry locking"
|
|
for err == ErrInodeChangedAtPath {
|
|
fh, err = lock_sys(f.path, false)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fh == nil {
|
|
return ErrLockFailed
|
|
}
|
|
|
|
f.fh = fh
|
|
return nil
|
|
}
|
|
|
|
// TryLock will try to acquire the lock, and returns immediately if the lock is already owned by another process.
|
|
func (f *Flock) TryLock() (bool, error) {
|
|
f.m.Lock()
|
|
defer f.m.Unlock()
|
|
|
|
if f.fh != nil {
|
|
return false, ErrAlreadyLocked
|
|
}
|
|
|
|
fh, err := lock_sys(f.path, true)
|
|
if err != nil {
|
|
return false, ErrLockFailed
|
|
}
|
|
|
|
f.fh = fh
|
|
return true, nil
|
|
}
|
|
|
|
// Unlock removes the lock file from disk and releases the lock.
|
|
// Whatever the result of `.Unlock()`, the caller must assume that it does not hold the lock anymore.
|
|
func (f *Flock) Unlock() error {
|
|
f.m.Lock()
|
|
defer f.m.Unlock()
|
|
|
|
if f.fh == nil {
|
|
return ErrLockNotHeld
|
|
}
|
|
|
|
err1 := rm_if_match(f.fh, f.path)
|
|
err2 := f.fh.Close()
|
|
|
|
if err1 != nil {
|
|
return err1
|
|
}
|
|
return err2
|
|
}
|