Skip to content

Commit 066402e

Browse files
authored
Add WaitLock function (#44)
* Add WaitLock function WaitLock keeps trying to acquire the lock that is held by someone else, until the lock is acquired or until the context is canceled. Retires once per second. Logs warning on each retry. - Update dependencies. - Update error checks - README.md to remove mention of gx - Update example to close lock file
1 parent 197dac0 commit 066402e

File tree

6 files changed

+222
-92
lines changed

6 files changed

+222
-92
lines changed

README.md

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,8 @@
3030
go get github.com/ipfs/go-fs-lock
3131
```
3232

33-
Note that `go-fs-lock` is packaged with Gx, so it is recommended to use Gx to install and use it (see Usage section).
34-
3533
## Usage
3634

37-
### Using Gx and Gx-go
38-
39-
This module is packaged with [Gx](https://github.com/whyrusleeping/gx). In order to use it in your own project it is recommended that you:
40-
41-
```sh
42-
go get -u github.com/whyrusleeping/gx
43-
go get -u github.com/whyrusleeping/gx-go
44-
cd <your-project-repository>
45-
gx init
46-
gx import github.com/ipfs/go-fs-lock
47-
gx install --global
48-
gx-go --rewrite
49-
```
50-
51-
Please check [Gx](https://github.com/whyrusleeping/gx) and [Gx-go](https://github.com/whyrusleeping/gx-go) documentation for more information.
52-
5335
### Running tests
5436

5537
Before running tests, please run:

example_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import (
99
)
1010

1111
func ExampleLockedError() {
12-
_, err := fslock.Lock(os.TempDir(), "foo.lock")
12+
lockFile, err := fslock.Lock(os.TempDir(), "foo.lock")
1313
fmt.Println("locked:", errors.As(err, new(fslock.LockedError)))
14+
defer lockFile.Close()
1415

1516
_, err = fslock.Lock(os.TempDir(), "foo.lock")
1617
fmt.Println("locked:", errors.As(err, new(fslock.LockedError)))

fslock.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package fslock
22

33
import (
4+
"context"
45
"errors"
56
"io"
7+
"io/fs"
68
"os"
79
"path/filepath"
810
"strings"
11+
"time"
912

10-
util "github.com/ipfs/go-ipfs-util"
1113
logging "github.com/ipfs/go-log/v2"
1214
lock "go4.org/lock"
1315
)
@@ -42,7 +44,7 @@ func Lock(confdir, lockFileName string) (io.Closer, error) {
4244
Path: lockFilePath,
4345
Err: LockedError("lock is already held by us"),
4446
}
45-
case os.IsPermission(err) || isLockCreatePermFail(err):
47+
case errors.Is(err, fs.ErrPermission) || isLockCreatePermFail(err):
4648
// lock fails on permissions error
4749

4850
// Using a path error like this ensures that
@@ -60,7 +62,7 @@ func Lock(confdir, lockFileName string) (io.Closer, error) {
6062
// Locked checks if there is a lock already set.
6163
func Locked(confdir, lockFile string) (bool, error) {
6264
log.Debugf("Checking lock")
63-
if !util.FileExists(filepath.Join(confdir, lockFile)) {
65+
if !fileExists(filepath.Join(confdir, lockFile)) {
6466
log.Debugf("File doesn't exist: %s", filepath.Join(confdir, lockFile))
6567
return false, nil
6668
}
@@ -80,7 +82,46 @@ func Locked(confdir, lockFile string) (bool, error) {
8082
return false, err
8183
}
8284

85+
// WaitLock keeps trying to acquire the lock that is held by someone else,
86+
// until the lock is acquired or until the context is canceled. Retires once
87+
// per second. Logs warning on each retry.
88+
func WaitLock(ctx context.Context, confdir, lockFileName string) (io.Closer, error) {
89+
var ticker *time.Ticker
90+
91+
retry:
92+
lk, err := Lock(confdir, lockFileName)
93+
if err != nil {
94+
var lkErr LockedError
95+
if errors.As(err, &lkErr) && lkErr.Error() == "someone else has the lock" {
96+
pe, ok := err.(*os.PathError)
97+
if !ok {
98+
return nil, err
99+
}
100+
log.Warnf("%s: %s. Retrying...", pe.Path, lkErr.Error())
101+
if ticker == nil {
102+
ticker = time.NewTicker(time.Second)
103+
defer ticker.Stop()
104+
}
105+
select {
106+
case <-ctx.Done():
107+
return nil, ctx.Err()
108+
case <-ticker.C:
109+
goto retry
110+
}
111+
}
112+
}
113+
return lk, err
114+
}
115+
83116
func isLockCreatePermFail(err error) bool {
84117
s := err.Error()
85118
return strings.Contains(s, "Lock Create of") && strings.Contains(s, "permission denied")
86119
}
120+
121+
func fileExists(filename string) bool {
122+
fi, err := os.Lstat(filename)
123+
if fi != nil || (err != nil && !errors.Is(err, fs.ErrNotExist)) {
124+
return true
125+
}
126+
return false
127+
}

fslock_test.go

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package fslock_test
22

33
import (
44
"bufio"
5+
"context"
6+
"errors"
57
"os"
68
"os/exec"
7-
"path"
9+
"path/filepath"
10+
"runtime"
811
"strings"
912
"testing"
1013
"time"
@@ -27,10 +30,7 @@ func assertLock(t *testing.T, confdir, lockFile string, expected bool) {
2730

2831
func TestLockSimple(t *testing.T) {
2932
lockFile := "my-test.lock"
30-
confdir := os.TempDir()
31-
32-
// make sure we start clean
33-
_ = os.Remove(path.Join(confdir, lockFile))
33+
confdir := t.TempDir()
3434

3535
assertLock(t, confdir, lockFile, false)
3636

@@ -66,11 +66,7 @@ func TestLockSimple(t *testing.T) {
6666
func TestLockMultiple(t *testing.T) {
6767
lockFile1 := "test-1.lock"
6868
lockFile2 := "test-2.lock"
69-
confdir := os.TempDir()
70-
71-
// make sure we start clean
72-
_ = os.Remove(path.Join(confdir, lockFile1))
73-
_ = os.Remove(path.Join(confdir, lockFile2))
69+
confdir := t.TempDir()
7470

7571
lock1, err := lock.Lock(confdir, lockFile1)
7672
if err != nil {
@@ -116,11 +112,7 @@ func TestLockedByOthers(t *testing.T) {
116112
return
117113
}
118114

119-
confdir, err := os.MkdirTemp("", "go-fs-lock-test")
120-
if err != nil {
121-
t.Fatalf("creating temporary directory: %v", err)
122-
}
123-
defer os.RemoveAll(confdir)
115+
confdir := t.TempDir()
124116

125117
// Execute a child process that locks the file.
126118
cmd := exec.Command(os.Args[0], "-test.run=TestLockedByOthers", "--", confdir)
@@ -158,3 +150,116 @@ func TestLockedByOthers(t *testing.T) {
158150
t.Fatalf("error %q does not contain %q", got, wantErr)
159151
}
160152
}
153+
154+
func TestWaitLock(t *testing.T) {
155+
const (
156+
lockedMsg = "locked\n"
157+
lockFile = "my-test.lock"
158+
someFile = "somefile"
159+
permErr = "permission denied"
160+
heldErr = "lock is already held by us"
161+
)
162+
163+
confdir := t.TempDir()
164+
165+
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
166+
defer cancel()
167+
168+
// Acquire lock.
169+
_, err := lock.WaitLock(ctx, confdir, confdir)
170+
if err == nil {
171+
t.Fatalf("acquired the lock on bad filename")
172+
}
173+
174+
someFilePath := filepath.Join(confdir, someFile)
175+
f, err := os.Create(someFilePath)
176+
if err != nil {
177+
t.Fatal("error opening file:", err)
178+
}
179+
f.Chmod(0444)
180+
err = f.Close()
181+
defer os.Remove(someFilePath)
182+
if err != nil {
183+
t.Fatal("error closing file:", err)
184+
}
185+
_, err = lock.WaitLock(ctx, confdir, someFile)
186+
if err == nil {
187+
t.Fatalf("acquired lock on read-only file")
188+
}
189+
if runtime.GOOS != "windows" {
190+
pe, ok := err.(*os.PathError)
191+
if !ok {
192+
t.Fatalf("wrong error type %T: %s", err, err)
193+
}
194+
if got := pe.Error(); !strings.Contains(got, permErr) {
195+
t.Fatalf("error %q does not contain %q", got, permErr)
196+
}
197+
}
198+
199+
// Acquire lock.
200+
lk, err := lock.WaitLock(ctx, confdir, lockFile)
201+
if err != nil {
202+
t.Fatal(err)
203+
}
204+
205+
// Try to acquire locked lock.
206+
_, err = lock.WaitLock(ctx, confdir, lockFile)
207+
if err == nil {
208+
t.Fatalf("acquired the lock when already locked")
209+
}
210+
pe, ok := err.(*os.PathError)
211+
if !ok {
212+
t.Fatalf("wrong error type %T", err)
213+
}
214+
if got := pe.Error(); !strings.Contains(got, heldErr) {
215+
t.Fatalf("error %q does not contain %q", got, heldErr)
216+
}
217+
218+
// Release lock.
219+
err = lk.Close()
220+
if err != nil {
221+
t.Fatal(err)
222+
}
223+
224+
if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" { // child process
225+
confdir := os.Args[3]
226+
if _, err := lock.Lock(confdir, lockFile); err != nil {
227+
t.Fatalf("child lock: %v", err)
228+
}
229+
os.Stdout.WriteString(lockedMsg)
230+
time.Sleep(10 * time.Minute)
231+
return
232+
}
233+
234+
// Execute a child process that locks the file.
235+
cmd := exec.Command(os.Args[0], "-test.run=TestLockedByOthers", "--", confdir)
236+
cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
237+
cmd.Stderr = os.Stderr
238+
stdout, err := cmd.StdoutPipe()
239+
if err != nil {
240+
t.Fatalf("cmd.StdoutPipe: %v", err)
241+
}
242+
if err = cmd.Start(); err != nil {
243+
t.Fatalf("cmd.Start: %v", err)
244+
}
245+
defer cmd.Process.Kill()
246+
247+
// Wait for the child to lock the file.
248+
b := bufio.NewReader(stdout)
249+
line, err := b.ReadString('\n')
250+
if err != nil {
251+
t.Fatalf("read from child: %v", err)
252+
}
253+
if got, want := line, lockedMsg; got != want {
254+
t.Fatalf("got %q from child; want %q", got, want)
255+
}
256+
257+
// Parent should not be able to lock the file.
258+
_, err = lock.WaitLock(ctx, confdir, lockFile)
259+
if err == nil {
260+
t.Fatalf("parent successfully acquired the lock")
261+
}
262+
if !errors.Is(err, context.DeadlineExceeded) {
263+
t.Fatalf("did not get expected error: %s", context.DeadlineExceeded)
264+
}
265+
}

go.mod

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
module github.com/ipfs/go-fs-lock
22

3+
go 1.23.8
4+
35
require (
4-
github.com/ipfs/go-ipfs-util v0.0.2
5-
github.com/ipfs/go-log/v2 v2.3.0
6-
go4.org v0.0.0-20200411211856-f5505b9728dd
7-
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f
6+
github.com/ipfs/go-log/v2 v2.5.1
7+
go4.org v0.0.0-20230225012048-214862532bf5
8+
golang.org/x/sys v0.32.0
89
)
910

1011
require (
11-
github.com/mattn/go-isatty v0.0.13 // indirect
12-
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
13-
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771 // indirect
14-
github.com/mr-tron/base58 v1.1.3 // indirect
15-
github.com/multiformats/go-multihash v0.0.13 // indirect
16-
github.com/multiformats/go-varint v0.0.5 // indirect
17-
github.com/spaolacci/murmur3 v1.1.0 // indirect
12+
github.com/mattn/go-isatty v0.0.14 // indirect
1813
go.uber.org/atomic v1.7.0 // indirect
1914
go.uber.org/multierr v1.6.0 // indirect
20-
go.uber.org/zap v1.16.0 // indirect
21-
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
15+
go.uber.org/zap v1.19.1 // indirect
2216
)
23-
24-
go 1.23

0 commit comments

Comments
 (0)