lock_unix.go

 1//go:build !windows
 2
 3package lock
 4
 5import (
 6	"context"
 7	"errors"
 8	"fmt"
 9	"os"
10	"time"
11
12	"golang.org/x/sys/unix"
13)
14
15// retrySleep is the interval between non-blocking flock retries in the
16// blocking File path. Small enough that contention resolution feels
17// snappy; large enough that we don't burn a CPU spinning.
18const retrySleep = 100 * time.Millisecond
19
20func lockFile(ctx context.Context, f *os.File) (func(), error) {
21	for {
22		err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB)
23		if err == nil {
24			return func() { _ = unix.Flock(int(f.Fd()), unix.LOCK_UN) }, nil
25		}
26		if !errors.Is(err, unix.EWOULDBLOCK) {
27			return nil, fmt.Errorf("flock: %w", err)
28		}
29		select {
30		case <-ctx.Done():
31			return nil, fmt.Errorf("acquire lock: %w", ctx.Err())
32		case <-time.After(retrySleep):
33		}
34	}
35}
36
37func tryLockFile(f *os.File) (func(), error) {
38	if err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
39		if errors.Is(err, unix.EWOULDBLOCK) {
40			return nil, ErrContended
41		}
42		return nil, fmt.Errorf("flock: %w", err)
43	}
44	return func() { _ = unix.Flock(int(f.Fd()), unix.LOCK_UN) }, nil
45}