Skip to content

Commit 242419c

Browse files
committed
Add TryLockPath, CreateAndLock, UnlockAndDelete functions
Signed-off-by: Jan Rodák <[email protected]>
1 parent a5c70d5 commit 242419c

File tree

2 files changed

+209
-223
lines changed

2 files changed

+209
-223
lines changed

internal/staging_lockfile/staging_lockfile.go

Lines changed: 105 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package staging_lockfile
22

33
import (
44
"fmt"
5+
"os"
56
"path/filepath"
67
"sync"
78

@@ -19,14 +20,13 @@ type StagingLockFile struct {
1920
// They are safe to access without any other locking.
2021
file string
2122

22-
// rwMutex serializes concurrent reader-writer acquisitions in the same process space
23-
rwMutex *sync.RWMutex
24-
// stateMutex is used to synchronize concurrent accesses to the state below
25-
stateMutex *sync.Mutex
26-
locked bool
27-
fd rawfilelock.FileHandle
23+
// The following fields are only set when the lock is acquired, and must never be modified afterwards.
24+
locked bool
25+
fd rawfilelock.FileHandle
2826
}
2927

28+
const maxRetries = 1000
29+
3030
var (
3131
stagingLockFiles map[string]*StagingLockFile
3232
stagingLockFileLock sync.Mutex
@@ -41,7 +41,7 @@ func (l *StagingLockFile) AssertLocked() {
4141
//
4242
// Hence, this “AssertLocked” method, which exists only for sanity checks.
4343

44-
// Don’t even bother with l.stateMutex: The caller is expected to hold the lock, and in that case l.locked is constant true
44+
// The caller is expected to hold the lock, and in that case l.locked is constant true
4545
// with no possible writers.
4646
// If the caller does not hold the lock, we are violating the locking/memory model anyway, and accessing the data
4747
// without the lock is more efficient for callers, and potentially more visible to lock analysers for incorrect callers.
@@ -50,75 +50,124 @@ func (l *StagingLockFile) AssertLocked() {
5050
}
5151
}
5252

53-
// getLockfile returns a StagingLockFile object associated with the specified path.
54-
// It ensures only one StagingLockFile object exists per path within the process.
55-
// If a StagingLockFile for the path already exists, it returns that instance.
56-
// Otherwise, it creates a new one.
57-
func getLockfile(path string) (*StagingLockFile, error) {
53+
// tryAcquireLockForFile attempts to acquire a lock for the specified file path.
54+
// It first checks if the lock is already in use by another thread.
55+
// If the lock is not in use, it creates a new StagingLockFile, opens the file, and tries to lock it.
56+
// If successful, it adds the StagingLockFile to the global map of staging lock files.
57+
// If the lock is already in use, it returns an error.
58+
func tryAcquireLockForFile(cleanPath string) (*StagingLockFile, error) {
5859
stagingLockFileLock.Lock()
5960
defer stagingLockFileLock.Unlock()
61+
6062
if stagingLockFiles == nil {
6163
stagingLockFiles = make(map[string]*StagingLockFile)
6264
}
63-
cleanPath, err := filepath.Abs(path)
64-
if err != nil {
65-
return nil, fmt.Errorf("ensuring that path %q is an absolute path: %w", path, err)
65+
66+
if _, ok := stagingLockFiles[cleanPath]; ok {
67+
return nil, fmt.Errorf("lock is used already with other thread %q", cleanPath)
6668
}
67-
if lockFile, ok := stagingLockFiles[cleanPath]; ok {
68-
return lockFile, nil
69+
70+
lockFile := &StagingLockFile{
71+
file: cleanPath,
72+
locked: false,
6973
}
70-
lockFile, err := createStagingLockFileForPath(cleanPath) // platform-dependent LockFile
74+
75+
fd, err := rawfilelock.OpenLock(lockFile.file, false)
7176
if err != nil {
7277
return nil, err
7378
}
79+
lockFile.fd = fd
80+
81+
if err = rawfilelock.TryLockFile(lockFile.fd, rawfilelock.WriteLock); err != nil {
82+
rawfilelock.CloseHandle(lockFile.fd) // This is safe because we hold stagingLockFileLock so we are the only possible holder of the lock within this process.
83+
return nil, fmt.Errorf("failed to acquire lock on %q: %w", cleanPath, err)
84+
}
85+
86+
lockFile.locked = true
87+
7488
stagingLockFiles[cleanPath] = lockFile
7589
return lockFile, nil
7690
}
7791

78-
// createStagingLockFileForPath creates a new StagingLockFile instance for the given path.
79-
// It verifies that the file can be opened before returning the StagingLockFile.
80-
// This function will be called at most once for each unique path within a process.
81-
func createStagingLockFileForPath(path string) (*StagingLockFile, error) {
82-
// Check if we can open the lock.
83-
fd, err := rawfilelock.OpenLock(path, false)
84-
if err != nil {
85-
return nil, err
92+
// UnlockAndDelete releases the lock, removes the associated file from the filesystem,
93+
// and removes this StagingLockFile from the global map of StagingLockFile.
94+
//
95+
// WARNING: After this operation, the StagingLockFile becomes invalid for further use as the file field is cleared.
96+
func (l *StagingLockFile) UnlockAndDelete() error {
97+
stagingLockFileLock.Lock()
98+
defer stagingLockFileLock.Unlock()
99+
100+
if !l.locked {
101+
// Panic when unlocking an unlocked lock. That's a violation
102+
// of the lock semantics and will reveal such.
103+
panic("calling Unlock on unlocked lock")
86104
}
87-
rawfilelock.UnlockAndCloseHandle(fd)
88-
89-
return &StagingLockFile{
90-
file: path,
91-
rwMutex: &sync.RWMutex{},
92-
stateMutex: &sync.Mutex{},
93-
locked: false,
94-
}, nil
95-
}
96105

97-
// tryLock attempts to acquire an exclusive lock on the StagingLockFile without blocking.
98-
// It first tries to acquire the internal rwMutex, then opens and tries to lock the file.
99-
// Returns nil on success or an error if any step fails.
100-
func (l *StagingLockFile) tryLock() error {
101-
success := l.rwMutex.TryLock()
102-
rwMutexUnlocker := l.rwMutex.Unlock
106+
delete(stagingLockFiles, l.file)
107+
l.locked = false
103108

104-
if !success {
105-
return fmt.Errorf("resource temporarily unavailable")
106-
}
107-
l.stateMutex.Lock()
108-
defer l.stateMutex.Unlock()
109-
fd, err := rawfilelock.OpenLock(l.file, false)
110-
if err != nil {
111-
rwMutexUnlocker()
109+
defer func() {
110+
rawfilelock.UnlockAndCloseHandle(l.fd)
111+
l.file = ""
112+
}()
113+
if err := os.Remove(l.file); err != nil && !os.IsNotExist(err) {
112114
return err
113115
}
114-
l.fd = fd
116+
return nil
117+
}
115118

116-
if err = rawfilelock.TryLockFile(l.fd, rawfilelock.WriteLock); err != nil {
117-
rawfilelock.CloseHandle(fd)
118-
rwMutexUnlocker()
119-
return err
119+
// CreateAndLock creates a new temporary file in the specified directory with the given pattern,
120+
// then creates and locks a StagingLockFile for it. The file is created using os.CreateTemp.
121+
// Caller MUST call UnlockAndDelete() on the returned StagingLockFile to release the lock and delete the file.
122+
//
123+
// Returns:
124+
// - The locked StagingLockFile
125+
// - The absolute path to the created file
126+
// - Any error that occurred during the process
127+
//
128+
// If the file cannot be locked, this function will retry up to maxRetries times before failing.
129+
func CreateAndLock(dir string, pattern string) (*StagingLockFile, string, error) {
130+
for try := 0; ; try++ {
131+
file, err := os.CreateTemp(dir, pattern)
132+
if err != nil {
133+
return nil, "", err
134+
}
135+
file.Close()
136+
137+
cleanPath, err := filepath.Abs(file.Name())
138+
if err != nil {
139+
return nil, "", err
140+
}
141+
142+
l, err := tryAcquireLockForFile(cleanPath)
143+
if err != nil {
144+
if try < maxRetries {
145+
continue // Retry if the lock cannot be acquired
146+
}
147+
stagingLockFileLock.Lock()
148+
delete(stagingLockFiles, cleanPath)
149+
stagingLockFileLock.Unlock()
150+
return nil, "", fmt.Errorf(
151+
"failed to allocate lock in %q after %d attempts; last failure on %q: %w",
152+
dir, try, cleanPath, err,
153+
)
154+
}
155+
156+
return l, cleanPath, nil
120157
}
158+
}
121159

122-
l.locked = true
123-
return nil
160+
// TryLockPath attempts to acquire a lock on an existing file. If the file does not exist,
161+
// it will be created.
162+
//
163+
// Warning: If acquiring a lock is successful, it returns a new StagingLockFile
164+
// instance for the file. Caller MUST call UnlockAndDelete() on the returned StagingLockFile
165+
// to release the lock and delete the file.
166+
func TryLockPath(path string) (*StagingLockFile, error) {
167+
cleanPath, err := filepath.Abs(path)
168+
if err != nil {
169+
return nil, fmt.Errorf("ensuring that path %q is an absolute path: %w", path, err)
170+
}
171+
172+
return tryAcquireLockForFile(cleanPath)
124173
}

0 commit comments

Comments
 (0)