-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
AIX doesn't provide a true flock() syscall. It does exist but it's just a wrapper around fcntl. It doesn't provide safe locks under file descriptors of a same process. The current implementation is based on the file cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go. Using fcntl implementation doesn't allow to have several RLocks at the same time as closing a file descriptor might release the lock if even others RLocks remain attached.
- Loading branch information
Clément Chigot
committed
Aug 26, 2020
1 parent
5135e61
commit 9ef783d
Showing
4 changed files
with
293 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters