@@ -2,6 +2,7 @@ package staging_lockfile
2
2
3
3
import (
4
4
"fmt"
5
+ "os"
5
6
"path/filepath"
6
7
"sync"
7
8
@@ -19,14 +20,13 @@ type StagingLockFile struct {
19
20
// They are safe to access without any other locking.
20
21
file string
21
22
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
28
26
}
29
27
28
+ const maxRetries = 1000
29
+
30
30
var (
31
31
stagingLockFiles map [string ]* StagingLockFile
32
32
stagingLockFileLock sync.Mutex
@@ -41,7 +41,7 @@ func (l *StagingLockFile) AssertLocked() {
41
41
//
42
42
// Hence, this “AssertLocked” method, which exists only for sanity checks.
43
43
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
45
45
// with no possible writers.
46
46
// If the caller does not hold the lock, we are violating the locking/memory model anyway, and accessing the data
47
47
// 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() {
50
50
}
51
51
}
52
52
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 ) {
58
59
stagingLockFileLock .Lock ()
59
60
defer stagingLockFileLock .Unlock ()
61
+
60
62
if stagingLockFiles == nil {
61
63
stagingLockFiles = make (map [string ]* StagingLockFile )
62
64
}
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 )
66
68
}
67
- if lockFile , ok := stagingLockFiles [cleanPath ]; ok {
68
- return lockFile , nil
69
+
70
+ lockFile := & StagingLockFile {
71
+ file : cleanPath ,
72
+ locked : false ,
69
73
}
70
- lockFile , err := createStagingLockFileForPath (cleanPath ) // platform-dependent LockFile
74
+
75
+ fd , err := rawfilelock .OpenLock (lockFile .file , false )
71
76
if err != nil {
72
77
return nil , err
73
78
}
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
+
74
88
stagingLockFiles [cleanPath ] = lockFile
75
89
return lockFile , nil
76
90
}
77
91
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" )
86
104
}
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
- }
96
105
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
103
108
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 ) {
112
114
return err
113
115
}
114
- l .fd = fd
116
+ return nil
117
+ }
115
118
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
120
157
}
158
+ }
121
159
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 )
124
173
}
0 commit comments