Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AIX support with fcntl #40

Merged
merged 1 commit into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion flock.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package flock
import (
"context"
"os"
"runtime"
"sync"
"time"
)
Expand Down Expand Up @@ -116,7 +117,15 @@ func tryCtx(ctx context.Context, fn func() (bool, error), retryDelay time.Durati
func (f *Flock) setFh() error {
// open a new os.File instance
// create it if it doesn't exist, and open the file read-only.
fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDONLY, os.FileMode(0600))
flags := os.O_CREATE
if runtime.GOOS == "aix" {
// AIX cannot preform write-lock (ie exclusive) on a
// read-only file.
flags |= os.O_RDWR
} else {
flags |= os.O_RDONLY
}
fh, err := os.OpenFile(f.path, flags, os.FileMode(0600))
if err != nil {
return err
}
Expand Down
271 changes: 271 additions & 0 deletions flock_aix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
// Copyright 2019 Tim Heckman. All rights reserved. Use of this source code is
// governed by the BSD 3-Clause license that can be found in the LICENSE file.

// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This code implements the filelock API using POSIX 'fcntl' locks, which attach
// to an (inode, process) pair rather than a file descriptor. To avoid unlocking
// files prematurely when the same file is opened through different descriptors,
// we allow only one read-lock at a time.
//
// This code is adapted from the Go package:
// cmd/go/internal/lockedfile/internal/filelock

//+build aix

package flock

import (
"errors"
"io"
"os"
"sync"
"syscall"

"golang.org/x/sys/unix"
)

type lockType int16

const (
readLock lockType = unix.F_RDLCK
writeLock lockType = unix.F_WRLCK
)

type inode = uint64

type inodeLock struct {
owner *Flock
queue []<-chan *Flock
}

var (
mu sync.Mutex
inodes = map[*Flock]inode{}
locks = map[inode]inodeLock{}
)

// Lock is a blocking call to try and take an exclusive file lock. It will wait
// until it is able to obtain the exclusive file lock. It's recommended that
// TryLock() be used over this function. This function may block the ability to
// query the current Locked() or RLocked() status due to a RW-mutex lock.
//
// If we are already exclusive-locked, this function short-circuits and returns
// immediately assuming it can take the mutex lock.
//
// If the *Flock has a shared lock (RLock), this may transparently replace the
// shared lock with an exclusive lock on some UNIX-like operating systems. Be
// careful when using exclusive locks in conjunction with shared locks
// (RLock()), because calling Unlock() may accidentally release the exclusive
// lock that was once a shared lock.
func (f *Flock) Lock() error {
return f.lock(&f.l, writeLock)
}

// RLock is a blocking call to try and take a shared file lock. It will wait
// until it is able to obtain the shared file lock. It's recommended that
// TryRLock() be used over this function. This function may block the ability to
// query the current Locked() or RLocked() status due to a RW-mutex lock.
//
// If we are already shared-locked, this function short-circuits and returns
// immediately assuming it can take the mutex lock.
func (f *Flock) RLock() error {
return f.lock(&f.r, readLock)
}

func (f *Flock) lock(locked *bool, flag lockType) error {
f.m.Lock()
defer f.m.Unlock()

if *locked {
return nil
}

if f.fh == nil {
if err := f.setFh(); err != nil {
return err
}
defer f.ensureFhState()
}

if _, err := f.doLock(flag, true); err != nil {
return err
}

*locked = true
return nil
}

func (f *Flock) doLock(lt lockType, blocking bool) (bool, error) {
// POSIX locks apply per inode and process, and the lock for an inode is
// released when *any* descriptor for that inode is closed. So we need to
// synchronize access to each inode internally, and must serialize lock and
// unlock calls that refer to the same inode through different descriptors.
fi, err := f.fh.Stat()
if err != nil {
return false, err
}
ino := inode(fi.Sys().(*syscall.Stat_t).Ino)

mu.Lock()
if i, dup := inodes[f]; dup && i != ino {
mu.Unlock()
return false, &os.PathError{
Path: f.Path(),
Err: errors.New("inode for file changed since last Lock or RLock"),
}
}

inodes[f] = ino

var wait chan *Flock
l := locks[ino]
if l.owner == f {
// This file already owns the lock, but the call may change its lock type.
} else if l.owner == nil {
// No owner: it's ours now.
l.owner = f
} else if !blocking {
// Already owned: cannot take the lock.
mu.Unlock()
return false, nil
} else {
// Already owned: add a channel to wait on.
wait = make(chan *Flock)
l.queue = append(l.queue, wait)
}
locks[ino] = l
mu.Unlock()

if wait != nil {
wait <- f
}

err = setlkw(f.fh.Fd(), lt)

if err != nil {
f.doUnlock()
return false, err
}

return true, nil
}

func (f *Flock) Unlock() error {
f.m.Lock()
defer f.m.Unlock()

// if we aren't locked or if the lockfile instance is nil
// just return a nil error because we are unlocked
if (!f.l && !f.r) || f.fh == nil {
return nil
}

if err := f.doUnlock(); err != nil {
return err
}

f.fh.Close()

f.l = false
f.r = false
f.fh = nil

return nil
}

func (f *Flock) doUnlock() (err error) {
var owner *Flock
mu.Lock()
ino, ok := inodes[f]
if ok {
owner = locks[ino].owner
}
mu.Unlock()

if owner == f {
err = setlkw(f.fh.Fd(), unix.F_UNLCK)
}

mu.Lock()
l := locks[ino]
if len(l.queue) == 0 {
// No waiters: remove the map entry.
delete(locks, ino)
} else {
// The first waiter is sending us their file now.
// Receive it and update the queue.
l.owner = <-l.queue[0]
l.queue = l.queue[1:]
locks[ino] = l
}
delete(inodes, f)
mu.Unlock()

return err
}

// TryLock is the preferred function for taking an exclusive file lock. This
// function takes an RW-mutex lock before it tries to lock the file, so there is
// the possibility that this function may block for a short time if another
// goroutine is trying to take any action.
//
// The actual file lock is non-blocking. If we are unable to get the exclusive
// file lock, the function will return false instead of waiting for the lock. If
// we get the lock, we also set the *Flock instance as being exclusive-locked.
func (f *Flock) TryLock() (bool, error) {
return f.try(&f.l, writeLock)
}

// TryRLock is the preferred function for taking a shared file lock. This
// function takes an RW-mutex lock before it tries to lock the file, so there is
// the possibility that this function may block for a short time if another
// goroutine is trying to take any action.
//
// The actual file lock is non-blocking. If we are unable to get the shared file
// lock, the function will return false instead of waiting for the lock. If we
// get the lock, we also set the *Flock instance as being share-locked.
func (f *Flock) TryRLock() (bool, error) {
return f.try(&f.r, readLock)
}

func (f *Flock) try(locked *bool, flag lockType) (bool, error) {
f.m.Lock()
defer f.m.Unlock()

if *locked {
return true, nil
}

if f.fh == nil {
if err := f.setFh(); err != nil {
return false, err
}
defer f.ensureFhState()
}

haslock, err := f.doLock(flag, false)
if err != nil {
return false, err
}

*locked = haslock
return haslock, nil
}

// setlkw calls FcntlFlock with F_SETLKW for the entire file indicated by fd.
func setlkw(fd uintptr, lt lockType) error {
for {
err := unix.FcntlFlock(fd, unix.F_SETLKW, &unix.Flock_t{
Type: int16(lt),
Whence: io.SeekStart,
Start: 0,
Len: 0, // All bytes.
})
if err != unix.EINTR {
return err
}
}
}
12 changes: 11 additions & 1 deletion flock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"io/ioutil"
"os"
"runtime"
"testing"
"time"

Expand Down Expand Up @@ -123,7 +124,16 @@ func (t *TestSuite) TestFlock_TryRLock(c *C) {
flock2 := flock.New(t.path)
locked, err = flock2.TryRLock()
c.Assert(err, IsNil)
c.Check(locked, Equals, true)
if runtime.GOOS == "aix" {
// When using POSIX locks, we can't safely read-lock the same
// inode through two different descriptors at the same time:
// when the first descriptor is closed, the second descriptor
// would still be open but silently unlocked. So a second
// TryRLock must return false.
c.Check(locked, Equals, false)
} else {
c.Check(locked, Equals, true)
}

// make sure we just return false with no error in cases
// where we would have been blocked
Expand Down
2 changes: 1 addition & 1 deletion flock_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by the BSD 3-Clause
// license that can be found in the LICENSE file.

// +build !windows
// +build !aix,!windows

package flock

Expand Down