paulgorman.org

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

⬅ Older Post Newer Post ➡