"veyron/lib/filelocker": File locking
This CL defines a file locker that locks a given file
to a particular process. The locker internally uses the Flock
system call.
Change-Id: I844324c9cb537228fcefc5f68eac50edbbdddd86
diff --git a/lib/filelocker/locker.go b/lib/filelocker/locker.go
new file mode 100644
index 0000000..5e6674f
--- /dev/null
+++ b/lib/filelocker/locker.go
@@ -0,0 +1,44 @@
+// package filelocker provides a primitive that lets a process
+// lock access to a file.
+package filelocker
+
+import (
+ "os"
+ "syscall"
+
+ "veyron.io/veyron/veyron2/vlog"
+)
+
+// Unlocker is the interface to unlock a locked file.
+type Unlocker interface {
+ // Unlock unlocks the file.
+ Unlock()
+}
+
+// Lock locks the provided file.
+//
+// If the file is already locked then the calling goroutine
+// blocks until the lock can be acquired.
+//
+// The file must exist otherwise an error is returned.
+func Lock(filepath string) (Unlocker, error) {
+ file, err := os.Open(filepath)
+ if err != nil {
+ return nil, err
+ }
+ if err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil {
+ return nil, err
+ }
+ return &unlocker{file}, nil
+}
+
+// unlocker implements Unlocker.
+type unlocker struct {
+ file *os.File
+}
+
+func (u *unlocker) Unlock() {
+ if err := u.file.Close(); err != nil {
+ vlog.Errorf("failed to unlock file %q: %v", u.file.Name(), err)
+ }
+}
diff --git a/lib/filelocker/locker_test.go b/lib/filelocker/locker_test.go
new file mode 100644
index 0000000..d98e093
--- /dev/null
+++ b/lib/filelocker/locker_test.go
@@ -0,0 +1,129 @@
+package filelocker
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "testing"
+ "time"
+
+ "veyron.io/veyron/veyron/lib/expect"
+ "veyron.io/veyron/veyron/lib/modules"
+)
+
+func init() {
+ modules.RegisterChild("testLockUnlockChild", "", testLockUnlockChild)
+}
+
+func TestHelperProcess(t *testing.T) {
+ modules.DispatchInTest()
+}
+
+func newFile() string {
+ file, err := ioutil.TempFile("", "test_lock_file")
+ if err != nil {
+ panic(err)
+ }
+ defer file.Close()
+ return file.Name()
+}
+
+func grabbedLock(lock <-chan bool) bool {
+ select {
+ case <-lock:
+ return true
+ case <-time.After(100 * time.Millisecond):
+ return false
+ }
+}
+
+func testLockUnlockChild(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+ // Lock the file
+ unlocker, err := Lock(args[1])
+ if err != nil {
+ return fmt.Errorf("Lock failed: %v", err)
+ }
+ fmt.Fprintf(stdout, "locked\n")
+
+ // Wait for message from parent to unlock the file.
+ scanner := bufio.NewScanner(stdin)
+ if scanned := scanner.Scan(); !scanned || (scanned && scanner.Text() != "unlock") {
+ unlocker.Unlock()
+ return fmt.Errorf("unexpected message read from stdout, expected %v", "unlock")
+ }
+ unlocker.Unlock()
+ fmt.Fprintf(stdout, "unlocked\n")
+ return nil
+}
+
+func TestLockUnlockInterProcess(t *testing.T) {
+ filepath := newFile()
+ defer os.Remove(filepath)
+
+ sh := modules.NewShell("testLockUnlockChild")
+ defer sh.Cleanup(os.Stderr, os.Stderr)
+ h, err := sh.Start("testLockUnlockChild", nil, filepath)
+ if err != nil {
+ t.Fatalf("sh.Start failed: %v", err)
+ }
+ s := expect.NewSession(t, h.Stdout(), time.Minute)
+
+ // Test that child grabbed the lock.
+ s.Expect("locked")
+
+ // Test that parent cannot grab the lock, and then send a message
+ // to the child to release the lock.
+ lock := make(chan bool)
+ go func() {
+ unlocker, err := Lock(filepath)
+ if err != nil {
+ t.Fatalf("Lock failed: %v", err)
+ }
+ close(lock)
+ unlocker.Unlock()
+ }()
+ if grabbedLock(lock) {
+ t.Fatal("Parent process unexpectedly grabbed lock before child released it")
+ }
+
+ // Test that the parent can grab the lock after the child has released it.
+ h.Stdin().Write([]byte("unlock\n"))
+ s.Expect("unlocked")
+ if !grabbedLock(lock) {
+ t.Fatal("Parent process failed to grab the lock after child released it")
+ }
+ s.ExpectEOF()
+}
+
+func TestLockUnlockIntraProcess(t *testing.T) {
+ filepath := newFile()
+ defer os.Remove(filepath)
+
+ // Grab the lock within this goroutine and test that
+ // another goroutine blocks when trying to grab the lock.
+ unlocker, err := Lock(filepath)
+ if err != nil {
+ t.Fatalf("Lock failed: %v", err)
+ }
+ lock := make(chan bool)
+ go func() {
+ unlocker, err := Lock(filepath)
+ if err != nil {
+ t.Fatalf("Lock failed: %v", err)
+ }
+ close(lock)
+ unlocker.Unlock()
+ }()
+ if grabbedLock(lock) {
+ unlocker.Unlock()
+ t.Fatal("Another goroutine unexpectedly grabbed lock before this goroutine released it")
+ }
+
+ // Release the lock and test that the goroutine can grab it.
+ unlocker.Unlock()
+ if !grabbedLock(lock) {
+ t.Fatal("Another goroutine failed to grab the lock after this goroutine released it")
+ }
+}