forked from gofrs/flock
-
Notifications
You must be signed in to change notification settings - Fork 0
/
flock_unix_fcntl.go
393 lines (323 loc) · 11 KB
/
flock_unix_fcntl.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
// Copyright 2015 Tim Heckman. All rights reserved.
// Copyright 2018-2024 The Gofrs. 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 (go.22):
// https://github.com/golang/go/blob/release-branch.go1.22/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go
//go:build aix || (solaris && !illumos)
package flock
import (
"errors"
"io"
"io/fs"
"math/rand"
"sync"
"syscall"
"time"
"golang.org/x/sys/unix"
)
// https://github.com/golang/go/blob/09aeb6e33ab426eff4676a3baf694d5a3019e9fc/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go#L28
type lockType int16
// String returns the name of the function corresponding to lt
// (Lock, RLock, or Unlock).
// https://github.com/golang/go/blob/09aeb6e33ab426eff4676a3baf694d5a3019e9fc/src/cmd/go/internal/lockedfile/internal/filelock/filelock.go#L67
func (lt lockType) String() string {
switch lt {
case readLock:
return "RLock"
case writeLock:
return "Lock"
default:
return "Unlock"
}
}
// https://github.com/golang/go/blob/09aeb6e33ab426eff4676a3baf694d5a3019e9fc/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go#L30-L33
const (
readLock lockType = unix.F_RDLCK
writeLock lockType = unix.F_WRLCK
)
// https://github.com/golang/go/blob/09aeb6e33ab426eff4676a3baf694d5a3019e9fc/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go#L35
type inode = uint64
// https://github.com/golang/go/blob/09aeb6e33ab426eff4676a3baf694d5a3019e9fc/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go#L37-L40
type inodeLock struct {
owner *Flock
queue []<-chan *Flock
}
type cmdType int
const (
tryLock cmdType = unix.F_SETLK
waitLock cmdType = unix.F_SETLKW
)
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(f.flag); err != nil {
return err
}
defer f.ensureFhState()
}
_, err := f.doLock(waitLock, flag, true)
if err != nil {
return err
}
*locked = true
return nil
}
// https://github.com/golang/go/blob/09aeb6e33ab426eff4676a3baf694d5a3019e9fc/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go#L48
func (f *Flock) doLock(cmd cmdType, 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
}
// Note(ldez): don't replace `syscall.Stat_t` by `unix.Stat_t` because `FileInfo.Sys()` returns `syscall.Stat_t`
ino := fi.Sys().(*syscall.Stat_t).Ino
mu.Lock()
if i, dup := inodes[f]; dup && i != ino {
mu.Unlock()
return false, &fs.PathError{
Op: lt.String(),
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]
switch {
case l.owner == f:
// This file already owns the lock, but the call may change its lock type.
case l.owner == nil:
// No owner: it's ours now.
l.owner = f
case !blocking:
// Already owned: cannot take the lock.
mu.Unlock()
return false, nil
default:
// 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
}
// Spurious EDEADLK errors arise on platforms that compute deadlock graphs at
// the process, rather than thread, level. Consider processes P and Q, with
// threads P.1, P.2, and Q.3. The following trace is NOT a deadlock, but will be
// reported as a deadlock on systems that consider only process granularity:
//
// P.1 locks file A.
// Q.3 locks file B.
// Q.3 blocks on file A.
// P.2 blocks on file B. (This is erroneously reported as a deadlock.)
// P.1 unlocks file A.
// Q.3 unblocks and locks file A.
// Q.3 unlocks files A and B.
// P.2 unblocks and locks file B.
// P.2 unlocks file B.
//
// These spurious errors were observed in practice on AIX and Solaris in
// cmd/go: see https://golang.org/issue/32817.
//
// We work around this bug by treating EDEADLK as always spurious. If there
// really is a lock-ordering bug between the interacting processes, it will
// become a livelock instead, but that's not appreciably worse than if we had
// a proper flock implementation (which generally does not even attempt to
// diagnose deadlocks).
//
// In the above example, that changes the trace to:
//
// P.1 locks file A.
// Q.3 locks file B.
// Q.3 blocks on file A.
// P.2 spuriously fails to lock file B and goes to sleep.
// P.1 unlocks file A.
// Q.3 unblocks and locks file A.
// Q.3 unlocks files A and B.
// P.2 wakes up and locks file B.
// P.2 unlocks file B.
//
// We know that the retry loop will not introduce a *spurious* livelock
// because, according to the POSIX specification, EDEADLK is only to be
// returned when “the lock is blocked by a lock from another process”.
// If that process is blocked on some lock that we are holding, then the
// resulting livelock is due to a real deadlock (and would manifest as such
// when using, for example, the flock implementation of this package).
// If the other process is *not* blocked on some other lock that we are
// holding, then it will eventually release the requested lock.
nextSleep := 1 * time.Millisecond
const maxSleep = 500 * time.Millisecond
for {
err = setlkw(f.fh.Fd(), cmd, lt)
if !errors.Is(err, unix.EDEADLK) {
break
}
time.Sleep(nextSleep)
nextSleep += nextSleep
if nextSleep > maxSleep {
nextSleep = maxSleep
}
// Apply 10% jitter to avoid synchronizing collisions when we finally unblock.
nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
}
if err != nil {
f.doUnlock()
if cmd == tryLock && errors.Is(err, unix.EACCES) {
return false, nil
}
return false, &fs.PathError{
Op: lt.String(),
Path: f.Path(),
Err: 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.reset()
return nil
}
// https://github.com/golang/go/blob/09aeb6e33ab426eff4676a3baf694d5a3019e9fc/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go#L163
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(), waitLock, 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(f.flag); err != nil {
return false, err
}
defer f.ensureFhState()
}
hasLock, err := f.doLock(tryLock, flag, false)
if err != nil {
return false, err
}
*locked = hasLock
return hasLock, nil
}
// setlkw calls FcntlFlock with cmd for the entire file indicated by fd.
// https://github.com/golang/go/blob/09aeb6e33ab426eff4676a3baf694d5a3019e9fc/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go#L198
func setlkw(fd uintptr, cmd cmdType, lt lockType) error {
for {
err := unix.FcntlFlock(fd, int(cmd), &unix.Flock_t{
Type: int16(lt),
Whence: io.SeekStart,
Start: 0,
Len: 0, // All bytes.
})
if !errors.Is(err, unix.EINTR) {
return err
}
}
}