Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Cai <jcai@gitlab.com>2022-04-19 20:40:19 +0300
committerJohn Cai <jcai@gitlab.com>2022-04-21 05:42:13 +0300
commit1a7197e3b2171626a95fbab2b28ecbe43ea79aea (patch)
tree4aa72be3f59fcf4050d8fefffd73148838301b1e
parent6ff441e3d1303f9507c0ca2bf599d2fe8f5798d8 (diff)
safe: Add new LockingDirectory
Sometimes it's useful to be able to lock a directory before modifying it. This is similar to LockingFileWriter, except it just gives the ability to lock a directory so another process cannot also lock the directory. This will be immediately useful in RestoreCustomHooks where we are about ot add transactional voting to it. With transactional voting, the semantics are that the `Prepared` vote is made once the data that is about to be changed is locked. This will enable us to lock the custom hooks directory. Changelog: added
-rw-r--r--internal/safe/locking_directory.go95
-rw-r--r--internal/safe/locking_directory_test.go78
2 files changed, 173 insertions, 0 deletions
diff --git a/internal/safe/locking_directory.go b/internal/safe/locking_directory.go
new file mode 100644
index 000000000..b15483bce
--- /dev/null
+++ b/internal/safe/locking_directory.go
@@ -0,0 +1,95 @@
+package safe
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+)
+
+type lockingDirectoryState int
+
+const (
+ lockingDirectoryStateUnlocked lockingDirectoryState = iota
+ lockingDirectoryStateLocked
+)
+
+// LockingDirectory allows locking and unlocking a directory for safe access and
+// modification.
+type LockingDirectory struct {
+ state lockingDirectoryState
+ path string
+}
+
+// NewLockingDirectory creates a new LockingDirectory.
+func NewLockingDirectory(path string) (*LockingDirectory, error) {
+ fi, err := os.Stat(path)
+ if err != nil {
+ return nil, fmt.Errorf("creating new locking directory: %w", err)
+ }
+
+ if !fi.IsDir() {
+ return nil, errors.New("not a directory")
+ }
+
+ ld := &LockingDirectory{
+ state: lockingDirectoryStateUnlocked,
+ path: path,
+ }
+
+ return ld, nil
+}
+
+// Lock locks the directory and prevents a second process with a
+// LockingDirectory from also locking the directory.
+func (ld *LockingDirectory) Lock() error {
+ if ld.state != lockingDirectoryStateUnlocked {
+ return errors.New("locking directory not lockable")
+ }
+
+ lock, err := os.OpenFile(ld.lockPath(), os.O_CREATE|os.O_EXCL|os.O_RDONLY, 0o400)
+ if err != nil {
+ if os.IsExist(err) {
+ return ErrFileAlreadyLocked
+ }
+
+ return fmt.Errorf("creating lock file: %w", err)
+ }
+ _ = lock.Close()
+
+ ld.state = lockingDirectoryStateLocked
+
+ return nil
+}
+
+// IsLocked returns whether or not the directory has been locked.
+func (ld *LockingDirectory) IsLocked() bool {
+ return ld.state == lockingDirectoryStateLocked
+}
+
+// Unlock unlocks the directory.
+func (ld *LockingDirectory) Unlock() error {
+ if ld.state != lockingDirectoryStateLocked {
+ return errors.New("locking directory not locked")
+ }
+
+ if err := os.Remove(ld.lockPath()); err != nil {
+ // A previous call might have returned an error
+ // but still removed the file.
+ if errors.Is(err, fs.ErrNotExist) {
+ ld.state = lockingDirectoryStateUnlocked
+ return nil
+ }
+
+ return fmt.Errorf("unlocking directory: %w", err)
+ }
+
+ ld.state = lockingDirectoryStateUnlocked
+
+ return nil
+}
+
+func (ld *LockingDirectory) lockPath() string {
+ return filepath.Join(ld.path, ".lock")
+}
diff --git a/internal/safe/locking_directory_test.go b/internal/safe/locking_directory_test.go
new file mode 100644
index 000000000..e9280c3dd
--- /dev/null
+++ b/internal/safe/locking_directory_test.go
@@ -0,0 +1,78 @@
+package safe_test
+
+import (
+ "errors"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/gitaly/v14/internal/safe"
+ "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper"
+)
+
+func TestLockingDirectory(t *testing.T) {
+ t.Parallel()
+
+ t.Run("normal lifecycle", func(t *testing.T) {
+ path := testhelper.TempDir(t)
+ lockingDir, err := safe.NewLockingDirectory(path)
+ require.NoError(t, err)
+ require.NoError(t, lockingDir.Lock())
+ secondLockingDir, err := safe.NewLockingDirectory(path)
+ require.NoError(t, err)
+ require.NoError(t, os.WriteFile(
+ filepath.Join(path, "somefile"),
+ []byte("data"),
+ 0o644),
+ )
+ assert.ErrorIs(t, secondLockingDir.Lock(), safe.ErrFileAlreadyLocked)
+ require.NoError(t, lockingDir.Unlock())
+ })
+
+ t.Run("multiple locks fail", func(t *testing.T) {
+ path := testhelper.TempDir(t)
+ lockingDir, err := safe.NewLockingDirectory(path)
+ require.NoError(t, err)
+ require.NoError(t, lockingDir.Lock())
+ assert.Equal(
+ t,
+ errors.New("locking directory not lockable"),
+ lockingDir.Lock(),
+ )
+ })
+
+ t.Run("unlock without lock fails", func(t *testing.T) {
+ path := testhelper.TempDir(t)
+ lockingDir, err := safe.NewLockingDirectory(path)
+ require.NoError(t, err)
+ assert.Equal(
+ t,
+ errors.New("locking directory not locked"),
+ lockingDir.Unlock(),
+ )
+ })
+
+ t.Run("multiple unlocks fail", func(t *testing.T) {
+ path := testhelper.TempDir(t)
+ lockingDir, err := safe.NewLockingDirectory(path)
+ require.NoError(t, err)
+ require.NoError(t, lockingDir.Lock())
+ require.NoError(t, lockingDir.Unlock())
+ assert.Equal(
+ t,
+ errors.New("locking directory not locked"),
+ lockingDir.Unlock(),
+ )
+ })
+
+ t.Run("fails if directory is missing", func(t *testing.T) {
+ path := testhelper.TempDir(t)
+ require.NoError(t, os.RemoveAll(path))
+
+ _, err := safe.NewLockingDirectory(path)
+ assert.True(t, errors.Is(err, fs.ErrNotExist))
+ })
+}