-
Notifications
You must be signed in to change notification settings - Fork 0
/
lockfile.go
168 lines (149 loc) · 4.29 KB
/
lockfile.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package githttp
import (
"path/filepath"
"syscall"
"github.com/omegaup/go-base/v3"
)
// LockfileState represents the stat of the lockfile.
type LockfileState int
const (
invalidFD = -1
// LockfileStateUnlocked represents that a lockfile is not locked.
LockfileStateUnlocked LockfileState = iota
// LockfileStateReadLocked represents that a lockfile has acquired a read lock.
LockfileStateReadLocked
// LockfileStateLocked represents that a lockfile has acquired a read/write lock.
LockfileStateLocked
)
// LockfileManager is a container for Lockfiles, which allows them to be reused
// between calls safely.
type LockfileManager struct {
fdCache *base.KeyedPool[int]
}
// NewLockfileManager returns a new LockfileManager.
func NewLockfileManager() *LockfileManager {
return &LockfileManager{
fdCache: base.NewKeyedPool[int](base.KeyedPoolOptions[int]{
New: func(path string) (int, error) {
return syscall.Creat(path, 0600)
},
OnEvicted: func(path string, value int) {
syscall.Close(value)
},
}),
}
}
// Clear releases all the lockfiles in the pool.
func (m *LockfileManager) Clear() {
m.fdCache.Clear()
}
// Lockfile represents a file-based lock that can be up/downgraded. Since this
// is using the flock(2) system call and the promotion/demotion is non-atomic,
// any attempt to change the lock type must verify any preconditions after
// calling Lock()/RLock().
type Lockfile struct {
path string
fd int
state LockfileState
fdCache *base.KeyedPool[int]
}
// NewLockfile creates a new Lockfile that is initially unlocked.
func (m *LockfileManager) NewLockfile(repositoryPath string) *Lockfile {
return &Lockfile{
path: filepath.Join(repositoryPath, "githttp.lock"),
fd: invalidFD,
fdCache: m.fdCache,
}
}
func (l *Lockfile) open() error {
if l.fd != invalidFD {
return nil
}
// This will reuse a previous (unlocked) lockfile if possible. Otherwise, it
// will open a new one.
fd, err := l.fdCache.Get(l.path)
if err != nil {
return err
}
l.fd = fd
return nil
}
// TryRLock attempts to acquires a shared lock for the Lockfile's path. More
// than one process / goroutine may hold a shared lock for this Lockfile's path
// at any given time.
func (l *Lockfile) TryRLock() (bool, error) {
if err := l.open(); err != nil {
return false, err
}
if err := syscall.Flock(l.fd, syscall.LOCK_SH|syscall.LOCK_NB); err != nil {
if err == syscall.EWOULDBLOCK {
return false, nil
}
return false, err
}
l.state = LockfileStateReadLocked
return true, nil
}
// RLock acquires a shared lock for the Lockfile's path. More than one process
// / goroutine may hold a shared lock for this Lockfile's path at any given
// time.
func (l *Lockfile) RLock() error {
if err := l.open(); err != nil {
return err
}
if err := syscall.Flock(l.fd, syscall.LOCK_SH); err != nil {
return err
}
l.state = LockfileStateReadLocked
return nil
}
// TryLock attempts to acquire an exclusive lock for the Lockfile's path and
// returns whether it was able to do so. Only one process / goroutine may hold
// an exclusive lock for this Lockfile's path at any given time.
func (l *Lockfile) TryLock() (bool, error) {
if err := l.open(); err != nil {
return false, err
}
if err := syscall.Flock(l.fd, syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
if err == syscall.EWOULDBLOCK {
return false, nil
}
return false, err
}
l.state = LockfileStateLocked
return true, nil
}
// Lock acquires an exclusive lock for the Lockfile's path. Only one process /
// goroutine may hold an exclusive lock for this Lockfile's path at any given
// time.
func (l *Lockfile) Lock() error {
if err := l.open(); err != nil {
return err
}
if err := syscall.Flock(l.fd, syscall.LOCK_EX); err != nil {
return err
}
l.state = LockfileStateLocked
return nil
}
// Unlock releases a lock for the Lockfile's path.
func (l *Lockfile) Unlock() error {
if l.fd == invalidFD {
return nil
}
err := syscall.Flock(l.fd, syscall.LOCK_UN)
if err != nil {
// We could not remove the lock, so let's just close the fd.
syscall.Close(l.fd)
} else {
// The file is now unlocked. We can reuse it later.
l.fdCache.Put(l.path, l.fd)
}
l.fd = invalidFD
l.state = LockfileStateUnlocked
return err
}
// State returns the Lockfile's current state.
func (l *Lockfile) State() LockfileState {
return l.state
}