internal/backuplock/backuplock.go

bd5cf5d7d1f34fa780660fd7562b9ffd9746ee27
gitbay/internal/backuplock/backuplock.go history · blame · raw

59 lines · 2015 bytes

 1// Package backuplock keeps repository deletes, renames, transfers and
 2// prunes out of a full backup's way (#259). The backup runs in its own
 3// process (gitbayd admin backup) and a delete in the daemon's, so the
 4// lock is flock(2) on a file under server.root: the backup holds it
 5// exclusively from its database snapshot until the last repository is
 6// archived, and each delete, move or prune holds it shared while it runs.
 7package backuplock
 8
 9import (
10	"errors"
11	"os"
12	"path/filepath"
13	"syscall"
14)
15
16// Name is the lock file under server.root. Backups skip it.
17const Name = "backup.lock"
18
19// ErrBusy is TryShared's answer while a backup holds the lock.
20var ErrBusy = errors.New("a backup is running; repositories cannot be deleted, renamed, moved or pruned until it finishes, usually within minutes")
21
22// open opens the lock file read-only, which is all flock needs, so the
23// daemon's user can lock a file a root-run backup created. O_NOFOLLOW
24// refuses a symlink planted at Name instead of following it, since the
25// path is inside server.root where only the daemon's own user writes.
26func open(root string) (*os.File, error) {
27	return os.OpenFile(filepath.Join(root, Name), os.O_RDONLY|os.O_CREATE|syscall.O_NOFOLLOW, 0o644)
28}
29
30// Hold takes the lock exclusively, waiting for deletes and moves under
31// way to finish. Closing the file releases it.
32func Hold(root string) (func(), error) {
33	f, err := open(root)
34	if err != nil {
35		return nil, err
36	}
37	if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
38		f.Close()
39		return nil, err
40	}
41	return func() { f.Close() }, nil
42}
43
44// TryShared takes the lock shared without waiting: ErrBusy while a
45// backup holds it.
46func TryShared(root string) (func(), error) {
47	f, err := open(root)
48	if err != nil {
49		return nil, err
50	}
51	if err := syscall.Flock(int(f.Fd()), syscall.LOCK_SH|syscall.LOCK_NB); err != nil {
52		f.Close()
53		if errors.Is(err, syscall.EWOULDBLOCK) {
54			return nil, ErrBusy
55		}
56		return nil, err
57	}
58	return func() { f.Close() }, nil
59}