diff options
| author | Claude <noreply@anthropic.com> | 2026-08-07 19:02:40 -0500 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-08-07 19:02:40 -0500 |
| commit | acde3827f60bc90738429fa490a36a1856a93283 (patch) | |
| tree | ae4b345b30a1308170ffe3e4c00cc1a8360c796b /internal/lock | |
| parent | d1e1c226d9a63432c652d373c039a3ae4430f3a2 (diff) | |
Add per-app flock locking around deploy, config change, rollback, new-app
Nothing previously serialized these against each other, so concurrent
invocations for the same app (a manual config change racing an
in-flight deploy, a double git push, a rollback mid-deploy) could
interleave snapshot/apply/restart/rollback sequences against the same
dataset and jail. The lock file lives under a new LocksDir, outside
any per-app ZFS dataset, since those datasets get rolled back on a
failed health check or a manual rollback.
Diffstat (limited to 'internal/lock')
| -rw-r--r-- | internal/lock/lock.go | 43 |
1 files changed, 43 insertions, 0 deletions
diff --git a/internal/lock/lock.go b/internal/lock/lock.go new file mode 100644 index 0000000..af24882 --- /dev/null +++ b/internal/lock/lock.go @@ -0,0 +1,43 @@ +// Package lock provides per-app exclusive locking, so that concurrent skiff +// invocations against the same app - a deploy racing a config change, either +// racing a manual rollback - can't interleave their snapshot/apply/restart +// sequences against the same dataset and jail. +package lock + +import ( + "fmt" + "os" + "path/filepath" + "syscall" +) + +// Handle is a held lock; call Release to give it up. +type Handle struct { + f *os.File +} + +// Acquire blocks until it holds an exclusive lock on path, creating the file +// (and its parent directory) if needed. The lock is held via flock(2) on the +// open file descriptor, so it's released automatically if the process dies - +// a crash never leaves a stale lock behind. +func Acquire(path string) (*Handle, error) { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return nil, fmt.Errorf("create lock dir: %w", err) + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_RDONLY, 0644) + if err != nil { + return nil, fmt.Errorf("open lock file: %w", err) + } + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { + f.Close() + return nil, fmt.Errorf("flock: %w", err) + } + return &Handle{f: f}, nil +} + +// Release gives up the lock. Safe to call via defer immediately after a +// successful Acquire. +func (h *Handle) Release() error { + defer h.f.Close() + return syscall.Flock(int(h.f.Fd()), syscall.LOCK_UN) +} |
