Skip to content

Commit a0a31b7

Browse files
authored
fix: update fnctl implementation (#84)
1 parent e49f343 commit a0a31b7

File tree

1 file changed

+72
-4
lines changed

1 file changed

+72
-4
lines changed

flock_unix_fcntl.go

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
// To avoid unlocking files prematurely when the same file is opened through different descriptors,
1313
// we allow only one read-lock at a time.
1414
//
15-
// This code is adapted from the Go package (go.12):
16-
// https://github.com/golang/go/blob/release-branch.go1.12/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go
15+
// This code is adapted from the Go package (go.22):
16+
// https://github.com/golang/go/blob/release-branch.go1.22/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go
1717

1818
//go:build aix || (solaris && !illumos)
1919

@@ -23,8 +23,10 @@ import (
2323
"errors"
2424
"io"
2525
"io/fs"
26+
"math/rand"
2627
"sync"
2728
"syscall"
29+
"time"
2830

2931
"golang.org/x/sys/unix"
3032
)
@@ -183,15 +185,81 @@ func (f *Flock) doLock(cmd cmdType, lt lockType, blocking bool) (bool, error) {
183185
wait <- f
184186
}
185187

186-
err = setlkw(f.fh.Fd(), cmd, lt)
188+
// Spurious EDEADLK errors arise on platforms that compute deadlock graphs at
189+
// the process, rather than thread, level. Consider processes P and Q, with
190+
// threads P.1, P.2, and Q.3. The following trace is NOT a deadlock, but will be
191+
// reported as a deadlock on systems that consider only process granularity:
192+
//
193+
// P.1 locks file A.
194+
// Q.3 locks file B.
195+
// Q.3 blocks on file A.
196+
// P.2 blocks on file B. (This is erroneously reported as a deadlock.)
197+
// P.1 unlocks file A.
198+
// Q.3 unblocks and locks file A.
199+
// Q.3 unlocks files A and B.
200+
// P.2 unblocks and locks file B.
201+
// P.2 unlocks file B.
202+
//
203+
// These spurious errors were observed in practice on AIX and Solaris in
204+
// cmd/go: see https://golang.org/issue/32817.
205+
//
206+
// We work around this bug by treating EDEADLK as always spurious. If there
207+
// really is a lock-ordering bug between the interacting processes, it will
208+
// become a livelock instead, but that's not appreciably worse than if we had
209+
// a proper flock implementation (which generally does not even attempt to
210+
// diagnose deadlocks).
211+
//
212+
// In the above example, that changes the trace to:
213+
//
214+
// P.1 locks file A.
215+
// Q.3 locks file B.
216+
// Q.3 blocks on file A.
217+
// P.2 spuriously fails to lock file B and goes to sleep.
218+
// P.1 unlocks file A.
219+
// Q.3 unblocks and locks file A.
220+
// Q.3 unlocks files A and B.
221+
// P.2 wakes up and locks file B.
222+
// P.2 unlocks file B.
223+
//
224+
// We know that the retry loop will not introduce a *spurious* livelock
225+
// because, according to the POSIX specification, EDEADLK is only to be
226+
// returned when “the lock is blocked by a lock from another process”.
227+
// If that process is blocked on some lock that we are holding, then the
228+
// resulting livelock is due to a real deadlock (and would manifest as such
229+
// when using, for example, the flock implementation of this package).
230+
// If the other process is *not* blocked on some other lock that we are
231+
// holding, then it will eventually release the requested lock.
232+
233+
nextSleep := 1 * time.Millisecond
234+
const maxSleep = 500 * time.Millisecond
235+
for {
236+
err = setlkw(f.fh.Fd(), cmd, lt)
237+
if !errors.Is(err, syscall.EDEADLK) {
238+
break
239+
}
240+
241+
time.Sleep(nextSleep)
242+
243+
nextSleep += nextSleep
244+
if nextSleep > maxSleep {
245+
nextSleep = maxSleep
246+
}
247+
// Apply 10% jitter to avoid synchronizing collisions when we finally unblock.
248+
nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
249+
}
250+
187251
if err != nil {
188252
f.doUnlock()
189253

190254
if cmd == tryLock && errors.Is(err, unix.EACCES) {
191255
return false, nil
192256
}
193257

194-
return false, err
258+
return false, &fs.PathError{
259+
Op: lt.String(),
260+
Path: f.Path(),
261+
Err: err,
262+
}
195263
}
196264

197265
return true, nil

0 commit comments

Comments
 (0)