Commit a166de255d
Verified · cmc
Layout: unified · split
internal/backuplock/backuplock.go added +59
| @@ -0,0 +1,59 @@ | |||
| 1 | // Package backuplock keeps repository deletes, renames and transfers | ||
| 2 | // out of a full backup's way (#259). The backup runs in its own process | ||
| 3 | // (gitbayd admin backup) and a delete in the daemon's, so the lock is | ||
| 4 | // flock(2) on a file under server.root: the backup holds it exclusively | ||
| 5 | // from its database snapshot until the last repository is archived, and | ||
| 6 | // each delete or move holds it shared while it runs. | ||
| 7 | package backuplock | ||
| 8 | |||
| 9 | import ( | ||
| 10 | "errors" | ||
| 11 | "os" | ||
| 12 | "path/filepath" | ||
| 13 | "syscall" | ||
| 14 | ) | ||
| 15 | |||
| 16 | // Name is the lock file under server.root. Backups skip it. | ||
| 17 | const Name = "backup.lock" | ||
| 18 | |||
| 19 | // ErrBusy is TryShared's answer while a backup holds the lock. | ||
| 20 | var ErrBusy = errors.New("a backup is running; repositories cannot be deleted, renamed or moved 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. | ||
| 26 | func 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. | ||
| 32 | func 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. | ||
| 46 | func 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 | } | ||
internal/backuplock/backuplock_test.go added +69
| @@ -0,0 +1,69 @@ | |||
| 1 | package backuplock | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "errors" | ||
| 5 | "testing" | ||
| 6 | "time" | ||
| 7 | ) | ||
| 8 | |||
| 9 | func TestTrySharedRefusedWhileHeld(t *testing.T) { | ||
| 10 | root := t.TempDir() | ||
| 11 | release, err := Hold(root) | ||
| 12 | if err != nil { | ||
| 13 | t.Fatal(err) | ||
| 14 | } | ||
| 15 | if _, err := TryShared(root); !errors.Is(err, ErrBusy) { | ||
| 16 | t.Fatalf("TryShared during a backup: %v", err) | ||
| 17 | } | ||
| 18 | release() | ||
| 19 | r, err := TryShared(root) | ||
| 20 | if err != nil { | ||
| 21 | t.Fatalf("TryShared after the backup: %v", err) | ||
| 22 | } | ||
| 23 | r() | ||
| 24 | } | ||
| 25 | |||
| 26 | func TestSharedHoldersCoexist(t *testing.T) { | ||
| 27 | root := t.TempDir() | ||
| 28 | a, err := TryShared(root) | ||
| 29 | if err != nil { | ||
| 30 | t.Fatal(err) | ||
| 31 | } | ||
| 32 | defer a() | ||
| 33 | b, err := TryShared(root) | ||
| 34 | if err != nil { | ||
| 35 | t.Fatalf("second shared holder: %v", err) | ||
| 36 | } | ||
| 37 | b() | ||
| 38 | } | ||
| 39 | |||
| 40 | // A backup waits for a delete already under way. | ||
| 41 | func TestHoldWaitsForSharedHolder(t *testing.T) { | ||
| 42 | root := t.TempDir() | ||
| 43 | shared, err := TryShared(root) | ||
| 44 | if err != nil { | ||
| 45 | t.Fatal(err) | ||
| 46 | } | ||
| 47 | got := make(chan struct{}) | ||
| 48 | go func() { | ||
| 49 | release, err := Hold(root) | ||
| 50 | if err != nil { | ||
| 51 | t.Error(err) | ||
| 52 | close(got) | ||
| 53 | return | ||
| 54 | } | ||
| 55 | close(got) | ||
| 56 | release() | ||
| 57 | }() | ||
| 58 | select { | ||
| 59 | case <-got: | ||
| 60 | t.Fatal("Hold returned while a shared holder was in") | ||
| 61 | case <-time.After(100 * time.Millisecond): | ||
| 62 | } | ||
| 63 | shared() | ||
| 64 | select { | ||
| 65 | case <-got: | ||
| 66 | case <-time.After(5 * time.Second): | ||
| 67 | t.Fatal("Hold never returned after the shared holder left") | ||
| 68 | } | ||
| 69 | } | ||