Skip to content

Commit 9ef783d

Browse files
author
Clément Chigot
committed
Add AIX support with fcntl
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.
1 parent 5135e61 commit 9ef783d

File tree

4 files changed

+293
-3
lines changed

4 files changed

+293
-3
lines changed

flock.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package flock
1919
import (
2020
"context"
2121
"os"
22+
"runtime"
2223
"sync"
2324
"time"
2425
)
@@ -116,7 +117,15 @@ func tryCtx(ctx context.Context, fn func() (bool, error), retryDelay time.Durati
116117
func (f *Flock) setFh() error {
117118
// open a new os.File instance
118119
// create it if it doesn't exist, and open the file read-only.
119-
fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDONLY, os.FileMode(0600))
120+
flags := os.O_CREATE
121+
if runtime.GOOS == "aix" {
122+
// AIX cannot preform write-lock (ie exclusive) on a
123+
// read-only file.
124+
flags |= os.O_RDWR
125+
} else {
126+
flags |= os.O_RDONLY
127+
}
128+
fh, err := os.OpenFile(f.path, flags, os.FileMode(0600))
120129
if err != nil {
121130
return err
122131
}

flock_aix.go

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
// Copyright 2019 Tim Heckman. All rights reserved. Use of this source code is
2+
// governed by the BSD 3-Clause license that can be found in the LICENSE file.
3+
4+
// Copyright 2018 The Go Authors. All rights reserved.
5+
// Use of this source code is governed by a BSD-style
6+
// license that can be found in the LICENSE file.
7+
8+
// This code implements the filelock API using POSIX 'fcntl' locks, which attach
9+
// to an (inode, process) pair rather than a file descriptor. To avoid unlocking
10+
// files prematurely when the same file is opened through different descriptors,
11+
// we allow only one read-lock at a time.
12+
//
13+
// This code is adapted from the Go package:
14+
// cmd/go/internal/lockedfile/internal/filelock
15+
16+
//+build aix
17+
18+
package flock
19+
20+
import (
21+
"errors"
22+
"io"
23+
"os"
24+
"sync"
25+
"syscall"
26+
27+
"golang.org/x/sys/unix"
28+
)
29+
30+
type lockType int16
31+
32+
const (
33+
readLock lockType = unix.F_RDLCK
34+
writeLock lockType = unix.F_WRLCK
35+
)
36+
37+
type inode = uint64
38+
39+
type inodeLock struct {
40+
owner *Flock
41+
queue []<-chan *Flock
42+
}
43+
44+
var (
45+
mu sync.Mutex
46+
inodes = map[*Flock]inode{}
47+
locks = map[inode]inodeLock{}
48+
)
49+
50+
// Lock is a blocking call to try and take an exclusive file lock. It will wait
51+
// until it is able to obtain the exclusive file lock. It's recommended that
52+
// TryLock() be used over this function. This function may block the ability to
53+
// query the current Locked() or RLocked() status due to a RW-mutex lock.
54+
//
55+
// If we are already exclusive-locked, this function short-circuits and returns
56+
// immediately assuming it can take the mutex lock.
57+
//
58+
// If the *Flock has a shared lock (RLock), this may transparently replace the
59+
// shared lock with an exclusive lock on some UNIX-like operating systems. Be
60+
// careful when using exclusive locks in conjunction with shared locks
61+
// (RLock()), because calling Unlock() may accidentally release the exclusive
62+
// lock that was once a shared lock.
63+
func (f *Flock) Lock() error {
64+
return f.lock(&f.l, writeLock)
65+
}
66+
67+
// RLock is a blocking call to try and take a shared file lock. It will wait
68+
// until it is able to obtain the shared file lock. It's recommended that
69+
// TryRLock() be used over this function. This function may block the ability to
70+
// query the current Locked() or RLocked() status due to a RW-mutex lock.
71+
//
72+
// If we are already shared-locked, this function short-circuits and returns
73+
// immediately assuming it can take the mutex lock.
74+
func (f *Flock) RLock() error {
75+
return f.lock(&f.r, readLock)
76+
}
77+
78+
func (f *Flock) lock(locked *bool, flag lockType) error {
79+
f.m.Lock()
80+
defer f.m.Unlock()
81+
82+
if *locked {
83+
return nil
84+
}
85+
86+
if f.fh == nil {
87+
if err := f.setFh(); err != nil {
88+
return err
89+
}
90+
defer f.ensureFhState()
91+
}
92+
93+
if _, err := f.doLock(flag, true); err != nil {
94+
return err
95+
}
96+
97+
*locked = true
98+
return nil
99+
}
100+
101+
func (f *Flock) doLock(lt lockType, blocking bool) (bool, error) {
102+
// POSIX locks apply per inode and process, and the lock for an inode is
103+
// released when *any* descriptor for that inode is closed. So we need to
104+
// synchronize access to each inode internally, and must serialize lock and
105+
// unlock calls that refer to the same inode through different descriptors.
106+
fi, err := f.fh.Stat()
107+
if err != nil {
108+
return false, err
109+
}
110+
ino := inode(fi.Sys().(*syscall.Stat_t).Ino)
111+
112+
mu.Lock()
113+
if i, dup := inodes[f]; dup && i != ino {
114+
mu.Unlock()
115+
return false, &os.PathError{
116+
Path: f.Path(),
117+
Err: errors.New("inode for file changed since last Lock or RLock"),
118+
}
119+
}
120+
121+
inodes[f] = ino
122+
123+
var wait chan *Flock
124+
l := locks[ino]
125+
if l.owner == f {
126+
// This file already owns the lock, but the call may change its lock type.
127+
} else if l.owner == nil {
128+
// No owner: it's ours now.
129+
l.owner = f
130+
} else if !blocking {
131+
// Already owned: cannot take the lock.
132+
mu.Unlock()
133+
return false, nil
134+
} else {
135+
// Already owned: add a channel to wait on.
136+
wait = make(chan *Flock)
137+
l.queue = append(l.queue, wait)
138+
}
139+
locks[ino] = l
140+
mu.Unlock()
141+
142+
if wait != nil {
143+
wait <- f
144+
}
145+
146+
err = setlkw(f.fh.Fd(), lt)
147+
148+
if err != nil {
149+
f.doUnlock()
150+
return false, err
151+
}
152+
153+
return true, nil
154+
}
155+
156+
func (f *Flock) Unlock() error {
157+
f.m.Lock()
158+
defer f.m.Unlock()
159+
160+
// if we aren't locked or if the lockfile instance is nil
161+
// just return a nil error because we are unlocked
162+
if (!f.l && !f.r) || f.fh == nil {
163+
return nil
164+
}
165+
166+
if err := f.doUnlock(); err != nil {
167+
return err
168+
}
169+
170+
f.fh.Close()
171+
172+
f.l = false
173+
f.r = false
174+
f.fh = nil
175+
176+
return nil
177+
}
178+
179+
func (f *Flock) doUnlock() (err error) {
180+
var owner *Flock
181+
mu.Lock()
182+
ino, ok := inodes[f]
183+
if ok {
184+
owner = locks[ino].owner
185+
}
186+
mu.Unlock()
187+
188+
if owner == f {
189+
err = setlkw(f.fh.Fd(), unix.F_UNLCK)
190+
}
191+
192+
mu.Lock()
193+
l := locks[ino]
194+
if len(l.queue) == 0 {
195+
// No waiters: remove the map entry.
196+
delete(locks, ino)
197+
} else {
198+
// The first waiter is sending us their file now.
199+
// Receive it and update the queue.
200+
l.owner = <-l.queue[0]
201+
l.queue = l.queue[1:]
202+
locks[ino] = l
203+
}
204+
delete(inodes, f)
205+
mu.Unlock()
206+
207+
return err
208+
}
209+
210+
// TryLock is the preferred function for taking an exclusive file lock. This
211+
// function takes an RW-mutex lock before it tries to lock the file, so there is
212+
// the possibility that this function may block for a short time if another
213+
// goroutine is trying to take any action.
214+
//
215+
// The actual file lock is non-blocking. If we are unable to get the exclusive
216+
// file lock, the function will return false instead of waiting for the lock. If
217+
// we get the lock, we also set the *Flock instance as being exclusive-locked.
218+
func (f *Flock) TryLock() (bool, error) {
219+
return f.try(&f.l, writeLock)
220+
}
221+
222+
// TryRLock is the preferred function for taking a shared file lock. This
223+
// function takes an RW-mutex lock before it tries to lock the file, so there is
224+
// the possibility that this function may block for a short time if another
225+
// goroutine is trying to take any action.
226+
//
227+
// The actual file lock is non-blocking. If we are unable to get the shared file
228+
// lock, the function will return false instead of waiting for the lock. If we
229+
// get the lock, we also set the *Flock instance as being share-locked.
230+
func (f *Flock) TryRLock() (bool, error) {
231+
return f.try(&f.r, readLock)
232+
}
233+
234+
func (f *Flock) try(locked *bool, flag lockType) (bool, error) {
235+
f.m.Lock()
236+
defer f.m.Unlock()
237+
238+
if *locked {
239+
return true, nil
240+
}
241+
242+
if f.fh == nil {
243+
if err := f.setFh(); err != nil {
244+
return false, err
245+
}
246+
defer f.ensureFhState()
247+
}
248+
249+
haslock, err := f.doLock(flag, false)
250+
if err != nil {
251+
return false, err
252+
}
253+
254+
*locked = haslock
255+
return haslock, nil
256+
}
257+
258+
// setlkw calls FcntlFlock with F_SETLKW for the entire file indicated by fd.
259+
func setlkw(fd uintptr, lt lockType) error {
260+
for {
261+
err := unix.FcntlFlock(fd, unix.F_SETLKW, &unix.Flock_t{
262+
Type: int16(lt),
263+
Whence: io.SeekStart,
264+
Start: 0,
265+
Len: 0, // All bytes.
266+
})
267+
if err != unix.EINTR {
268+
return err
269+
}
270+
}
271+
}

flock_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"io/ioutil"
1111
"os"
12+
"runtime"
1213
"testing"
1314
"time"
1415

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

128138
// make sure we just return false with no error in cases
129139
// where we would have been blocked

flock_unix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by the BSD 3-Clause
33
// license that can be found in the LICENSE file.
44

5-
// +build !windows
5+
// +build !aix,!windows
66

77
package flock
88

0 commit comments

Comments
 (0)