Skip to content

Commit a5c70d5

Browse files
committed
Deduplicate code
Signed-off-by: Jan Rodák <[email protected]>
1 parent 12889ed commit a5c70d5

File tree

10 files changed

+203
-281
lines changed

10 files changed

+203
-281
lines changed

internal/rawfilelock/rawfilelock.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package rawfilelock
2+
3+
import (
4+
"os"
5+
)
6+
7+
type LockType byte
8+
9+
const (
10+
ReadLock LockType = iota
11+
WriteLock
12+
)
13+
14+
type FileHandle = fileHandle
15+
16+
// OpenLock opens a file for locking
17+
// WARNING: This is the underlying file locking primitive of the OS;
18+
// because closing FileHandle releases the lock, it is not suitable for use
19+
// if there is any chance of two concurrent goroutines attempting to use the same lock.
20+
// Most users should use the higher-level operations from internal/staging_lockfile or pkg/lockfile.
21+
func OpenLock(path string, readOnly bool) (FileHandle, error) {
22+
flags := os.O_CREATE
23+
if readOnly {
24+
flags |= os.O_RDONLY
25+
} else {
26+
flags |= os.O_RDWR
27+
}
28+
29+
fd, err := openHandle(path, flags)
30+
if err == nil {
31+
return fd, nil
32+
}
33+
34+
return fd, &os.PathError{Op: "open", Path: path, Err: err}
35+
}
36+
37+
// TryLockFile attempts to lock a file handle
38+
func TryLockFile(fd FileHandle, lockType LockType) error {
39+
return lockHandle(fd, lockType, true)
40+
}
41+
42+
// LockFile locks a file handle
43+
func LockFile(fd FileHandle, lockType LockType) error {
44+
return lockHandle(fd, lockType, false)
45+
}
46+
47+
// UnlockAndClose unlocks and closes a file handle
48+
func UnlockAndCloseHandle(fd FileHandle) {
49+
unlockAndCloseHandle(fd)
50+
}
51+
52+
// CloseHandle closes a file handle without unlocking
53+
//
54+
// WARNING: This is a last-resort function for error handling only!
55+
// On Unix systems, closing a file descriptor automatically releases any locks,
56+
// so "closing without unlocking" is impossible. This function will release
57+
// the lock as a side effect of closing the file.
58+
//
59+
// This function should only be used in error paths where the lock state
60+
// is already corrupted or when giving up on lock management entirely.
61+
// Normal code should use UnlockAndCloseHandle instead.
62+
func CloseHandle(fd FileHandle) {
63+
closeHandle(fd)
64+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package rawfilelock
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestOpenLock(t *testing.T) {
12+
t.Parallel()
13+
14+
for _, tc := range []struct {
15+
name string
16+
prepare func() (path string, readOnly bool)
17+
}{
18+
{
19+
name: "file exists (read/write)",
20+
prepare: func() (string, bool) {
21+
tempFile, err := os.CreateTemp(t.TempDir(), "lock-")
22+
require.NoError(t, err)
23+
tempFile.Close()
24+
return tempFile.Name(), false
25+
},
26+
},
27+
{
28+
name: "file exists readonly (readonly)",
29+
prepare: func() (string, bool) {
30+
tempFile, err := os.CreateTemp(t.TempDir(), "lock-")
31+
require.NoError(t, err)
32+
tempFile.Close()
33+
return tempFile.Name(), true
34+
},
35+
},
36+
{
37+
name: "base dir exists (read/write)",
38+
prepare: func() (string, bool) {
39+
tempDir := os.TempDir()
40+
require.DirExists(t, tempDir)
41+
return filepath.Join(tempDir, "test-1.lock"), false
42+
},
43+
},
44+
} {
45+
path, readOnly := tc.prepare()
46+
47+
fd, err := OpenLock(path, readOnly)
48+
require.NoError(t, err, tc.name)
49+
UnlockAndCloseHandle(fd)
50+
51+
fd, err = OpenLock(path, readOnly)
52+
require.NoError(t, err)
53+
UnlockAndCloseHandle(fd)
54+
55+
require.Nil(t, os.RemoveAll(path))
56+
}
57+
}
58+
59+
func TestOpenLockNotCreateParentDir(t *testing.T) {
60+
tmpDir := t.TempDir()
61+
lockPath := filepath.Join(tmpDir, "lockfile")
62+
fd, err := OpenLock(lockPath, false)
63+
require.NoError(t, err)
64+
UnlockAndCloseHandle(fd)
65+
66+
lockPath = filepath.Join(tmpDir, "subdir", "lockfile")
67+
_, err = OpenLock(lockPath, false)
68+
require.Error(t, err)
69+
}
70+
71+
func TestTryLockFileAndLockFile(t *testing.T) {
72+
tmpFile, err := os.CreateTemp(t.TempDir(), "lockfile")
73+
require.NoError(t, err)
74+
defer os.Remove(tmpFile.Name())
75+
fd, err := OpenLock(tmpFile.Name(), false)
76+
require.NoError(t, err)
77+
78+
require.NoError(t, TryLockFile(fd, WriteLock))
79+
UnlockAndCloseHandle(fd)
80+
81+
fd2, err := OpenLock(tmpFile.Name(), false)
82+
require.NoError(t, err)
83+
84+
require.NoError(t, LockFile(fd2, WriteLock))
85+
UnlockAndCloseHandle(fd2)
86+
}

internal/staging_lockfile/staging_lockfile_unix.go renamed to internal/rawfilelock/rawfilelock_unix.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//go:build !windows
22

3-
package staging_lockfile
3+
package rawfilelock
44

55
import (
66
"time"
@@ -16,9 +16,9 @@ func openHandle(path string, mode int) (fileHandle, error) {
1616
return fileHandle(fd), err
1717
}
1818

19-
func lockHandle(fd fileHandle, lType lockType, nonblocking bool) error {
19+
func lockHandle(fd fileHandle, lType LockType, nonblocking bool) error {
2020
fType := unix.F_RDLCK
21-
if lType != readLock {
21+
if lType != ReadLock {
2222
fType = unix.F_WRLCK
2323
}
2424
lk := unix.Flock_t{

internal/staging_lockfile/staging_lockfile_windows.go renamed to internal/rawfilelock/rawfilelock_windows.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//go:build windows
22

3-
package staging_lockfile
3+
package rawfilelock
44

55
import (
66
"golang.org/x/sys/windows"
@@ -19,9 +19,9 @@ func openHandle(path string, mode int) (fileHandle, error) {
1919
return fileHandle(fd), err
2020
}
2121

22-
func lockHandle(fd fileHandle, lType lockType, nonblocking bool) error {
22+
func lockHandle(fd fileHandle, lType LockType, nonblocking bool) error {
2323
flags := 0
24-
if lType != readLock {
24+
if lType != ReadLock {
2525
flags = windows.LOCKFILE_EXCLUSIVE_LOCK
2626
}
2727
if nonblocking {

internal/staging_lockfile/staging_lockfile.go

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,10 @@ package staging_lockfile
22

33
import (
44
"fmt"
5-
"os"
65
"path/filepath"
76
"sync"
8-
)
9-
10-
type lockType byte
117

12-
const (
13-
readLock lockType = iota
14-
writeLock
8+
"github.com/containers/storage/internal/rawfilelock"
159
)
1610

1711
// StagingLockFile represents a file lock used to coordinate access to staging areas.
@@ -30,7 +24,7 @@ type StagingLockFile struct {
3024
// stateMutex is used to synchronize concurrent accesses to the state below
3125
stateMutex *sync.Mutex
3226
locked bool
33-
fd fileHandle
27+
fd rawfilelock.FileHandle
3428
}
3529

3630
var (
@@ -86,11 +80,11 @@ func getLockfile(path string) (*StagingLockFile, error) {
8680
// This function will be called at most once for each unique path within a process.
8781
func createStagingLockFileForPath(path string) (*StagingLockFile, error) {
8882
// Check if we can open the lock.
89-
fd, err := openLock(path)
83+
fd, err := rawfilelock.OpenLock(path, false)
9084
if err != nil {
9185
return nil, err
9286
}
93-
unlockAndCloseHandle(fd)
87+
rawfilelock.UnlockAndCloseHandle(fd)
9488

9589
return &StagingLockFile{
9690
file: path,
@@ -100,30 +94,6 @@ func createStagingLockFileForPath(path string) (*StagingLockFile, error) {
10094
}, nil
10195
}
10296

103-
// openLock opens the file at the specified path with read-write access.
104-
// It creates the file and any necessary parent directories if they don't exist.
105-
// Returns a file handle that can be used for locking.
106-
func openLock(path string) (fd fileHandle, err error) {
107-
flags := os.O_CREATE
108-
flags |= os.O_RDWR
109-
110-
fd, err = openHandle(path, flags)
111-
if err == nil {
112-
return fd, nil
113-
}
114-
115-
// the directory of the lockfile seems to be removed, try to create it
116-
if os.IsNotExist(err) {
117-
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
118-
return fd, fmt.Errorf("creating lock file directory: %w", err)
119-
}
120-
121-
return openLock(path)
122-
}
123-
124-
return fd, &os.PathError{Op: "open", Path: path, Err: err}
125-
}
126-
12797
// tryLock attempts to acquire an exclusive lock on the StagingLockFile without blocking.
12898
// It first tries to acquire the internal rwMutex, then opens and tries to lock the file.
12999
// Returns nil on success or an error if any step fails.
@@ -136,15 +106,15 @@ func (l *StagingLockFile) tryLock() error {
136106
}
137107
l.stateMutex.Lock()
138108
defer l.stateMutex.Unlock()
139-
fd, err := openLock(l.file)
109+
fd, err := rawfilelock.OpenLock(l.file, false)
140110
if err != nil {
141111
rwMutexUnlocker()
142112
return err
143113
}
144114
l.fd = fd
145115

146-
if err = lockHandle(l.fd, writeLock, true); err != nil {
147-
closeHandle(fd)
116+
if err = rawfilelock.TryLockFile(l.fd, rawfilelock.WriteLock); err != nil {
117+
rawfilelock.CloseHandle(fd)
148118
rwMutexUnlocker()
149119
return err
150120
}

internal/staging_lockfile/staging_lockfile_test.go

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"io"
55
"os"
66
"os/exec"
7-
"path/filepath"
87
"runtime"
98
"sync"
109
"sync/atomic"
@@ -229,50 +228,3 @@ func TestLockfileMultiProcess(t *testing.T) {
229228
wg.Wait()
230229
assert.True(t, whighest == 1, "expected to have no more than one writer lock active at a time, had %d", whighest)
231230
}
232-
233-
func TestOpenLock(t *testing.T) {
234-
t.Parallel()
235-
236-
for _, tc := range []struct {
237-
name string
238-
prepare func() (path string)
239-
}{
240-
{
241-
name: "file exists (read/write)",
242-
prepare: func() string {
243-
tempFile, err := os.CreateTemp("", "lock-")
244-
require.NoError(t, err)
245-
tempFile.Close()
246-
return tempFile.Name()
247-
},
248-
},
249-
{
250-
name: "base dir exists (read/write)",
251-
prepare: func() string {
252-
tempDir := os.TempDir()
253-
require.DirExists(t, tempDir)
254-
return filepath.Join(tempDir, "test-1.lock")
255-
},
256-
},
257-
{
258-
name: "base dir not exists (read/write)",
259-
prepare: func() string {
260-
tempDir, err := os.MkdirTemp("", "lock-")
261-
require.NoError(t, err)
262-
return filepath.Join(tempDir, "subdir", "test-1.lock")
263-
},
264-
},
265-
} {
266-
path := tc.prepare()
267-
268-
fd, err := openLock(path)
269-
require.NoError(t, err, tc.name)
270-
unlockAndCloseHandle(fd)
271-
272-
fd, err = openLock(path)
273-
require.NoError(t, err)
274-
unlockAndCloseHandle(fd)
275-
276-
require.Nil(t, os.RemoveAll(path))
277-
}
278-
}

0 commit comments

Comments
 (0)