| 1 | // Package identitylock combines filesystem path identity with filelock's |
| 2 | // process-local queue and cross-process advisory lock. |
| 3 | package identitylock |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "strings" |
| 11 | "time" |
| 12 | |
| 13 | "reasonix/internal/filelock" |
| 14 | "reasonix/internal/pathidentity" |
| 15 | ) |
| 16 | |
| 17 | type Mode = filelock.Mode |
| 18 | |
| 19 | const ( |
| 20 | ModeExclusive = filelock.ModeExclusive |
| 21 | ModeShared = filelock.ModeShared |
| 22 | ) |
| 23 | |
| 24 | var ErrHeld = filelock.ErrHeld |
| 25 | |
| 26 | var identityLockAfterResolve = func() {} |
| 27 | |
| 28 | func Acquire(ctx context.Context, path string) (func(), error) { |
| 29 | return AcquireMode(ctx, path, ModeExclusive) |
| 30 | } |
| 31 | |
| 32 | func AcquireMode(ctx context.Context, path string, mode Mode) (func(), error) { |
| 33 | accessPath, key, err := resolve(path) |
| 34 | if err != nil { |
| 35 | return nil, err |
| 36 | } |
| 37 | identityLockAfterResolve() |
| 38 | release, err := filelock.AcquireModeWithKey(ctx, accessPath, key, mode) |
| 39 | return revalidate(accessPath, key, release, err) |
| 40 | } |
| 41 | |
| 42 | func AcquireWithExternalTimeout(ctx context.Context, path string, timeout time.Duration) (func(), error) { |
| 43 | accessPath, key, err := resolve(path) |
| 44 | if err != nil { |
| 45 | return nil, err |
| 46 | } |
| 47 | identityLockAfterResolve() |
| 48 | release, err := filelock.AcquireWithExternalTimeoutAndKey(ctx, accessPath, key, timeout) |
| 49 | return revalidate(accessPath, key, release, err) |
| 50 | } |
| 51 | |
| 52 | func TryAcquire(path string) (func(), error) { |
| 53 | return TryAcquireMode(path, ModeExclusive) |
| 54 | } |
| 55 | |
| 56 | func TryAcquireMode(path string, mode Mode) (func(), error) { |
| 57 | accessPath, key, err := resolve(path) |
| 58 | if err != nil { |
| 59 | return nil, err |
| 60 | } |
| 61 | identityLockAfterResolve() |
| 62 | release, err := filelock.TryAcquireModeWithKey(accessPath, key, mode) |
| 63 | return revalidate(accessPath, key, release, err) |
| 64 | } |
| 65 | |
| 66 | func revalidate(accessPath, expectedKey string, release func(), acquireErr error) (func(), error) { |
| 67 | if acquireErr != nil { |
| 68 | return nil, acquireErr |
| 69 | } |
| 70 | _, actualKey, err := resolve(accessPath) |
| 71 | if err != nil { |
| 72 | release() |
| 73 | return nil, fmt.Errorf("revalidate file lock identity: %w", err) |
| 74 | } |
| 75 | if actualKey != expectedKey { |
| 76 | release() |
| 77 | return nil, fmt.Errorf("file lock identity changed while acquiring") |
| 78 | } |
| 79 | return release, nil |
| 80 | } |
| 81 | |
| 82 | func resolve(path string) (string, string, error) { |
| 83 | baseDir := "" |
| 84 | if !filepath.IsAbs(strings.TrimSpace(path)) { |
| 85 | var err error |
| 86 | baseDir, err = os.Getwd() |
| 87 | if err != nil { |
| 88 | return "", "", fmt.Errorf("resolve file lock identity: %w", err) |
| 89 | } |
| 90 | } |
| 91 | identity, err := pathidentity.Resolve(path, pathidentity.Options{BaseDir: baseDir, FollowLeaf: true}) |
| 92 | if err != nil { |
| 93 | return "", "", fmt.Errorf("resolve file lock identity: %w", err) |
| 94 | } |
| 95 | return identity.AccessPath, identity.Key, nil |
| 96 | } |
| 97 |