1//go:build !windows
2
3package db
4
5import (
6 "errors"
7 "fmt"
8 "os"
9
10 "golang.org/x/sys/unix"
11)
12
13// errLockContended is returned by tryFileLock when the lock is already
14// held by another open file description (typically another process).
15var errLockContended = errors.New("file lock is held by another process")
16
17// tryFileLock takes an exclusive non-blocking BSD flock on path,
18// creating the file if necessary. On success it returns a release
19// function that drops the lock and closes the descriptor. When the
20// lock is contended it returns errLockContended.
21//
22// BSD flock is advisory and per-open-file-description, so it does not
23// interfere with the byte-range locks SQLite itself uses on the same
24// file's siblings (crush.db, crush.db-wal, crush.db-shm). The lock is
25// also released automatically by the kernel when the file descriptor
26// is closed, including on process crash, so we do not need any
27// explicit stale-lock recovery.
28func tryFileLock(path string) (func(), error) {
29 f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600)
30 if err != nil {
31 return nil, fmt.Errorf("open lock file: %w", err)
32 }
33 if err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
34 _ = f.Close()
35 if errors.Is(err, unix.EWOULDBLOCK) {
36 return nil, errLockContended
37 }
38 return nil, fmt.Errorf("flock: %w", err)
39 }
40 return func() {
41 // Closing the descriptor releases the flock atomically.
42 _ = unix.Flock(int(f.Fd()), unix.LOCK_UN)
43 _ = f.Close()
44 }, nil
45}