The fsync package provides Mutex and RWMutex types which aim to provide the same semantics as their namesakes in the sync package, extending that locking across processes by using file locks and randomly-generated identifiers to allow processes to determine whether or not they were the last to modify a resource that's protected by the lock. Signed-off-by: Nalin Dahyabhai <nalin@redhat.com> (github: nalind)
164 lines
3.7 KiB
Go
164 lines
3.7 KiB
Go
// +build linux
|
|
|
|
package fsync
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"path/filepath"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
)
|
|
|
|
type holderID [16]byte
|
|
|
|
// Mutex represents an RWMutex which synchronizes its state with a file lock,
|
|
// allowing two process which use the same lock file to share reading and
|
|
// writing locks.
|
|
type Mutex struct {
|
|
rw sync.RWMutex
|
|
m sync.Mutex
|
|
lockfile string
|
|
locked bool
|
|
lockfd int
|
|
readers, writers int
|
|
us, holder holderID
|
|
}
|
|
|
|
// lockop obtains or clears a file lock on the specified descriptor, blocking
|
|
// and retrying indefinitely if it fails to do so for any reason.
|
|
func lockop(name string, fd, lockop int) {
|
|
err := syscall.Flock(fd, lockop)
|
|
for err != nil {
|
|
logrus.Debugf("waiting for file lock on %s", name)
|
|
time.Sleep(100 * time.Millisecond)
|
|
err = syscall.Flock(fd, lockop)
|
|
if err == nil {
|
|
logrus.Debugf("obtained file lock on %s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RLock obtains a read lock on the Mutex.
|
|
func (m *Mutex) RLock() {
|
|
m.rw.RLock()
|
|
m.m.Lock()
|
|
defer m.m.Unlock()
|
|
m.readers++
|
|
if m.readers == 1 {
|
|
lockop(m.lockfile, m.lockfd, syscall.LOCK_SH)
|
|
m.locked = true
|
|
}
|
|
}
|
|
|
|
// RUnlock releases a read lock on the Mutex.
|
|
func (m *Mutex) RUnlock() {
|
|
m.rw.RUnlock()
|
|
m.m.Lock()
|
|
defer m.m.Unlock()
|
|
m.readers--
|
|
if m.readers == 0 {
|
|
if !m.locked {
|
|
panic(fmt.Sprintf("attempted to unlock %s while not locked", m.lockfile))
|
|
}
|
|
lockop(m.lockfile, m.lockfd, syscall.LOCK_UN)
|
|
m.locked = false
|
|
}
|
|
}
|
|
|
|
// Lock obtains a write lock on the Mutex.
|
|
func (m *Mutex) Lock() {
|
|
m.rw.Lock()
|
|
m.m.Lock()
|
|
defer m.m.Unlock()
|
|
m.writers++
|
|
if m.writers == 1 {
|
|
lockop(m.lockfile, m.lockfd, syscall.LOCK_EX)
|
|
m.locked = true
|
|
}
|
|
}
|
|
|
|
// Unlock releases a write lock on the Mutex.
|
|
func (m *Mutex) Unlock() {
|
|
m.rw.Unlock()
|
|
m.m.Lock()
|
|
defer m.m.Unlock()
|
|
m.writers--
|
|
if m.writers == 0 {
|
|
if !m.locked {
|
|
panic(fmt.Sprintf("attempted to unlock %s while not locked", m.lockfile))
|
|
}
|
|
lockop(m.lockfile, m.lockfd, syscall.LOCK_UN)
|
|
m.locked = false
|
|
}
|
|
}
|
|
|
|
// Touch updates the contents of the lock file to hold our ID, as an indicator
|
|
// that other processes can consult to see that it was this process that last
|
|
// modified the data that's protected by the lock. Should only be called with
|
|
// a write lock held.
|
|
func (m *Mutex) Touch() error {
|
|
n, err := rand.Read(m.us[:])
|
|
if n != len(m.us) || err != nil {
|
|
return err
|
|
}
|
|
if _, err := syscall.Pwrite(m.lockfd, m.us[:], 0); err != nil {
|
|
return err
|
|
}
|
|
m.holder = m.us
|
|
return nil
|
|
}
|
|
|
|
// Updated tells us if the last recorded ID in the lock file is different from
|
|
// ours and has changed since we last looked at it. Should only be called with
|
|
// either a read or write lock held.
|
|
func (m *Mutex) Updated() bool {
|
|
var holder holderID
|
|
if n, err := syscall.Pread(m.lockfd, holder[:], 0); err != nil && n != 0 {
|
|
return true
|
|
}
|
|
updated := bytes.Compare(holder[:], m.us[:]) != 0 &&
|
|
bytes.Compare(holder[:], m.holder[:]) != 0
|
|
m.holder = holder
|
|
return updated
|
|
}
|
|
|
|
// get initializes a mutex, opening the lockfile.
|
|
func get(lockfile string) (*Mutex, error) {
|
|
var noHolder, us holderID
|
|
|
|
lockMgr.m.Lock()
|
|
defer lockMgr.m.Unlock()
|
|
|
|
if bytes.Compare(us[:], noHolder[:]) == 0 {
|
|
n, err := rand.Read(us[:])
|
|
if n != len(us) || err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
name, err := filepath.Abs(lockfile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
name = filepath.Clean(name)
|
|
fl, ok := lockMgr.locks[name]
|
|
if !ok {
|
|
lockfd, err := syscall.Open(name, syscall.O_CREAT|syscall.O_RDWR, syscall.S_IRUSR|syscall.S_IWUSR)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
syscall.CloseOnExec(lockfd)
|
|
fl = &Mutex{
|
|
lockfd: lockfd,
|
|
lockfile: name,
|
|
us: us,
|
|
}
|
|
lockMgr.locks[name] = fl
|
|
}
|
|
return fl, nil
|
|
}
|