pkg/fsync/fsync_unix.go
Nalin Dahyabhai bd3b44be3e Add a file-backed read-write mutex implementation
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)
2016-07-15 17:21:14 -04:00

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
}