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