-
Notifications
You must be signed in to change notification settings - Fork 113
/
filelocks.go
193 lines (164 loc) · 5.3 KB
/
filelocks.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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package filelocks
import (
"errors"
"io/fs"
"os"
"sync"
"time"
"github.com/gofrs/flock"
)
// LockFileSuffix to use for lock files
const LockFileSuffix = ".flock"
var (
_localLocks sync.Map
// waiting 20 lock cycles with a factor of 30 yields 6300ms, or a little over 6 sec
_lockCycles sync.Once
_lockCyclesValue = 20
_lockCycleDuration sync.Once
_lockCycleDurationFactor = 30
// ErrPathEmpty indicates that no path was specified
ErrPathEmpty = errors.New("lock path is empty")
// ErrAcquireLockFailed indicates that it was not possible to lock the resource.
ErrAcquireLockFailed = errors.New("unable to acquire a lock on the file")
)
// SetMaxLockCycles configures the maximum amount of lock cycles. Subsequent calls to SetMaxLockCycles have no effect
func SetMaxLockCycles(v int) {
_lockCycles.Do(func() {
_lockCyclesValue = v
})
}
// SetLockCycleDurationFactor configures the factor applied to the timeout allowed during a lock cycle. Subsequent calls to SetLockCycleDurationFactor have no effect
func SetLockCycleDurationFactor(v int) {
_lockCycleDuration.Do(func() {
_lockCycleDurationFactor = v
})
}
// getMutexedFlock returns a new Flock struct for the given file.
// If there is already one in the local store, it returns nil.
// The caller has to wait until it can get a new one out of this
// mehtod.
func getMutexedFlock(file string) *flock.Flock {
// Is there lock already?
if _, ok := _localLocks.Load(file); ok {
// There is already a lock for this file, another can not be acquired
return nil
}
// Acquire the write log on the target node first.
l := flock.New(file)
_localLocks.Store(file, l)
return l
}
// releaseMutexedFlock releases a Flock object that was acquired
// before by the getMutexedFlock function.
func releaseMutexedFlock(file string) {
if len(file) > 0 {
_localLocks.Delete(file)
}
}
// acquireWriteLog acquires a lock on a file or directory.
// if the parameter write is true, it gets an exclusive write lock, otherwise a shared read lock.
// The function returns a Flock object, unlocking has to be done in the calling function.
func acquireLock(file string, write bool) (*flock.Flock, error) {
var err error
// Create a file to carry the log
n := FlockFile(file)
if len(n) == 0 {
return nil, ErrPathEmpty
}
var flock *flock.Flock
for i := 1; i <= _lockCyclesValue; i++ {
if flock = getMutexedFlock(n); flock != nil {
break
}
w := time.Duration(i*_lockCycleDurationFactor) * time.Millisecond
time.Sleep(w)
}
if flock == nil {
return nil, ErrAcquireLockFailed
}
var ok bool
for i := 1; i <= _lockCyclesValue; i++ {
if write {
ok, err = flock.TryLock()
} else {
ok, err = flock.TryRLock()
}
if ok {
break
}
time.Sleep(time.Duration(i*_lockCycleDurationFactor) * time.Millisecond)
}
if !ok {
err = ErrAcquireLockFailed
}
if err != nil {
return nil, err
}
return flock, nil
}
// FlockFile returns the flock filename for a given file name
// it returns an empty string if the input is empty
func FlockFile(file string) string {
if file == "" {
return ""
}
return file + LockFileSuffix
}
// AcquireReadLock tries to acquire a shared lock to read from the
// file and returns a lock object or an error accordingly.
// Call with the file to lock. This function creates .lock file next
// to it.
func AcquireReadLock(file string) (*flock.Flock, error) {
return acquireLock(file, false)
}
// AcquireWriteLock tries to acquire a shared lock to write from the
// file and returns a lock object or an error accordingly.
// Call with the file to lock. This function creates an extra .lock
// file next to it.
func AcquireWriteLock(file string) (*flock.Flock, error) {
return acquireLock(file, true)
}
// ReleaseLock releases a lock from a file that was previously created
// by AcquireReadLock or AcquireWriteLock.
func ReleaseLock(lock *flock.Flock) error {
if lock == nil {
return errors.New("cannot unlock nil lock")
}
// there is a probability that if the file can not be unlocked,
// we also can not remove the file. We will only try to remove if it
// was successfully unlocked.
var err error
n := lock.Path()
// There is already a lock for this file
err = lock.Unlock()
if err == nil {
if !lock.Locked() && !lock.RLocked() {
err = os.Remove(n)
// there is a concurrency issue when deleting the file
// see https://github.com/owncloud/ocis/issues/3757
// for now we just ignore "not found" errors when they pop up
if err != nil && errors.Is(err, fs.ErrNotExist) {
err = nil
}
}
}
releaseMutexedFlock(n)
return err
}