Asterisk file locking in Go
I recently looked into how Asterisk locks files. I’m working on a service written in Go that needs to mess with Asterisk voicemail files. I don’t want to step on Asterisk’s toes (or let it step on mine). This is a first take on a Go library for Asterisk-compatible file/directory locking.
The Go functions largely follow their Asterisk C counterparts.
Go’s golang.org/x/sys/unix
package provides an interface to the low-level operating system primitives.
This Go package doesn’t provide much documentation, but we can refer to the OpenBSD man pages:
// Package asterisk provides functions to interoperate with Asterisk.
package asterisk
import (
"fmt"
"math/rand"
"path"
"time"
"golang.org/x/sys/unix"
)
const maxRetries int = 12
const waitTime time.Duration = time.Millisecond * 500
func init() {
rand.Seed(time.Now().UnixNano())
}
// LockPathLockfile locks a directory with a `.lock` file.
func LockPathLockfile(p string) error {
var err error
fs := path.Join(p, fmt.Sprintf(".lock-%08x", rand.Int31()))
s := path.Join(p, ".lock")
fd, err := unix.Open(fs, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL, 0600)
if err != nil {
return fmt.Errorf("LockPathLockfile can't open file descriptor for %s: %v", fs, err)
}
unix.Close(fd)
for retries := 0; retries < maxRetries; retries++ {
err = unix.Link(fs, s)
if err == nil {
break
}
err = fmt.Errorf("LockPathLockfile can't link lock files %s and %s: %v", fs, s, err)
time.Sleep(waitTime)
}
unix.Unlink(fs)
return err
}
// UnlockPathLockfile unlocks a directory locked by LockPathLockfile.
func UnlockPathLockfile(p string) error {
var err error
s := path.Join(p, ".lock")
err = unix.Unlink(s)
if err != nil {
err = fmt.Errorf("UnlockPathLockfile can't unlink %s: %v", s, err)
}
return err
}
// LockPathFlock locks a directory using the `flock` system call on a `lock` file.
func LockPathFlock(p string) error {
var err error
fs := path.Join(p, "lock")
fd, err := unix.Open(fs, unix.O_WRONLY|unix.O_CREAT, 0600)
if err != nil {
return fmt.Errorf("LockPathFlock can't open file descriptor for %s: %v", fs, err)
}
for retries := 0; retries < maxRetries; retries++ {
err = unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB)
if err != nil {
err = fmt.Errorf("LockPathFlock can't flock file %s: %v", fs, err)
time.Sleep(waitTime)
continue
}
// Avoid the race condition where another prococess flocks our lock file first!
var st0, st1 unix.Stat_t
err = unix.Fstat(fd, &st0)
if err != nil {
return fmt.Errorf("LockPathFlock can't stat file descriptor %v: %v", fd, err)
}
err = unix.Stat(fs, &st1)
if err != nil {
return fmt.Errorf("LockPathFlock can't stat lock file %s: %v", fs, err)
}
if st0.Ino == st1.Ino {
break
}
time.Sleep(waitTime)
}
unix.Close(fd)
return err
}
// UnlockPathFlock unlocks a directory locked by LockPathFlock.
func UnlockPathFlock(p string) error {
var err error
s := path.Join(p, "lock")
fd, err := unix.Open(s, unix.O_WRONLY|unix.O_CREAT, 0600)
if err != nil {
err = fmt.Errorf("UnlockPathFlock can't open lock file descriptor for %s during unlock: %v", s, err)
}
unix.Unlink(s)
if err != nil {
err = fmt.Errorf("UnlockPathFlock can't unlink %s: %v", s, err)
}
unix.Flock(fd, unix.LOCK_UN)
if err != nil {
err = fmt.Errorf("UnlockPathFlock can't un-flock file descriptor %v: %v", fd, err)
}
return err
}
#asterisk #golang