1// Package lock provides cross-process advisory file locking.
2//
3// File acquires an exclusive lock on the file at path, blocking until
4// the context is cancelled (or its deadline elapses). TryFile does the
5// same but returns ErrContended immediately if the lock is already
6// held. In both cases the returned release function drops the lock and
7// closes the underlying file descriptor.
8//
9// The lock is released automatically by the kernel on process
10// termination (including crash), so no stale-lock recovery is needed.
11//
12// The lock file at path is created if it does not exist. It is never
13// unlinked — flock is keyed by inode, not path, and unlinking could
14// create a window where two processes lock different inodes at the
15// same path.
16//
17// This is the canonical file-locking helper for Crush. Callers should
18// prefer it over rolling their own platform-specific code.
19package lock
20
21import (
22 "context"
23 "errors"
24 "fmt"
25 "os"
26)
27
28// ErrContended is returned by TryFile when the lock is already held by
29// another process.
30var ErrContended = errors.New("file lock is held by another process")
31
32// File acquires an exclusive advisory lock on the file at path, blocking
33// until the lock is acquired or ctx is cancelled. It returns a release
34// function that drops the lock and closes the underlying file descriptor.
35//
36// Pass a context with a deadline (e.g. context.WithTimeout) to bound the
37// wait. Pass context.Background() to block indefinitely.
38func File(ctx context.Context, path string) (func(), error) {
39 f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600)
40 if err != nil {
41 return nil, fmt.Errorf("open lock file %q: %w", path, err)
42 }
43
44 release, err := lockFile(ctx, f)
45 if err != nil {
46 f.Close()
47 return nil, err
48 }
49
50 return func() {
51 release()
52 f.Close()
53 }, nil
54}
55
56// TryFile is like File but returns ErrContended immediately if the lock
57// is already held by another process. Use this when you want to fail
58// fast rather than wait.
59func TryFile(path string) (func(), error) {
60 f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600)
61 if err != nil {
62 return nil, fmt.Errorf("open lock file %q: %w", path, err)
63 }
64
65 release, err := tryLockFile(f)
66 if err != nil {
67 f.Close()
68 return nil, err
69 }
70
71 return func() {
72 release()
73 f.Close()
74 }, nil
75}