datadirlock_unix.go

 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}