lock.go

 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}